shotgun/game/game_test.go

500 lines
12 KiB
Go

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.game != 1, "first group is %d, want 1", []any{g.game}},
{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.game != 1, "group is %d, want 1", []any{g.game}},
{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.game = 2
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{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.game != i, "group is %d, want %d", []any{g.game, 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.NextGame()
}
}
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.game != 1, "group is %d, want 1", []any{g.game}},
{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 := 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{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.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{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 TestConcede(t *testing.T) {
dealer, chall := player.ID{1}, player.ID{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{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 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
}