boardgame-core/tests/samples/regicide.test.ts

374 lines
10 KiB
TypeScript
Raw Normal View History

import { describe, it, expect } from "vitest";
import { createGameContext } from "@/core/game";
import { registry } from "@/samples/regicide/commands";
import { createInitialState } from "@/samples/regicide/state";
2026-04-10 13:43:12 +08:00
import {
buildEnemyDeck,
buildTavernDeck,
createAllCards,
createCard,
createEnemy,
getCardValue,
isEnemyDefeated,
} from "@/samples/regicide/utils";
import { Mulberry32RNG } from "@/utils/rng";
import {
CARD_VALUES,
ENEMY_COUNT,
FACE_CARDS,
INITIAL_HAND_SIZE,
} from "@/samples/regicide/constants";
import { PlayerType } from "@/samples/regicide/types";
describe("Regicide - Utils", () => {
describe("getCardValue", () => {
it("should return correct value for number cards", () => {
expect(getCardValue("A")).toBe(1);
expect(getCardValue("5")).toBe(5);
expect(getCardValue("10")).toBe(10);
2026-04-10 13:43:12 +08:00
});
it("should return correct value for face cards", () => {
expect(getCardValue("J")).toBe(10);
expect(getCardValue("Q")).toBe(15);
expect(getCardValue("K")).toBe(20);
});
});
describe("createCard", () => {
it("should create a card with correct properties", () => {
const card = createCard("spades_A", "spades", "A");
expect(card.id).toBe("spades_A");
expect(card.suit).toBe("spades");
expect(card.rank).toBe("A");
expect(card.value).toBe(1);
});
});
describe("createEnemy", () => {
it("should create an enemy with correct HP", () => {
const enemy = createEnemy("enemy_0", "J", "spades");
expect(enemy.rank).toBe("J");
expect(enemy.value).toBe(10);
expect(enemy.hp).toBe(20);
expect(enemy.maxHp).toBe(20);
2026-04-10 13:43:12 +08:00
});
it("should create enemy with different values for different ranks", () => {
const jEnemy = createEnemy("enemy_0", "J", "spades");
const qEnemy = createEnemy("enemy_1", "Q", "hearts");
const kEnemy = createEnemy("enemy_2", "K", "diamonds");
expect(jEnemy.value).toBe(10);
expect(qEnemy.value).toBe(15);
expect(kEnemy.value).toBe(20);
expect(jEnemy.hp).toBe(20);
expect(qEnemy.hp).toBe(30);
expect(kEnemy.hp).toBe(40);
2026-04-10 13:43:12 +08:00
});
});
2026-04-10 13:43:12 +08:00
describe("createAllCards", () => {
it("should create 52 cards", () => {
const cards = createAllCards();
expect(Object.keys(cards).length).toBe(52);
2026-04-10 13:43:12 +08:00
});
it("should have all suits and ranks", () => {
const cards = createAllCards();
const suits = ["spades", "hearts", "diamonds", "clubs"];
const ranks = [
"A",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"J",
"Q",
"K",
];
for (const suit of suits) {
for (const rank of ranks) {
const id = `${suit}_${rank}`;
expect(cards[id]).toBeDefined();
expect(cards[id].suit).toBe(suit);
expect(cards[id].rank).toBe(rank);
}
}
2026-04-10 13:43:12 +08:00
});
});
2026-04-10 13:43:12 +08:00
describe("buildEnemyDeck", () => {
it("should create 12 enemies (J/Q/K)", () => {
const rng = new Mulberry32RNG(12345);
const deck = buildEnemyDeck(rng);
expect(deck.length).toBe(12);
2026-04-10 13:43:12 +08:00
});
it("should have J at top, Q in middle, K at bottom", () => {
const rng = new Mulberry32RNG(12345);
const deck = buildEnemyDeck(rng);
2026-04-10 13:43:12 +08:00
for (let i = 0; i < 4; i++) {
expect(deck[i].rank).toBe("J");
}
2026-04-10 13:43:12 +08:00
for (let i = 4; i < 8; i++) {
expect(deck[i].rank).toBe("Q");
}
2026-04-10 13:43:12 +08:00
for (let i = 8; i < 12; i++) {
expect(deck[i].rank).toBe("K");
}
2026-04-10 13:43:12 +08:00
});
});
describe("buildTavernDeck", () => {
it("should create 40 cards (A-10)", () => {
const rng = new Mulberry32RNG(12345);
const deck = buildTavernDeck(rng);
expect(deck.length).toBe(40);
});
it("should not contain face cards", () => {
const rng = new Mulberry32RNG(12345);
const deck = buildTavernDeck(rng);
for (const card of deck) {
expect(FACE_CARDS.includes(card.rank)).toBe(false);
}
});
});
describe("isEnemyDefeated", () => {
it("should return true when enemy HP <= 0", () => {
const enemy = createEnemy("enemy_0", "J", "spades");
expect(isEnemyDefeated(enemy)).toBe(false);
enemy.hp = 0;
expect(isEnemyDefeated(enemy)).toBe(true);
enemy.hp = -5;
expect(isEnemyDefeated(enemy)).toBe(true);
});
it("should return false for null enemy", () => {
expect(isEnemyDefeated(null)).toBe(false);
});
});
2026-04-10 13:43:12 +08:00
});
describe("Regicide - Commands", () => {
function createTestContext() {
const initialState = createInitialState();
return createGameContext(registry, initialState);
}
function setupTestGame(game: ReturnType<typeof createTestContext>) {
const cards = createAllCards();
const rng = new Mulberry32RNG(12345);
const enemyDeck = buildEnemyDeck(rng);
const tavernDeck = buildTavernDeck(rng);
game.produce((state) => {
state.cards = cards;
state.playerCount = 2;
state.currentPlayerIndex = 0;
state.enemyDeck = [...enemyDeck];
state.currentEnemy = { ...enemyDeck[0] };
for (const card of tavernDeck) {
state.regions.tavernDeck.childIds.push(card.id);
}
for (let i = 0; i < 6; i++) {
const card1 = tavernDeck[i];
const card2 = tavernDeck[i + 6];
card1.regionId = "hand_player1";
card2.regionId = "hand_player2";
state.playerHands.player1.push(card1.id);
state.playerHands.player2.push(card2.id);
state.regions.hand_player1.childIds.push(card1.id);
state.regions.hand_player2.childIds.push(card2.id);
}
2026-04-10 13:43:12 +08:00
});
}
describe("play command", () => {
it("should deal damage to current enemy", async () => {
const game = createTestContext();
setupTestGame(game);
2026-04-10 13:43:12 +08:00
const enemyHpBefore = game.value.currentEnemy!.hp;
const cardId = game.value.playerHands.player1[0];
const card = game.value.cards[cardId];
2026-04-10 13:43:12 +08:00
const result = await game.run(`play player1 ${cardId}`);
2026-04-10 13:43:12 +08:00
expect(game.value.currentEnemy!.hp).toBe(enemyHpBefore - card.value);
2026-04-10 13:43:12 +08:00
});
it("should double damage for clubs suit", async () => {
const game = createTestContext();
setupTestGame(game);
2026-04-10 13:43:12 +08:00
game.produce((state) => {
state.cards["clubs_5"] = createCard("clubs_5", "clubs", "5");
state.playerHands.player1.push("clubs_5");
state.regions.hand_player1.childIds.push("clubs_5");
});
2026-04-10 13:43:12 +08:00
const clubsCardId = "clubs_5";
const enemyHpBefore = game.value.currentEnemy!.hp;
const card = game.value.cards[clubsCardId];
2026-04-10 13:43:12 +08:00
await game.run(`play player1 ${clubsCardId}`);
2026-04-10 13:43:12 +08:00
expect(game.value.currentEnemy!.hp).toBe(enemyHpBefore - card.value * 2);
});
});
describe("pass command", () => {
it("should allow player to pass", async () => {
const game = createTestContext();
setupTestGame(game);
const result = await game.run("pass player1");
expect(result.success).toBe(true);
});
});
describe("check-enemy command", () => {
it("should detect defeated enemy and reveal next", async () => {
const game = createTestContext();
setupTestGame(game);
const firstEnemy = game.value.currentEnemy!;
game.produce((state) => {
state.currentEnemy!.hp = 0;
});
await game.run("check-enemy");
2026-04-10 13:43:12 +08:00
expect(game.value.regions.discardPile.childIds).toContain(firstEnemy.id);
expect(game.value.currentEnemy).not.toBe(firstEnemy);
});
it("should not defeat enemy if HP > 0", async () => {
const game = createTestContext();
setupTestGame(game);
2026-04-10 13:43:12 +08:00
const currentEnemyId = game.value.currentEnemy!.id;
2026-04-10 13:43:12 +08:00
await game.run("check-enemy");
expect(game.value.currentEnemy!.id).toBe(currentEnemyId);
2026-04-10 13:43:12 +08:00
});
});
2026-04-10 13:43:12 +08:00
describe("next-turn command", () => {
it("should switch to next player", async () => {
const game = createTestContext();
setupTestGame(game);
2026-04-10 13:43:12 +08:00
expect(game.value.currentPlayerIndex).toBe(0);
2026-04-10 13:43:12 +08:00
await game.run("next-turn");
2026-04-10 13:43:12 +08:00
expect(game.value.currentPlayerIndex).toBe(1);
});
2026-04-10 13:43:12 +08:00
it("should wrap around to first player", async () => {
const game = createTestContext();
setupTestGame(game);
2026-04-10 13:43:12 +08:00
game.produce((state) => {
state.currentPlayerIndex = 1;
});
2026-04-10 13:43:12 +08:00
await game.run("next-turn");
2026-04-10 13:43:12 +08:00
expect(game.value.currentPlayerIndex).toBe(0);
2026-04-10 13:43:12 +08:00
});
});
2026-04-10 13:43:12 +08:00
});
describe("Regicide - Game Flow", () => {
function createTestContext() {
const initialState = createInitialState();
return createGameContext(registry, initialState);
}
it("should complete a full turn cycle", async () => {
const game = createTestContext();
const cards = createAllCards();
const rng = new Mulberry32RNG(12345);
const enemyDeck = buildEnemyDeck(rng);
const tavernDeck = buildTavernDeck(rng);
game.produce((state) => {
state.cards = cards;
state.playerCount = 1;
state.currentPlayerIndex = 0;
state.enemyDeck = [...enemyDeck.slice(1)];
state.currentEnemy = { ...enemyDeck[0] };
for (const card of tavernDeck) {
state.regions.tavernDeck.childIds.push(card.id);
}
for (let i = 0; i < 6; i++) {
const card = tavernDeck[i];
card.regionId = "hand_player1";
state.playerHands.player1.push(card.id);
state.regions.hand_player1.childIds.push(card.id);
}
2026-04-10 13:43:12 +08:00
});
const cardId = game.value.playerHands.player1[0];
const card = game.value.cards[cardId];
const enemyHpBefore = game.value.currentEnemy!.hp;
2026-04-10 13:43:12 +08:00
await game.run(`play player1 ${cardId}`);
2026-04-10 13:43:12 +08:00
expect(game.value.currentEnemy!.hp).toBeLessThan(enemyHpBefore);
});
2026-04-10 13:43:12 +08:00
it("should win game when all enemies defeated", async () => {
const game = createTestContext();
2026-04-10 13:43:12 +08:00
const cards = createAllCards();
const rng = new Mulberry32RNG(12345);
const tavernDeck = buildTavernDeck(rng);
2026-04-10 13:43:12 +08:00
game.produce((state) => {
state.cards = cards;
state.playerCount = 1;
state.currentPlayerIndex = 0;
state.enemyDeck = [];
state.currentEnemy = null;
for (const card of tavernDeck) {
state.regions.tavernDeck.childIds.push(card.id);
}
});
game.produce((state) => {
state.phase = "victory";
state.winner = true;
2026-04-10 13:43:12 +08:00
});
expect(game.value.phase).toBe("victory");
expect(game.value.winner).toBe(true);
});
2026-04-10 13:43:12 +08:00
});