shotgun/game/game_test.go

504 lines
13 KiB
Go
Raw Normal View History

2024-01-21 11:21:50 -06:00
package game
import (
"testing"
2024-02-03 21:11:09 -06:00
"time"
2024-01-21 11:21:50 -06:00
"github.com/google/uuid"
2024-01-21 11:21:50 -06:00
"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}}
2024-01-21 11:21:50 -06:00
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.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}})
2024-01-29 13:16:34 -06:00
for i := int8(1); i < 100; i++ {
2024-01-21 11:21:50 -06:00
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.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
2024-01-28 12:42:42 -06:00
g.popShell()
g.popShell()
2024-01-21 11:21:50 -06:00
g.turn = 3
g.damage = 2
// Start the next round and check again.
2024-01-28 12:42:42 -06:00
g.NextRound()
2024-01-21 11:21:50 -06:00
}
}
func TestGameStartGroup(t *testing.T) {
t.Parallel()
g := New(player.ID{UUID: uuid.UUID{1}}, player.ID{UUID: uuid.UUID{2}})
2024-01-21 11:21:50 -06:00
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}},
2024-01-21 11:21:50 -06:00
{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.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
2024-01-28 12:42:42 -06:00
g.popShell()
g.popShell()
2024-01-21 11:21:50 -06:00
g.turn = 3
g.damage = 2
g.reveal = true
// Now advance the group.
2024-01-28 12:42:42 -06:00
g.NextGame()
2024-01-21 11:21:50 -06:00
}
}
func TestGameNextTurn(t *testing.T) {
t.Parallel()
g := New(player.ID{UUID: uuid.UUID{1}}, player.ID{UUID: uuid.UUID{2}})
2024-01-29 13:16:34 -06:00
for i := int8(1); i < 100; i++ {
2024-01-21 11:21:50 -06:00
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.CurrentPlayer().cuffs != uncuffed, "cuffs is %d, want %d", []any{g.CurrentPlayer().cuffs, uncuffed}},
2024-01-21 11:21:50 -06:00
}
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()
2024-01-21 11:21:50 -06:00
}
}
// 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}}
2024-01-21 11:21:50 -06:00
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()
2024-01-21 11:21:50 -06:00
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()
2024-01-21 11:21:50 -06:00
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()
2024-01-21 11:21:50 -06:00
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}})
2024-01-28 12:42:42 -06:00
if live := g.popShell(); live != g.shellArray[0] {
2024-01-21 11:21:50 -06:00
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])
}
2024-01-28 12:42:42 -06:00
if live := g.popShell(); live != g.shellArray[1] {
2024-01-21 11:21:50 -06:00
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
}
2024-01-28 12:42:42 -06:00
g.popShell()
2024-01-21 11:21:50 -06:00
}
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}}
2024-01-21 11:21:50 -06:00
t.Run("empty", func(t *testing.T) {
t.Parallel()
g := New(dealer, chall)
for !g.Empty() {
2024-01-28 12:42:42 -06:00
g.popShell()
2024-01-21 11:21:50 -06:00
}
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()
2024-01-21 11:21:50 -06:00
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.
2024-01-28 12:42:42 -06:00
g := Match{shells: c.shells}
2024-01-21 11:21:50 -06:00
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}}
2024-01-21 11:21:50 -06:00
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
2024-01-28 21:47:14 -06:00
if got := g.RoundWinner(); deref(got).id != c.want {
2024-01-21 11:21:50 -06:00
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}}
2024-01-21 11:21:50 -06:00
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")
}
})
}
}
2024-01-23 20:45:00 -06:00
func TestConcede(t *testing.T) {
dealer, chall := player.ID{UUID: uuid.UUID{1}}, player.ID{UUID: uuid.UUID{2}}
2024-01-23 20:45:00 -06:00
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}},
2024-01-23 20:45:00 -06:00
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)
2024-01-28 21:47:14 -06:00
if got := g.RoundWinner(); deref(got).id != c.winner {
2024-01-23 20:45:00 -06:00
t.Errorf("wrong winner: %#v doesn't have id %#v", got, c.winner)
}
})
}
}
2024-01-29 21:20:17 -06:00
func TestGameDTO(t *testing.T) {
t.Parallel()
dealer, chall := player.ID{UUID: uuid.UUID{1}}, player.ID{UUID: uuid.UUID{2}}
2024-01-29 21:20:17 -06:00
g := New(dealer, chall)
{
want := serve.Game{
Players: [2]serve.Player{
g.players[0].dto(),
g.players[1].dto(),
2024-01-29 21:20:17 -06:00
},
Action: "Start",
2024-01-29 21:20:17 -06:00
Round: g.round,
Dealer: false,
2024-01-29 21:20:17 -06:00
Damage: g.damage,
Shell: nil,
Previous: nil,
Live: len(g.shells) / 2,
Blank: (len(g.shells) + 1) / 2,
}
2024-02-03 21:11:09 -06:00
if got := g.DTO(dealer, time.UnixMilli(0)); want != got {
2024-01-29 21:20:17 -06:00
t.Errorf("dealer sees the wrong thing:\nwant %+v\ngot %+v", want, got)
}
2024-02-03 21:11:09 -06:00
if got := g.DTO(chall, time.UnixMilli(0)); want != got {
2024-01-29 21:20:17 -06:00
t.Errorf("challenger sees the wrong thing:\nwant %+v\ngot %+v", want, got)
}
2024-02-03 21:11:09 -06:00
if got := g.DTO(player.ID{}, time.UnixMilli(0)); want != got {
2024-01-29 21:20:17 -06:00
t.Errorf("observer sees the wrong thing:\nwant %+v\ngot %+v", want, got)
}
2024-01-21 11:21:50 -06:00
}
2024-01-29 21:20:17 -06:00
g.Shoot(chall, false)
{
want := serve.Game{
Players: [2]serve.Player{
g.players[0].dto(),
g.players[1].dto(),
2024-01-29 21:20:17 -06:00
},
Action: "Shoot",
2024-01-29 21:20:17 -06:00
Round: g.round,
Dealer: true,
2024-01-29 21:20:17 -06:00
Damage: g.damage,
Shell: nil,
Previous: &g.shellArray[0],
Live: 0,
Blank: 0,
}
2024-02-03 21:11:09 -06:00
if got := g.DTO(dealer, time.UnixMilli(0)); want != got {
2024-01-29 21:20:17 -06:00
t.Errorf("dealer sees the wrong thing:\nwant %+v\ngot %+v", want, got)
}
2024-02-03 21:11:09 -06:00
if got := g.DTO(chall, time.UnixMilli(0)); want != got {
2024-01-29 21:20:17 -06:00
t.Errorf("challenger sees the wrong thing:\nwant %+v\ngot %+v", want, got)
}
2024-02-03 21:11:09 -06:00
if got := g.DTO(player.ID{}, time.UnixMilli(0)); want != got {
2024-01-29 21:20:17 -06:00
t.Errorf("observer sees the wrong thing:\nwant %+v\ngot %+v", want, got)
}
2024-01-21 11:21:50 -06:00
}
// TODO(zeph): many more tests about actions
2024-01-21 11:21:50 -06:00
}
func deref[T any, P ~*T](p P) (x T) {
if p != nil {
x = *p
}
return
}