354 lines
11 KiB
TypeScript
354 lines
11 KiB
TypeScript
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);
|
|
});
|
|
});
|