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

354 lines
11 KiB
TypeScript
Raw Permalink Normal View History

2026-04-10 13:43:12 +08:00
import {createGameContext} from '@/core/game';
import {registry} from '@/samples/regicide/commands';
import {createInitialState} from '@/samples/regicide/state';
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);
});
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);
});
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);
});
});
describe('createAllCards', () => {
it('should create 52 cards', () => {
const cards = createAllCards();
expect(Object.keys(cards).length).toBe(52);
});
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);
}
}
});
});
describe('buildEnemyDeck', () => {
it('should create 12 enemies (J/Q/K)', () => {
const rng = new Mulberry32RNG(12345);
const deck = buildEnemyDeck(rng);
expect(deck.length).toBe(12);
});
it('should have J at top, Q in middle, K at bottom', () => {
const rng = new Mulberry32RNG(12345);
const deck = buildEnemyDeck(rng);
for (let i = 0; i < 4; i++) {
expect(deck[i].rank).toBe('J');
}
for (let i = 4; i < 8; i++) {
expect(deck[i].rank).toBe('Q');
}
for (let i = 8; i < 12; i++) {
expect(deck[i].rank).toBe('K');
}
});
});
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);
});
});
});
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);
}
});
}
describe('play command', () => {
it('should deal damage to current enemy', async () => {
const game = createTestContext();
setupTestGame(game);
const enemyHpBefore = game.value.currentEnemy!.hp;
const cardId = game.value.playerHands.player1[0];
const card = game.value.cards[cardId];
const result = await game.run(`play player1 ${cardId}`);
expect(game.value.currentEnemy!.hp).toBe(enemyHpBefore - card.value);
});
it('should double damage for clubs suit', async () => {
const game = createTestContext();
setupTestGame(game);
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');
});
const clubsCardId = 'clubs_5';
const enemyHpBefore = game.value.currentEnemy!.hp;
const card = game.value.cards[clubsCardId];
await game.run(`play player1 ${clubsCardId}`);
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');
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);
const currentEnemyId = game.value.currentEnemy!.id;
await game.run('check-enemy');
expect(game.value.currentEnemy!.id).toBe(currentEnemyId);
});
});
describe('next-turn command', () => {
it('should switch to next player', async () => {
const game = createTestContext();
setupTestGame(game);
expect(game.value.currentPlayerIndex).toBe(0);
await game.run('next-turn');
expect(game.value.currentPlayerIndex).toBe(1);
});
it('should wrap around to first player', async () => {
const game = createTestContext();
setupTestGame(game);
game.produce(state => {
state.currentPlayerIndex = 1;
});
await game.run('next-turn');
expect(game.value.currentPlayerIndex).toBe(0);
});
});
});
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);
}
});
const cardId = game.value.playerHands.player1[0];
const card = game.value.cards[cardId];
const enemyHpBefore = game.value.currentEnemy!.hp;
await game.run(`play player1 ${cardId}`);
expect(game.value.currentEnemy!.hp).toBeLessThan(enemyHpBefore);
});
it('should win game when all enemies defeated', async () => {
const game = createTestContext();
const cards = createAllCards();
const rng = new Mulberry32RNG(12345);
const tavernDeck = buildTavernDeck(rng);
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;
});
expect(game.value.phase).toBe('victory');
expect(game.value.winner).toBe(true);
});
});