shotgun/game/game.go

194 lines
4.1 KiB
Go

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")