package game import ( "errors" "git.sunturtle.xyz/studio/shotgun/player" "git.sunturtle.xyz/studio/shotgun/serve" ) type Game struct { rng RNG players [2]Player shells []bool round uint group uint turn uint hp int8 damage int8 reveal bool prev *bool // Backing storage for shells. shellArray [8]bool } // New creates a new game started at round 1. func New(dealer, challenger player.ID) *Game { g := &Game{ rng: NewRNG(), players: [2]Player{ {id: dealer}, {id: challenger}, }, } g.StartRound() return g } // StartRound starts the next round of a game. func (g *Game) StartRound() { g.hp = int8(g.rng.Intn(3) + 2) g.players[0].StartRound(g.hp) g.players[1].StartRound(g.hp) g.round++ g.group = 0 g.StartGroup() } // StartGroup starts the next shell group of a round. func (g *Game) StartGroup() { items := g.rng.Intn(4) + 1 g.players[0].StartGroup(&g.rng, items) g.players[1].StartGroup(&g.rng, items) shells := g.rng.Intn(6) + 2 for i := 0; i < shells/2; i++ { g.shellArray[i] = true } for i := shells / 2; i < shells; i++ { g.shellArray[i] = false } g.shells = g.shellArray[:shells] ShuffleSlice(&g.rng, g.shells) g.group++ g.turn = 0 g.prev = nil g.NextTurn() } // NextTurn advances the turn. func (g *Game) NextTurn() { g.turn++ g.damage = 1 g.reveal = false cur := g.CurrentPlayer() skip := cur.cuffs == CuffedSkip cur.cuffs = cur.cuffs.NextState() if skip { g.NextTurn() } } // CurrentPlayer gets the index of the current player, either 0 or 1. func (g *Game) CurrentPlayer() *Player { return &g.players[g.turn%2] } // Opponent returns the player who is not the current player. func (g *Game) Opponent() *Player { return &g.players[g.turn%2^1] } // Apply uses an item by index for the current player. // This may cause the group to end. // Returns ErrWrongTurn if id does not correspond to the current player. func (g *Game) Apply(id player.ID, item int) error { cur := g.CurrentPlayer() if cur.id != id { return ErrWrongTurn } if item < 0 || item >= len(cur.items) { return errors.New("item index out of bounds") } if cur.items[item].Apply(g) { cur.items[item] = ItemNone } return nil } // PopShell removes a shell from the shotgun. // This may cause the shotgun to be empty, but does not start the next group // if so. func (g *Game) PopShell() bool { g.prev = &g.shells[0] g.shells = g.shells[1:] return *g.prev } // Peek returns the current turn's shell if it is revealed for the player with // the given ID, or nil otherwise. func (g *Game) Peek(id player.ID) *bool { if len(g.shells) == 0 || id != g.CurrentPlayer().id || !g.reveal { return nil } return &g.shells[0] } // Empty returns whether the shotgun is empty. func (g *Game) Empty() bool { return len(g.shells) == 0 } // Winner returns the player who won, or nil if the round is not over. func (g *Game) Winner() *Player { if g.players[0].hp <= 0 { return &g.players[1] } if g.players[1].hp <= 0 { return &g.players[0] } return nil } // Shoot fires the shotgun, at the opponent if self is false and at the current // player if self is true. Afterward, the round may be over, or the shotgun may // be empty. // Returns ErrWrongTurn if id does not correspond to the current player. func (g *Game) Shoot(id player.ID, self bool) error { cur := g.CurrentPlayer() if cur.id != id { return ErrWrongTurn } target := g.Opponent() if self { target = g.CurrentPlayer() } live := g.PopShell() if live { target.hp -= g.damage g.NextTurn() } else if !self { g.NextTurn() } return nil } // DTO returns the current game state as viewed by the given player. func (g *Game) DTO(id player.ID) serve.Game { return serve.Game{ Players: [2]serve.Player{ g.players[0].DTO(), g.players[1].DTO(), }, Round: g.round, Damage: g.damage, Shell: g.Peek(id), Previous: g.prev, } } // ShellCounts returns the number of live and blank shells. func (g *Game) ShellCounts() serve.ShellCounts { var counts serve.ShellCounts for _, s := range g.shells { if s { counts.Live++ } else { counts.Blank++ } } return counts } var ErrWrongTurn = errors.New("not your turn")