kunt

golang IRC bot
git clone git://git.2f30.org/kunt
Log | Files | Refs | LICENSE

hangman.go (6093B)


      1 // Copyright 2013 TLH and dsp. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package games
      6 
      7 import (
      8 	"bytes"
      9 	"fmt"
     10 	"irc"
     11 	"strings"
     12 	"time"
     13 )
     14 
     15 // Just a function with a gamestate pointer as input
     16 type hgstateFn func(*hangmanState)
     17 
     18 // Specific state for hangman game
     19 type hangmanState struct {
     20 	gameState
     21 	lives    uint            // Lives remaining
     22 	tried    string          // Tried characters
     23 	goal     string          // Goal word
     24 	foundnow string          // Found now chars (_ if not found)
     25 	botname  string          // Name of irc bot for privmsg
     26 	channame string          // Name of invoking chan for privmsg
     27 	statchan *chan hgstateFn // Channel of next game state
     28 }
     29 
     30 // Creates a new hangman state with default values
     31 func newHangmanState(ic *irc.Context, gc *chan irc.Message, sc *chan hgstateFn) *hangmanState {
     32 	return &hangmanState{gameState{ic, gc, "hangman", "dsp"},
     33 		5, "", "", "", "unnamed", "", sc}
     34 }
     35 
     36 // For printf style functions, a short status of the game state
     37 func (s *hangmanState) String() string {
     38 	return fmt.Sprintf(">>[%s]<< lives: %d - tried: [%s]", s.foundnow, s.lives, s.tried)
     39 }
     40 
     41 // First hangman state. Awaits on the game chan for 2 special irc messages to set the nickname
     42 // of the bot and the channel on which to send msgs. Then it emits the next state (hangmanWaitGoal)
     43 // to the state channel.
     44 func hangmanInit(cs *hangmanState) {
     45 	for msg := range *cs.gchan {
     46 		if msg.Command == "SETNICK" {
     47 			cs.botname = msg.Args[0]
     48 		}
     49 		if msg.Command == "SETCHAN" {
     50 			cs.channame = msg.Args[0]
     51 		}
     52 		if cs.botname != "" && cs.channame != "" {
     53 			break
     54 		}
     55 	}
     56 	*cs.statchan <- hangmanWaitGoal
     57 }
     58 
     59 // Second hangman state. It waits on a private query for the goal and then emits the next state (hangmanMainLoop)
     60 // to the state channel.
     61 func hangmanWaitGoal(cs *hangmanState) {
     62 	cs.ictx.Privmsg(cs.channame, "Hangman is waiting for goal. When ready use hangman <letter|possibleword>.")
     63 	for msg := range *cs.gchan {
     64 		if len(msg.Args) < 2 {
     65 			continue
     66 		}
     67 		msg.Args[1] = strings.TrimSpace(msg.Args[1])
     68 		if msg.Args[0] == cs.botname {
     69 			if strings.HasPrefix(msg.Args[1], "setgoal ") {
     70 				cs.goal = msg.Args[1][8:]
     71 				who := msg.Nick
     72 				cs.ictx.Privmsg(who, "Goal is now set to: "+cs.goal)
     73 				runes := make([]rune, len(cs.goal))
     74 				for i := 0; i < len(runes); i++ {
     75 					runes[i] = '_'
     76 				}
     77 				cs.foundnow = string(runes)
     78 				break
     79 			} else {
     80 				who := msg.Nick
     81 				cs.ictx.Privmsg(who, "Now we are playing hangman. Use setgoal <WORD>")
     82 			}
     83 		} else {
     84 			if strings.HasPrefix(msg.Args[1], "hangman ") {
     85 				cs.ictx.Privmsg(msg.Args[0], "Still waiting for goal...")
     86 			}
     87 		}
     88 	}
     89 	*cs.statchan <- hangmanMainLoop
     90 }
     91 
     92 // Third hangman state. It waits for hangman prefixed messages on the game channel. If a word is provided
     93 // and it doesn't match the goal it reduces lives and continues. If it matches the game is won.
     94 // If a character is provided, it checks on the tried characters and if it is in it continues without losing.
     95 // If it is not in the tried characters , checks for it with checkLetter(this updates the tried characters)
     96 // and reprints the game state. At each loop if foundNow matches the goal the game is won.
     97 // Then it emits the final state (hangmanEnd) to the state channel.
     98 func hangmanMainLoop(cs *hangmanState) {
     99 	cs.ictx.Privmsg(cs.channame, cs.String())
    100 	for msg := range *cs.gchan {
    101 		if len(msg.Args) < 2 { //this was a bug when kunt was getting NICK xxxx Args[1] was  nil
    102 			continue
    103 		}
    104 		msg.Args[1] = strings.TrimSpace(msg.Args[1])
    105 		if msg.Args[0] != cs.botname {
    106 			if cs.lives < 1 {
    107 				cs.ictx.Privmsg(msg.Args[0], "You are hanged! Goal was ->"+cs.goal)
    108 				*cs.statchan <- hangmanEnd
    109 			}
    110 			if strings.HasPrefix(msg.Args[1], "hangman") {
    111 				in := msg.Args[1][8:]
    112 				if len(in) > 1 {
    113 					if in == cs.goal {
    114 						cs.ictx.Privmsg(msg.Args[0], "You won!")
    115 						*cs.statchan <- hangmanEnd
    116 					} else {
    117 						cs.ictx.Privmsg(msg.Args[0], "You lost a life punk...")
    118 						cs.lives--
    119 						cs.ictx.Privmsg(msg.Args[0], cs.String())
    120 						continue
    121 					}
    122 				} else {
    123 					if strings.Index(cs.tried, string(in[0])) != -1 {
    124 						cs.ictx.Privmsg(msg.Args[0], "I heard you the first time")
    125 						continue
    126 					}
    127 					hm := cs.checkLetter([]rune(in)[0])
    128 					cs.ictx.Privmsg(msg.Args[0], fmt.Sprintf("You got %d letters", hm))
    129 					if hm == 0 {
    130 						cs.lives--
    131 					}
    132 					cs.ictx.Privmsg(msg.Args[0], cs.String())
    133 					if cs.foundnow == cs.goal {
    134 						cs.ictx.Privmsg(msg.Args[0], "You won!")
    135 						*cs.statchan <- hangmanEnd
    136 					}
    137 				}
    138 			}
    139 		}
    140 	}
    141 }
    142 
    143 // Fourth hangman state. Final state mostly for cleanup code.
    144 func hangmanEnd(cs *hangmanState) {
    145 	*cs.statchan <- nil
    146 }
    147 
    148 // Main hangman dispatch, It sets a timeout and then runs as a new goroutine each
    149 // consecutive state it receives on the statechannel. Before ending it removes
    150 // the irc Intercept channel for this game.
    151 func cmdHangman(a *irc.Context, gc *chan irc.Message) {
    152 	timeout := time.After(4 * time.Minute)
    153 	a.AddIntercept(gc)
    154 	stchan := make(chan hgstateFn, 1)
    155 	hc := newHangmanState(a, gc, &stchan)
    156 	stchan <- hangmanInit
    157 	for {
    158 		select {
    159 		case <-timeout:
    160 			if hc.goal == "" {
    161 				a.Privmsg(hc.channame, "Time is up! No goal was given")
    162 			} else {
    163 				a.Privmsg(hc.channame, "Time is up! Goal was -> "+hc.goal)
    164 			}
    165 			a.DelIntercept(gc)
    166 			close(stchan)
    167 			return
    168 		case st := <-stchan:
    169 			if st != nil {
    170 				go st(hc)
    171 				continue
    172 			} else {
    173 				break
    174 			}
    175 		}
    176 		break
    177 	}
    178 	a.DelIntercept(gc)
    179 	close(stchan)
    180 }
    181 
    182 // Checks letter against the goal.
    183 // Returns the number of new letters discovered.
    184 // Returns -1 if repeated check is detected.
    185 func (g *hangmanState) checkLetter(c rune) int {
    186 	now := []rune(g.foundnow)
    187 	all := []rune(g.goal)
    188 	var buf bytes.Buffer
    189 	for _, v := range g.tried {
    190 		if c == v {
    191 			return -1
    192 		}
    193 	}
    194 	buf.WriteString(g.tried + " " + string(c))
    195 	g.tried = buf.String()
    196 	hm := 0
    197 	for i, v := range all {
    198 		if v == c {
    199 			if now[i] == v {
    200 				return -1
    201 			} /* again */
    202 			now[i] = v
    203 			hm++
    204 		}
    205 	}
    206 	g.foundnow = string(now)
    207 	return hm
    208 }