add bingo completion tests: tap-only, detection, scoring, redraw, announcements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 11:44:20 +01:00
parent 0784b4b077
commit af0499d354

View File

@@ -0,0 +1,270 @@
import { describe, it, expect, beforeEach } from "bun:test"
import { GameManager } from "../game-manager"
describe("Bingo", () => {
let gm: GameManager
beforeEach(() => {
gm = new GameManager()
})
describe("generateBingoCards", () => {
it("should create a 16-square card for each player", () => {
gm.generateBingoCards(["p1", "p2"])
const card1 = gm.getBingoCard("p1")
const card2 = gm.getBingoCard("p2")
expect(card1).not.toBeNull()
expect(card1!.squares).toHaveLength(16)
expect(card1!.hasBingo).toBe(false)
expect(card2).not.toBeNull()
expect(card2!.squares).toHaveLength(16)
})
it("should return null for unknown player", () => {
expect(gm.getBingoCard("unknown")).toBeNull()
})
})
describe("tapBingoSquare", () => {
beforeEach(() => {
gm.generateBingoCards(["p1"])
})
it("should mark a square as tapped", () => {
const card = gm.getBingoCard("p1")!
const tropeId = card.squares[0]!.tropeId
const result = gm.tapBingoSquare("p1", tropeId)
expect(result).toHaveProperty("success", true)
expect(card.squares[0]!.tapped).toBe(true)
})
it("should be tap-only (not toggle)", () => {
const card = gm.getBingoCard("p1")!
const tropeId = card.squares[0]!.tropeId
gm.tapBingoSquare("p1", tropeId)
expect(card.squares[0]!.tapped).toBe(true)
// Tap again — should stay tapped
const result = gm.tapBingoSquare("p1", tropeId)
expect(result).toHaveProperty("success", true)
expect(card.squares[0]!.tapped).toBe(true)
})
it("should error for unknown player", () => {
const result = gm.tapBingoSquare("unknown", "trope1")
expect(result).toHaveProperty("error")
})
it("should error for trope not on card", () => {
const result = gm.tapBingoSquare("p1", "nonexistent-trope")
expect(result).toHaveProperty("error")
})
})
describe("bingo detection", () => {
beforeEach(() => {
gm.generateBingoCards(["p1"])
})
it("should detect a completed row", () => {
const card = gm.getBingoCard("p1")!
// Tap first row (indices 0-3)
for (let i = 0; i < 4; i++) {
const result = gm.tapBingoSquare("p1", card.squares[i]!.tropeId)
if (i < 3) {
expect((result as any).hasBingo).toBe(false)
} else {
expect((result as any).hasBingo).toBe(true)
expect((result as any).isNewBingo).toBe(true)
}
}
})
it("should detect a completed column", () => {
const card = gm.getBingoCard("p1")!
// Tap first column (indices 0, 4, 8, 12)
for (const i of [0, 4, 8, 12]) {
gm.tapBingoSquare("p1", card.squares[i]!.tropeId)
}
expect(card.hasBingo).toBe(true)
})
it("should detect a completed diagonal", () => {
const card = gm.getBingoCard("p1")!
// Tap main diagonal (indices 0, 5, 10, 15)
for (const i of [0, 5, 10, 15]) {
gm.tapBingoSquare("p1", card.squares[i]!.tropeId)
}
expect(card.hasBingo).toBe(true)
})
it("should detect anti-diagonal", () => {
const card = gm.getBingoCard("p1")!
// Tap anti-diagonal (indices 3, 6, 9, 12)
for (const i of [3, 6, 9, 12]) {
gm.tapBingoSquare("p1", card.squares[i]!.tropeId)
}
expect(card.hasBingo).toBe(true)
})
})
describe("bingo completion flow", () => {
beforeEach(() => {
gm.generateBingoCards(["p1"])
})
it("should NOT move card to completedBingoCards on bingo detection (only on redraw)", () => {
const card = gm.getBingoCard("p1")!
// Complete first row — sets hasBingo but does NOT move card
for (let i = 0; i < 4; i++) {
gm.tapBingoSquare("p1", card.squares[i]!.tropeId)
}
expect(card.hasBingo).toBe(true)
expect(gm.getCompletedBingoCards()).toHaveLength(0)
})
it("should move card to completedBingoCards on requestNewBingoCard", () => {
const card = gm.getBingoCard("p1")!
// Complete first row
for (let i = 0; i < 4; i++) {
gm.tapBingoSquare("p1", card.squares[i]!.tropeId)
}
// Redraw moves the completed card
gm.requestNewBingoCard("p1", "Player 1")
const completed = gm.getCompletedBingoCards()
expect(completed).toHaveLength(1)
expect(completed[0]!.playerId).toBe("p1")
expect(completed[0]!.displayName).toBe("Player 1")
expect(completed[0]!.card.hasBingo).toBe(true)
expect(completed[0]!.completedAt).toBeTruthy()
})
})
describe("requestNewBingoCard", () => {
beforeEach(() => {
gm.generateBingoCards(["p1"])
})
it("should error if card has no bingo", () => {
const result = gm.requestNewBingoCard("p1", "Player 1")
expect(result).toHaveProperty("error")
})
it("should generate a fresh card after bingo", () => {
const card = gm.getBingoCard("p1")!
const originalTropes = card.squares.map((s) => s.tropeId)
// Complete first row
for (let i = 0; i < 4; i++) {
gm.tapBingoSquare("p1", card.squares[i]!.tropeId)
}
const result = gm.requestNewBingoCard("p1", "Player 1")
expect(result).toHaveProperty("success", true)
const newCard = gm.getBingoCard("p1")!
expect(newCard.hasBingo).toBe(false)
expect(newCard.squares.every((s) => !s.tapped)).toBe(true)
expect(newCard.squares).toHaveLength(16)
})
})
describe("getBingoScore — accumulation across cards", () => {
beforeEach(() => {
gm.generateBingoCards(["p1"])
})
it("should score tapped squares on active card", () => {
const card = gm.getBingoCard("p1")!
gm.tapBingoSquare("p1", card.squares[0]!.tropeId)
gm.tapBingoSquare("p1", card.squares[1]!.tropeId)
// 2 tapped squares * 2 points = 4
expect(gm.getBingoScore("p1")).toBe(4)
})
it("should include bingo bonus on completed card", () => {
const card = gm.getBingoCard("p1")!
// Complete first row (4 squares)
for (let i = 0; i < 4; i++) {
gm.tapBingoSquare("p1", card.squares[i]!.tropeId)
}
// 4 tapped * 2 = 8, plus 10 bonus = 18
expect(gm.getBingoScore("p1")).toBe(18)
})
it("should accumulate scores across completed + new card", () => {
const card = gm.getBingoCard("p1")!
// Complete first row (4 squares) — triggers completion
for (let i = 0; i < 4; i++) {
gm.tapBingoSquare("p1", card.squares[i]!.tropeId)
}
// Request new card
gm.requestNewBingoCard("p1", "Player 1")
const newCard = gm.getBingoCard("p1")!
// Tap 2 squares on new card
gm.tapBingoSquare("p1", newCard.squares[0]!.tropeId)
gm.tapBingoSquare("p1", newCard.squares[1]!.tropeId)
// Old card: 4 tapped * 2 = 8 + 10 bonus = 18
// New card: 2 tapped * 2 = 4
// Total: 22
expect(gm.getBingoScore("p1")).toBe(22)
})
it("should return 0 for unknown player", () => {
expect(gm.getBingoScore("unknown")).toBe(0)
})
})
describe("addBingoAnnouncement — multiple per player", () => {
beforeEach(() => {
gm.generateBingoCards(["p1"])
})
it("should announce first bingo", () => {
const card = gm.getBingoCard("p1")!
for (let i = 0; i < 4; i++) {
gm.tapBingoSquare("p1", card.squares[i]!.tropeId)
}
const isNew = gm.addBingoAnnouncement("p1", "Player 1")
expect(isNew).toBe(true)
})
it("should not re-announce same bingo", () => {
const card = gm.getBingoCard("p1")!
for (let i = 0; i < 4; i++) {
gm.tapBingoSquare("p1", card.squares[i]!.tropeId)
}
gm.addBingoAnnouncement("p1", "Player 1")
const isNew = gm.addBingoAnnouncement("p1", "Player 1")
expect(isNew).toBe(false)
})
it("should announce second bingo after redraw", () => {
const card = gm.getBingoCard("p1")!
for (let i = 0; i < 4; i++) {
gm.tapBingoSquare("p1", card.squares[i]!.tropeId)
}
gm.addBingoAnnouncement("p1", "Player 1")
// Redraw
gm.requestNewBingoCard("p1", "Player 1")
const newCard = gm.getBingoCard("p1")!
// Complete first row of new card
for (let i = 0; i < 4; i++) {
gm.tapBingoSquare("p1", newCard.squares[i]!.tropeId)
}
const isNew = gm.addBingoAnnouncement("p1", "Player 1")
expect(isNew).toBe(true)
expect(gm.getBingoAnnouncements()).toHaveLength(2)
})
})
describe("game state includes completedBingoCards", () => {
it("should include completedBingoCards in player game state", () => {
gm.generateBingoCards(["p1"])
const state = gm.getGameStateForPlayer("p1", ["p1"], { p1: "Player 1" })
expect(state.completedBingoCards).toEqual([])
})
it("should include completedBingoCards in display game state", () => {
gm.generateBingoCards(["p1"])
const state = gm.getGameStateForDisplay(["p1"], { p1: "Player 1" })
expect(state.completedBingoCards).toEqual([])
})
})
})