diff --git a/game/game_test.go b/game/game_test.go new file mode 100644 index 0000000..a67e95d --- /dev/null +++ b/game/game_test.go @@ -0,0 +1,463 @@ +package game + +import ( + "testing" + + "git.sunturtle.xyz/studio/shotgun/player" + "git.sunturtle.xyz/studio/shotgun/serve" +) + +func TestNewGame(t *testing.T) { + t.Parallel() + dealer := player.ID{1} + chall := player.ID{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.group != 1, "first group is %d, want 1", []any{g.group}}, + {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.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{1}, player.ID{2}) + for i := uint(1); i < 10000; 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.group != 1, "group is %d, want 1", []any{g.group}}, + {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.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.group = 2 + g.turn = 3 + g.damage = 2 + // Start the next round and check again. + g.StartRound() + } +} + +func TestGameStartGroup(t *testing.T) { + t.Parallel() + g := New(player.ID{1}, player.ID{2}) + for i := uint(1); i < 10000; i++ { + counts := g.ShellCounts() + 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]}}, + {counts.Live != counts.Blank && counts.Live+1 != counts.Blank, "imbalanced live/blank %d/%d", []any{counts.Live, counts.Blank}}, + {g.round != 1, "round is %d, want 1", []any{g.round}}, + {g.group != i, "group is %d, want %d", []any{g.group, i}}, + {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.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.StartGroup() + } +} + +func TestGameNextTurn(t *testing.T) { + t.Parallel() + g := New(player.ID{1}, player.ID{2}) + for i := uint(1); i < 10000; i++ { + checks := []struct { + failed bool + msg string + args []any + }{ + {g.round != 1, "round is %d, want 1", []any{g.round}}, + {g.group != 1, "group is %d, want 1", []any{g.group}}, + {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.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{1} + chall := player.ID{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{1}, player.ID{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{1} + chall := player.ID{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 := Game{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{1} + chall := player.ID{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.Winner(); 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{1} + chall := player.ID{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 TestGameShellCounts(t *testing.T) { + cases := []struct { + name string + shells []bool + want serve.ShellCounts + }{ + { + name: "empty", + shells: nil, + want: serve.ShellCounts{Live: 0, Blank: 0}, + }, + { + name: "one-live", + shells: []bool{true}, + want: serve.ShellCounts{Live: 1, Blank: 0}, + }, + { + name: "one-blank", + shells: []bool{false}, + want: serve.ShellCounts{Live: 0, Blank: 1}, + }, + { + name: "two", + shells: []bool{true, false}, + want: serve.ShellCounts{Live: 1, Blank: 1}, + }, + { + name: "three", + shells: []bool{true, false, true}, + want: serve.ShellCounts{Live: 2, Blank: 1}, + }, + { + name: "four", + shells: []bool{true, false, true, false}, + want: serve.ShellCounts{Live: 2, Blank: 2}, + }, + } + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + g := New(player.ID{1}, player.ID{2}) + g.shells = c.shells + if got := g.ShellCounts(); got != c.want { + t.Errorf("wrong shell counts: got %#v, wanted %#v", got, c.want) + } + }) + } +} + +func deref[T any, P ~*T](p P) (x T) { + if p != nil { + x = *p + } + return +}