package main import ( "log" "math/rand" "strconv" "strings" "time" ) type Game struct { // The corresponding Lobby object, to allow communication // with clients lobby *Lobby // It's easier if we index the spectators, so we use a map // Only synced with the client during netburst spectators map[*Client]bool // We need a strict order for players, so we use a slice // The player list is order-sensitive, so we re-sync it // with the client every time it's updated. players []*Client currentPlayer int // Various game-related variables started bool defusing bool attack bool favouring *Client // Who is asking for a favour? favoured *Client // Who is being asked for a favour? favourType int // !! not reset !! // 1 - favour, 2 - random, 3 - steal deck *Deck hands map[*Client]*Hand // We store one action's worth of 'history' in case of a nope history *GameState } func newGame(lobby *Lobby) *Game { return &Game{ lobby: lobby, spectators: make(map[*Client]bool), hands: make(map[*Client]*Hand), currentPlayer: -1, } } func (g *Game) addPlayer(client *Client) { // Adds a player to the spectators // /!\ This function expects the caller to have already obtained a lock on // g.lobby.clients - not doing this leads to a race condition g.spectators[client] = true g.lobby.sendBcastRaw("joins " + client.name) g.netburst(client) } func (g *Game) removePlayer(client *Client) { _, ok := g.spectators[client] if !ok { // First downgrade the player to spectator g.downgradePlayer(client) } delete(g.spectators, client) g.lobby.sendBcast("parts " + client.name) } func (g *Game) playerNumber(client *Client) (num int) { // Finds the number of a player in the players slice // Mostly useful for removing players num = -1 for i, _ := range g.players { if g.players[i] == client { num = i break } } return } func (g *Game) playerByName(name string) (player *Client) { for _, thisPlayer := range g.players { if thisPlayer.name == name { player = thisPlayer break } } return } func (g *Game) upgradePlayer(client *Client) { // Move a player from the spectators into the players delete(g.spectators, client) g.players = append(g.players, client) g.lobby.sendBcast("upgrades " + client.name) g.lobby.sendBcast("players" + g.playerList()) // Display a message to tell the client they are playing client.sendMsg("message playing") } func (g *Game) downgradePlayer(client *Client) { currentlyPlaying := false if g.playerNumber(client) == g.currentPlayer { currentlyPlaying = true } clientToRemove := g.playerNumber(client) g.players = append(g.players[:clientToRemove], g.players[clientToRemove+1:]...) g.spectators[client] = true delete(g.hands, client) g.lobby.sendBcast("downgrades " + client.name) g.lobby.sendBcast("players" + g.playerList()) if !g.started { // Display a message to tell the client they are spectating client.sendMsg("message spectating") return } // Gracefully remove the player from the game in progress client.sendMsg("message spectating_exploded") // Erase their hand client.sendMsg("hand") if len(g.players) == 1 { go g.wins(g.players[0]) return } if g.favouring == client { g.favoured.sendMsg("q_cancel") g.favouring = nil g.favoured = nil } if g.favoured == client && g.favourType == 1 { // The favour is cancelled g.lobby.sendBcast("bcast favour_cancel") g.favouring.sendMsg("unlock") g.favouring = nil g.favoured = nil } // for favourType 2, it is instant - this can't happen // for favourType 3, we are waiting on a response from the player ASKING // (so we can deal with it in answersQuestion) // If they are currently playing, advance to the next player if currentlyPlaying && len(g.players) > 0 { g.nextTurn() } } func (g *Game) wins(winner *Client) { g.lobby.sendBcast("wins " + winner.name) // This function runs a separate goroutine, so it's safe to sleep time.Sleep(5 * time.Second) // Destroy the game and create a new one g.lobby.sendBcast("hand") g.lobby.sendBcast("draw_pile no") g.lobby.sendBcast("no_discard") g.lobby.sendBcast("bcast new_game") g.lobby.clientsMu.Lock() defer g.lobby.clientsMu.Unlock() g.lobby.currentGame = newGame(g.lobby) for client := range g.lobby.clients { g.lobby.currentGame.addPlayer(client) // We add a very short delay to allow each joining client to be processed separately time.Sleep(50 * time.Millisecond) } // The GC should now be able to collect this old game object, I think } func (g *Game) netburst(client *Client) { // Communicates the current game state to a newly joining client client.sendMsg("spectators" + g.spectatorList()) client.sendMsg("players" + g.playerList()) // Display a message to tell the client they are spectating if !g.started { client.sendMsg("message spectating") return } // allow the client to spectate a game-in-progress client.sendMsg("message spectating_started") if g.deck.cardsLeft() > 0 { client.sendMsg("draw_pile yes") } } func (g *Game) spectatorList() (list string) { for spec := range g.spectators { list = list + " " + spec.name } return } func (g *Game) playerList() (list string) { for _, player := range g.players { list = list + " " + player.name } return } func (g *Game) readFromClient(c *Client, msg string) { fields := strings.Fields(msg) switch fields[0] { case "join": // Joining the game (from spectators) if g.started { // You can't join an active game break } _, ok := g.spectators[c] if !ok { // Player is not a spectator! break } g.upgradePlayer(c) case "leave": // Leaving the game (back to spectators) _, ok := g.spectators[c] if ok { // Player is a spectator! break } g.downgradePlayer(c) case "start": if g.started { break } if len(g.players) < 2 { c.sendMsg("bcast min_players") break } if len(g.players) > 6 { g.lobby.sendBcast("bcast max_players") break } if len(g.players) == 6 { // Warning message g.lobby.sendBcast("bcast high_players") } g.start() case "draw": _, ok := g.spectators[c] if ok { break } if g.currentPlayer >= len(g.players) { log.Println("Player out of range") break } if g.deck.cardsLeft() < 1 { c.sendMsg("err illegal_move") break } if g.players[g.currentPlayer].name != c.name { c.sendMsg("err illegal_move") break } if g.defusing { log.Println("Defusing") break } g.drawCard(c) case "play": _, ok := g.spectators[c] if ok { break } if len(fields) != 2 { break } if g.currentPlayer >= len(g.players) { log.Println("Player out of range") break } if g.favouring != nil { // There is a case where it would be legal to play // NOPE here - TODO break } card, err := strconv.Atoi(fields[1]) if err != nil { c.sendMsg("err illegal_move") break } if card >= g.hands[c].getLength() { c.sendMsg("err illegal_move") break } cardText := g.hands[c].getCard(card) if g.defusing && cardText != "defuse" { log.Println("Defusing") break } if cardText != "nope" && g.players[g.currentPlayer].name != c.name { c.sendMsg("err illegal_move") break } g.favouring = nil g.favoured = nil g.hands[c].removeCard(card) c.sendMsg("hand" + g.hands[c].cardList()) g.playsCard(c, cardText) case "play_multiple": _, ok := g.spectators[c] if ok { break } if len(fields) != 3 { break } if g.currentPlayer >= len(g.players) { break } if g.favouring != nil { c.sendMsg("err illegal_move") break } if g.defusing { break } num, err := strconv.Atoi(fields[1]) if err != nil || num > 3 || num < 2 { c.sendMsg("err illegal_move") break } if !g.hands[c].containsMultiple(fields[2], num) { c.sendMsg("err illegal_move") break } g.favouring = nil g.favoured = nil for i := 0; i < num; i++ { g.hands[c].removeByName(fields[2]) } c.sendMsg("hand" + g.hands[c].cardList()) g.playsCombo(c, fields[2], num) case "a": if len(fields) != 3 { if len(fields) == 2 { c.sendMsg("q "+fields[1]) } break } g.answersQuestion(c, fields[1], fields[2]) case "sort": _, ok := g.spectators[c] if ok { break } if !g.started { break } g.hands[c].sort() c.sendMsg("hand" + g.hands[c].cardList()) default: log.Println("Uncaught message from", c.name+":", msg) } // End switch } func (g *Game) drawCard(c *Client) { card := g.deck.draw() g.history = nil g.favouring = nil g.favoured = nil g.lobby.sendBcast("cards_left "+strconv.Itoa(g.deck.cardsLeft())) if card == "exploding" { g.lobby.sendBcast("exploded " + c.name) if !g.hands[c].contains("defuse") { g.downgradePlayer(c) return } g.defusing = true c.sendMsg("defusing") g.nextTurn() return } g.hands[c].addCard(card) c.sendMsg("hand" + g.hands[c].cardList()) // Tell the player what card they drew c.sendMsg("drew " + card) // Tell everyone else that a mystery card was drawn g.lobby.sendComplexBcast("drew_other "+c.name, map[*Client]bool{c: true}) g.incrementTurn() g.nextTurn() } func (g *Game) playsCard(player *Client, card string) { g.lobby.sendBcast("played " + player.name + " " + card) // Every case here should do something with g.history // - either make a backup, or clear it so that we can't // NOPE too far back into history switch card { case "defuse": if !g.defusing { return } player.sendMsg("q defuse_pos") case "favour": g.favouring = player g.favourType = 1 player.sendMsg("q favour_who") case "shuffle": g.history = makeGameState(g) g.deck.shuffle() case "nope": if g.history == nil { g.lobby.sendBcast("bcast no_nope") return } // Back up the game state before restoring the old one // - that way, you can NOPE a NOPE history := makeGameState(g) g.history.restore(g) g.history = history g.nextTurn() case "skip": g.history = makeGameState(g) g.incrementTurn() g.nextTurn() case "attack": g.history = makeGameState(g) if g.attack { // player is on the first turn of an attack g.attack = false } else { g.currentPlayer++ g.attack = true } g.nextTurn() case "see3": cards := g.deck.peek(3) player.sendMsg("seen " + strings.Join(cards, " ")) g.history = nil default: log.Println("unhandled card: ", card) } } func (g *Game) playsCombo(player *Client, card string, num int) { g.lobby.sendBcast("played_multiple " + player.name + " " + strconv.Itoa(num) + " " + card) g.history = nil // TODO if num == 2 { // 2 of a kind - random card g.favouring = player g.favourType = 2 player.sendMsg("q random_who") } else if num == 3 { // 3 of a kind - 'stealing' a card g.favouring = player g.favourType = 3 player.sendMsg("q steal_who") } else { log.Fatal("what the chuff??") } } func (g *Game) answersQuestion(player *Client, question string, answer string) { switch question { case "defuse_pos": if !g.defusing { break } if g.players[g.currentPlayer] != player { break } pos, err := strconv.Atoi(answer) if err != nil { player.sendMsg("q " + question) break } if pos > g.deck.cardsLeft() { player.sendMsg("q " + question) break } g.deck.insertAtPos(pos, "exploding") g.defusing = false g.lobby.sendBcast("cards_left "+strconv.Itoa(g.deck.cardsLeft())) g.incrementTurn() g.nextTurn() case "favour_who": if g.favouring != player { break } target := g.playerByName(answer) if target == nil || target == player { player.sendMsg("q " + question) break } g.favoured = target g.lobby.sendComplexBcast("favoured "+player.name+" "+target.name, map[*Client]bool{target: true}) target.sendMsg("q favour_what " + player.name) player.sendMsg("lock") // block further play until the transaction completes case "random_who": if g.favouring != player { break } target := g.playerByName(answer) if target == nil || target == player { player.sendMsg("q " + question) break } // removes the card from the target's hand // what if they have no cards? if g.hands[target].getLength() == 0 { g.lobby.sendBcast("random_n "+player.name+" "+target.name) g.favouring = nil g.favoured = nil break } card := g.hands[target].takeRandom() g.lobby.sendComplexBcast("randomed "+player.name+" "+target.name, map[*Client]bool{target: true, player: true}) target.sendMsg("random_gave "+player.name+" "+card) player.sendMsg("random_recv "+target.name+" "+card) g.hands[player].addCard(card) player.sendMsg("hand" + g.hands[player].cardList()) target.sendMsg("hand" + g.hands[target].cardList()) g.favouring = nil g.favoured = nil case "steal_who": if g.favouring != player { break } target := g.playerByName(answer) if target == nil || target == player { player.sendMsg("q " + question) break } g.favoured = target player.sendMsg("q steal_what") case "favour_what": if g.favoured != player { player.sendMsg("err illegal_move") break } card, err := strconv.Atoi(answer) if err != nil { player.sendMsg("err illegal_move") break } if card >= g.hands[player].getLength() { player.sendMsg("err illegal_move") break } cardText := g.hands[player].getCard(card) // TODO: prevent favouring a nope // this is harder than it seems and will require some changes in the game's logic... g.hands[player].removeCard(card) player.sendMsg("hand" + g.hands[player].cardList()) g.hands[g.favouring].addCard(cardText) g.favouring.sendMsg("hand" + g.hands[g.favouring].cardList()) // The favour transaction is complete g.favouring.sendMsg("unlock") g.favouring.sendMsg("favour_recv " + g.favoured.name + " " + cardText) g.favoured.sendMsg("favour_gave " + g.favouring.name + " " + cardText) g.lobby.sendComplexBcast("favour_complete "+g.favouring.name+" "+g.favoured.name, map[*Client]bool{g.favoured: true, g.favouring: true}) g.favouring = nil g.favoured = nil case "steal_what": if g.favouring != player || g.favoured == nil { break } if g.playerNumber(g.favoured) == -1 { // The player being asked has left :( player.sendMsg("q steal_who") break } if !g.hands[g.favoured].contains(answer) { g.lobby.sendBcast("steal_n "+g.favouring.name+" "+g.favoured.name+" "+answer) } else { g.hands[g.favoured].removeByName(answer) g.favoured.sendMsg("hand" + g.hands[g.favoured].cardList()) g.hands[player].addCard(answer) player.sendMsg("hand" + g.hands[g.favouring].cardList()) g.lobby.sendBcast("steal_y "+g.favouring.name+" "+g.favoured.name+" "+answer) } g.favouring = nil g.favoured = nil default: log.Println("unexpected Q/A: ", question, answer) } } func (g *Game) incrementTurn() { // Changes the turn counter to the next player // (or not, if an attack has been played) if g.attack { g.attack = false return } g.currentPlayer++ return } func (g *Game) nextTurn() { // Begins the next turn // NB. this doesn't change currentPlayer if g.currentPlayer >= len(g.players) { g.currentPlayer = 0 } g.lobby.sendBcast("now_playing " + g.players[g.currentPlayer].name) if g.deck.cardsLeft() == 0 { g.lobby.sendBcast("draw_pile no") } else { g.lobby.sendBcast("draw_pile yes") } } func (g *Game) start() { // Starts the game g.started = true g.lobby.sendBcast("clear_message") g.lobby.sendBcast("bcast starting") // Shuffle the player list and re-send it rand.Seed(time.Now().UnixNano()) // Hard enough to predict; should be fine rand.Shuffle(len(g.players), func(i, j int) { g.players[i], g.players[j] = g.players[j], g.players[i] }) g.currentPlayer = 0 g.lobby.sendBcast("players" + g.playerList()) g.lobby.sendBcast("now_playing " + g.players[g.currentPlayer].name) // Generate the deck g.deck = newDeck() // Give each player a hand for _, player := range g.players { // Pass the number of players so the deal method knows how many cards to deal g.hands[player] = g.deck.dealHand(len(g.players)) } // Shuffle in extra cards g.deck.addExtraCards(len(g.players)) // Sync the cards to the client for _, player := range g.players { player.sendMsg("hand" + g.hands[player].cardList()) } g.lobby.sendBcast("draw_pile yes") g.lobby.sendBcast("cards_left "+strconv.Itoa(g.deck.cardsLeft())) }