package game import ( "testing" "time" "github.com/google/uuid" "git.sunturtle.xyz/studio/shotgun/player" "git.sunturtle.xyz/studio/shotgun/serve" ) func TestNewGame(t *testing.T) { t.Parallel() dealer := player.ID{UUID: uuid.UUID{1}} chall := player.ID{UUID: uuid.UUID{2}} for i := 0; i < 10000; i++ { g := New(dealer, chall) checks := []struct { failed bool msg string args []any }{ {g.players[0].id != dealer, "dealer isn't in first position", nil}, {g.players[0].hp != g.hp, "dealer hp is %d, want %d", []any{g.players[0].hp, g.hp}}, {g.players[1].id != chall, "challenger isn't in second position", nil}, {g.players[1].hp != g.hp, "challenger hp is %d, want %d", []any{g.players[1].hp, g.hp}}, {len(g.shells) < 2 || len(g.shells) > 8, "bad shells count %d, want 2-8", []any{len(g.shells)}}, {&g.shells[0] != &g.shellArray[0], "shells[0] is %p, want %p", []any{&g.shells[0], &g.shellArray[0]}}, {g.round != 1, "first round is %d, want 1", []any{g.round}}, {g.turn != 1, "first turn is %d, want 1", []any{g.turn}}, {g.hp < 2 || g.hp > 4, "hp is %d, want 2-4", []any{g.hp}}, {g.damage != 1, "damage is %d, want 1", []any{g.damage}}, {g.reveal, "revealed at start", nil}, {g.adrenaline, "adrenaline at start", nil}, {g.prev != nil, "already discharged", nil}, } for _, c := range checks { if c.failed { t.Errorf(c.msg, c.args...) } } } } func TestGameStartRound(t *testing.T) { t.Parallel() g := New(player.ID{UUID: uuid.UUID{1}}, player.ID{UUID: uuid.UUID{2}}) for i := int8(1); i < 100; i++ { checks := []struct { failed bool msg string args []any }{ {g.players[0].hp != g.hp, "dealer hp is %d, want %d", []any{g.players[0].hp, g.hp}}, {g.players[1].hp != g.hp, "challenger hp is %d, want %d", []any{g.players[1].hp, g.hp}}, {len(g.shells) < 2 || len(g.shells) > 8, "bad shells count %d, want 2-8", []any{len(g.shells)}}, {&g.shells[0] != &g.shellArray[0], "shells[0] is %p, want %p", []any{&g.shells[0], &g.shellArray[0]}}, {g.round != i, "round is %d, want %d", []any{g.round, i}}, {g.turn != 1, "turn is %d, want 1", []any{g.turn}}, {g.hp < 2 || g.hp > 4, "hp is %d, want 2-4", []any{g.hp}}, {g.damage != 1, "damage is %d, want 1", []any{g.damage}}, {g.reveal, "revealed at start", nil}, {g.adrenaline, "adrenaline at start", nil}, {g.prev != nil, "already discharged", nil}, } for _, c := range checks { if c.failed { t.Errorf(c.msg, c.args...) } } // H*ck with the game state as if a round is played. g.players[0].hp = 0 g.popShell() g.popShell() g.turn = 3 g.damage = 2 // Start the next round and check again. g.NextRound() } } func TestGameStartGroup(t *testing.T) { t.Parallel() g := New(player.ID{UUID: uuid.UUID{1}}, player.ID{UUID: uuid.UUID{2}}) for i := uint(1); i < 10000; i++ { checks := []struct { failed bool msg string args []any }{ {g.players[0].items[0] == itemNone, "dealer has no first item: %v", []any{g.players[0].items}}, {g.players[1].items[0] == itemNone, "challenger has no first item: %v", []any{g.players[1].items}}, {len(g.shells) < 2 || len(g.shells) > 8, "bad shells count %d, want 2-8", []any{len(g.shells)}}, {&g.shells[0] != &g.shellArray[0], "shells[0] is %p, want %p", []any{&g.shells[0], &g.shellArray[0]}}, {g.round != 1, "round is %d, want 1", []any{g.round}}, {g.turn != 1, "turn is %d, want 1", []any{g.turn}}, {g.damage != 1, "damage is %d, want 1", []any{g.damage}}, {g.reveal, "revealed at start", nil}, {g.adrenaline, "adrenaline at start", nil}, {g.prev != nil, "already discharged", nil}, } for _, c := range checks { if c.failed { t.Errorf(c.msg, c.args...) } } // H*ck with the game state. g.players[0].items[0] = itemNone g.players[1].items[0] = itemNone g.popShell() g.popShell() g.turn = 3 g.damage = 2 g.reveal = true // Now advance the group. g.NextGame() } } func TestGameNextTurn(t *testing.T) { t.Parallel() g := New(player.ID{UUID: uuid.UUID{1}}, player.ID{UUID: uuid.UUID{2}}) for i := int8(1); i < 100; i++ { checks := []struct { failed bool msg string args []any }{ {g.round != 1, "round is %d, want 1", []any{g.round}}, {g.turn != i, "turn is %d, want %d", []any{g.turn, i}}, {g.damage != 1, "damage is %d, want 1", []any{g.damage}}, {g.reveal, "revealed at start", nil}, {g.adrenaline, "adrenaline at start", nil}, {g.CurrentPlayer().cuffs != uncuffed, "cuffs is %d, want %d", []any{g.CurrentPlayer().cuffs, uncuffed}}, } for _, c := range checks { if c.failed { t.Errorf(c.msg, c.args...) } } g.damage = 2 g.reveal = true g.Opponent().cuffs = cuffed g.nextTurn() } } // TODO(zeph): test next turn when the opponent has just been cuffed func TestGamePlayers(t *testing.T) { t.Parallel() dealer := player.ID{UUID: uuid.UUID{1}} chall := player.ID{UUID: uuid.UUID{2}} g := New(dealer, chall) if g.CurrentPlayer().id != chall { t.Errorf("challenger isn't current player at start") } if g.Opponent().id != dealer { t.Errorf("dealer isn't opponent at start") } g.nextTurn() if g.CurrentPlayer().id != dealer { t.Errorf("dealer isn't current player after turn") } if g.Opponent().id != chall { t.Errorf("challenger isn't opponent after turn") } g.nextTurn() if g.CurrentPlayer().id != chall { t.Errorf("challenger isn't current player after two turns") } if g.Opponent().id != dealer { t.Errorf("dealer isn't opponent after two turns") } me, you := g.CurrentPlayer(), g.Opponent() for i := 3; i < 1000; i++ { g.nextTurn() if g.CurrentPlayer() != you { t.Errorf("wrong player after %d turns", i) } if g.Opponent() != me { t.Errorf("wrong opponent after %d turns", i) } me, you = you, me } } // TODO(zeph): test using items func TestGamePopShell(t *testing.T) { t.Parallel() g := New(player.ID{UUID: uuid.UUID{1}}, player.ID{UUID: uuid.UUID{2}}) if live := g.popShell(); live != g.shellArray[0] { t.Errorf("first pop %t, wanted %t", live, g.shellArray[0]) } if g.prev != &g.shellArray[0] { t.Errorf("first prev is %p, wanted %p", g.prev, &g.shellArray[0]) } if live := g.popShell(); live != g.shellArray[1] { t.Errorf("second pop %t, wanted %t", live, g.shellArray[1]) } if g.prev != &g.shellArray[1] { t.Errorf("second prev is %p, wanted %p", g.prev, &g.shellArray[1]) } for range g.shellArray { if g.Empty() { return } g.popShell() } if !g.Empty() { t.Errorf("popped too many shells") } } func TestGamePeek(t *testing.T) { dealer := player.ID{UUID: uuid.UUID{1}} chall := player.ID{UUID: uuid.UUID{2}} t.Run("empty", func(t *testing.T) { t.Parallel() g := New(dealer, chall) for !g.Empty() { g.popShell() } if g.Peek(dealer) != nil || g.Peek(chall) != nil { t.Errorf("peeked a shell when empty") } }) t.Run("unrevealed", func(t *testing.T) { t.Parallel() g := New(dealer, chall) if g.Peek(dealer) != nil || g.Peek(chall) != nil { t.Errorf("peeked a shell when not revealed") } }) t.Run("turn", func(t *testing.T) { t.Parallel() g := New(dealer, chall) g.reveal = true if g.Peek(dealer) != nil { t.Errorf("dealer peeked on challenger's turn") } if g.Peek(chall) != &g.shellArray[0] { t.Errorf("challenger couldn't peek on own turn") } g.nextTurn() g.reveal = true if g.Peek(dealer) != &g.shellArray[0] { t.Errorf("dealer couldn't peek on own turn") } if g.Peek(chall) != nil { t.Errorf("challenger peeked on dealer's turn") } }) } func TestGameEmpty(t *testing.T) { t.Parallel() cases := []struct { shells []bool want bool }{ {nil, true}, {[]bool{}, true}, {[]bool{true}, false}, {[]bool{false}, false}, } for _, c := range cases { // We don't care about anything but the shells list. g := Match{shells: c.shells} if got := g.Empty(); got != c.want { t.Errorf("empty reported %t, wanted %t", got, c.want) } } } func TestGameWinner(t *testing.T) { t.Parallel() dealer := player.ID{UUID: uuid.UUID{1}} chall := player.ID{UUID: uuid.UUID{2}} cases := []struct { name string p0, p1 int8 want player.ID }{ { name: "none", p0: 1, p1: 1, want: player.ID{}, }, { name: "dealer", p0: 1, p1: 0, want: dealer, }, { name: "challenger", p0: 0, p1: 1, want: chall, }, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() g := New(dealer, chall) g.players[0].hp = c.p0 g.players[1].hp = c.p1 if got := g.RoundWinner(); deref(got).id != c.want { t.Errorf("wrong winner: %#v doesn't have id %#v", got, c.want) } }) } } func TestGameShoot(t *testing.T) { dealer := player.ID{UUID: uuid.UUID{1}} chall := player.ID{UUID: uuid.UUID{2}} cases := []struct { name string live bool self bool shooter player.ID p0l, p1l int8 turn bool err error }{ { name: "challenger shoots dealer live", live: true, self: false, shooter: chall, p0l: 1, p1l: 0, turn: true, }, { name: "challenger shoots dealer blank", live: false, self: false, shooter: chall, p0l: 0, p1l: 0, turn: true, }, { name: "challenger shoots self live", live: true, self: true, shooter: chall, p0l: 0, p1l: 1, turn: true, }, { name: "challenger shoots self blank", live: false, self: true, shooter: chall, p0l: 0, p1l: 0, turn: false, }, { name: "dealer shoots", live: true, self: false, shooter: dealer, err: ErrWrongTurn, }, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() g := New(dealer, chall) g.shellArray[0] = c.live err := g.Shoot(c.shooter, c.self) if err != c.err { t.Errorf("wrong error: %v, wanted %v", err, c.err) } if err == nil && &g.shells[0] != &g.shellArray[1] { t.Errorf("no error but shells didn't move") } if err != nil && &g.shells[0] != &g.shellArray[0] { t.Errorf("error but shells moved") } if l := g.hp - g.players[0].hp; l != c.p0l { t.Errorf("wrong hp loss for dealer: got %d, wanted %d", l, c.p0l) } if l := g.hp - g.players[1].hp; l != c.p1l { t.Errorf("wrong hp loss for challenger: got %d, wanted %d", l, c.p1l) } if c.turn && g.CurrentPlayer().id != dealer { t.Errorf("shot should have advanced turn but didn't") } if !c.turn && g.CurrentPlayer().id != chall { t.Errorf("shot should not have advanced turn but did") } }) } } func TestConcede(t *testing.T) { dealer, chall := player.ID{UUID: uuid.UUID{1}}, player.ID{UUID: uuid.UUID{2}} cases := []struct { name string p player.ID winner player.ID }{ { name: "dealer", p: dealer, winner: chall, }, { name: "challenger", p: chall, winner: dealer, }, { name: "neither", p: player.ID{UUID: uuid.UUID{3}}, winner: player.ID{}, }, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() g := New(dealer, chall) g.Concede(c.p) if got := g.RoundWinner(); deref(got).id != c.winner { t.Errorf("wrong winner: %#v doesn't have id %#v", got, c.winner) } }) } } func TestGameDTO(t *testing.T) { t.Parallel() dealer, chall := player.ID{UUID: uuid.UUID{1}}, player.ID{UUID: uuid.UUID{2}} g := New(dealer, chall) { want := serve.Game{ Players: [2]serve.Player{ g.players[0].dto(), g.players[1].dto(), }, Action: "Start", Round: g.round, Dealer: false, Damage: g.damage, Adrenaline: false, Shell: nil, Previous: nil, Live: len(g.shells) / 2, Blank: (len(g.shells) + 1) / 2, } if got := g.DTO(dealer, time.UnixMilli(0)); want != got { t.Errorf("dealer sees the wrong thing:\nwant %+v\ngot %+v", want, got) } if got := g.DTO(chall, time.UnixMilli(0)); want != got { t.Errorf("challenger sees the wrong thing:\nwant %+v\ngot %+v", want, got) } if got := g.DTO(player.ID{}, time.UnixMilli(0)); want != got { t.Errorf("observer sees the wrong thing:\nwant %+v\ngot %+v", want, got) } } g.Shoot(chall, false) { want := serve.Game{ Players: [2]serve.Player{ g.players[0].dto(), g.players[1].dto(), }, Action: "Shoot", Round: g.round, Dealer: true, Damage: g.damage, Adrenaline: false, Shell: nil, Previous: &g.shellArray[0], Live: 0, Blank: 0, } if got := g.DTO(dealer, time.UnixMilli(0)); want != got { t.Errorf("dealer sees the wrong thing:\nwant %+v\ngot %+v", want, got) } if got := g.DTO(chall, time.UnixMilli(0)); want != got { t.Errorf("challenger sees the wrong thing:\nwant %+v\ngot %+v", want, got) } if got := g.DTO(player.ID{}, time.UnixMilli(0)); want != got { t.Errorf("observer sees the wrong thing:\nwant %+v\ngot %+v", want, got) } } // TODO(zeph): many more tests about actions } func deref[T any, P ~*T](p P) (x T) { if p != nil { x = *p } return }