511 lines
19 KiB
TypeScript
511 lines
19 KiB
TypeScript
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|||
|
|
import {
|
|||
|
|
registry,
|
|||
|
|
createInitialState,
|
|||
|
|
OnitamaState,
|
|||
|
|
createPawns,
|
|||
|
|
createCards,
|
|||
|
|
createRegions,
|
|||
|
|
PlayerType,
|
|||
|
|
} from '@/samples/onitama';
|
|||
|
|
import { createGameContext } from '@/core/game';
|
|||
|
|
import type { PromptEvent } from '@/utils/command';
|
|||
|
|
|
|||
|
|
function createTestContext() {
|
|||
|
|
const ctx = createGameContext(registry, createInitialState());
|
|||
|
|
return { registry, ctx };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function createDeterministicContext() {
|
|||
|
|
// Create a state with known card distribution for testing
|
|||
|
|
const regions = createRegions();
|
|||
|
|
const pawns = createPawns();
|
|||
|
|
const cards = createCards();
|
|||
|
|
|
|||
|
|
// Populate board region
|
|||
|
|
for(const pawn of Object.values(pawns)){
|
|||
|
|
if(pawn.regionId === 'board'){
|
|||
|
|
regions.board.childIds.push(pawn.id);
|
|||
|
|
regions.board.partMap[pawn.position.join(',')] = pawn.id;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Force known card distribution
|
|||
|
|
const redCards = ['tiger', 'dragon'];
|
|||
|
|
const blackCards = ['frog', 'rabbit'];
|
|||
|
|
const spareCard = 'crab';
|
|||
|
|
|
|||
|
|
// Set card regions
|
|||
|
|
cards['tiger'].regionId = 'red';
|
|||
|
|
cards['dragon'].regionId = 'red';
|
|||
|
|
cards['frog'].regionId = 'black';
|
|||
|
|
cards['rabbit'].regionId = 'black';
|
|||
|
|
cards['crab'].regionId = 'spare';
|
|||
|
|
|
|||
|
|
regions.red.childIds = [...redCards];
|
|||
|
|
regions.black.childIds = [...blackCards];
|
|||
|
|
regions.spare.childIds = [spareCard];
|
|||
|
|
|
|||
|
|
const state = {
|
|||
|
|
regions,
|
|||
|
|
pawns,
|
|||
|
|
cards,
|
|||
|
|
currentPlayer: 'red' as PlayerType,
|
|||
|
|
winner: null as PlayerType | null,
|
|||
|
|
spareCard,
|
|||
|
|
redCards,
|
|||
|
|
blackCards,
|
|||
|
|
turn: 0,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const ctx = createGameContext(registry, () => state);
|
|||
|
|
return { registry, ctx };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function waitForPrompt(ctx: ReturnType<typeof createTestContext>['ctx']): Promise<PromptEvent> {
|
|||
|
|
return new Promise(resolve => {
|
|||
|
|
ctx._commands.on('prompt', resolve);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
describe('Onitama Game', () => {
|
|||
|
|
describe('Setup', () => {
|
|||
|
|
it('should create initial state correctly', () => {
|
|||
|
|
const state = createInitialState();
|
|||
|
|
|
|||
|
|
expect(state.currentPlayer).toBeDefined();
|
|||
|
|
expect(state.winner).toBeNull();
|
|||
|
|
expect(state.regions.board).toBeDefined();
|
|||
|
|
expect(state.regions.red).toBeDefined();
|
|||
|
|
expect(state.regions.black).toBeDefined();
|
|||
|
|
expect(state.regions.spare).toBeDefined();
|
|||
|
|
|
|||
|
|
// Should have 10 pawns (5 per player)
|
|||
|
|
const redPawns = Object.values(state.pawns).filter(p => p.owner === 'red');
|
|||
|
|
const blackPawns = Object.values(state.pawns).filter(p => p.owner === 'black');
|
|||
|
|
expect(redPawns.length).toBe(5);
|
|||
|
|
expect(blackPawns.length).toBe(5);
|
|||
|
|
|
|||
|
|
// Each player should have 1 master and 4 students
|
|||
|
|
const redMaster = redPawns.find(p => p.type === 'master');
|
|||
|
|
const redStudents = redPawns.filter(p => p.type === 'student');
|
|||
|
|
expect(redMaster).toBeDefined();
|
|||
|
|
expect(redStudents.length).toBe(4);
|
|||
|
|
|
|||
|
|
// Master should be at center
|
|||
|
|
expect(redMaster?.position[0]).toBe(2);
|
|||
|
|
expect(redMaster?.position[1]).toBe(0);
|
|||
|
|
|
|||
|
|
// Cards should be distributed: 2 per player + 1 spare
|
|||
|
|
expect(state.redCards.length).toBe(2);
|
|||
|
|
expect(state.blackCards.length).toBe(2);
|
|||
|
|
expect(state.spareCard).toBeDefined();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should create pawns in correct positions', () => {
|
|||
|
|
const pawns = createPawns();
|
|||
|
|
|
|||
|
|
// Red player at y=0
|
|||
|
|
expect(pawns['red-master'].position).toEqual([2, 0]);
|
|||
|
|
expect(pawns['red-student-1'].position).toEqual([0, 0]);
|
|||
|
|
expect(pawns['red-student-2'].position).toEqual([1, 0]);
|
|||
|
|
expect(pawns['red-student-3'].position).toEqual([3, 0]);
|
|||
|
|
expect(pawns['red-student-4'].position).toEqual([4, 0]);
|
|||
|
|
|
|||
|
|
// Black player at y=4
|
|||
|
|
expect(pawns['black-master'].position).toEqual([2, 4]);
|
|||
|
|
expect(pawns['black-student-1'].position).toEqual([0, 4]);
|
|||
|
|
expect(pawns['black-student-2'].position).toEqual([1, 4]);
|
|||
|
|
expect(pawns['black-student-3'].position).toEqual([3, 4]);
|
|||
|
|
expect(pawns['black-student-4'].position).toEqual([4, 4]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should parse cards correctly from CSV', () => {
|
|||
|
|
const cards = createCards();
|
|||
|
|
|
|||
|
|
// Should have 16 unique cards
|
|||
|
|
expect(Object.keys(cards).length).toBe(16);
|
|||
|
|
|
|||
|
|
// Check tiger card moves
|
|||
|
|
const tiger = cards['tiger'];
|
|||
|
|
expect(tiger.moveCandidates).toHaveLength(2);
|
|||
|
|
expect(tiger.moveCandidates).toContainEqual({ dx: 0, dy: 2 });
|
|||
|
|
expect(tiger.moveCandidates).toContainEqual({ dx: 0, dy: -1 });
|
|||
|
|
expect(tiger.startingPlayer).toBe('black');
|
|||
|
|
|
|||
|
|
// Check dragon card moves
|
|||
|
|
const dragon = cards['dragon'];
|
|||
|
|
expect(dragon.moveCandidates.length).toBe(4);
|
|||
|
|
expect(dragon.startingPlayer).toBe('red');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should create regions correctly', () => {
|
|||
|
|
const regions = createRegions();
|
|||
|
|
|
|||
|
|
expect(regions.board.id).toBe('board');
|
|||
|
|
expect(regions.board.axes).toHaveLength(2);
|
|||
|
|
expect(regions.board.axes[0].max).toBe(4);
|
|||
|
|
expect(regions.board.axes[1].max).toBe(4);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('Move Validation', () => {
|
|||
|
|
it('should validate card ownership', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Red tries to use a card they don't have
|
|||
|
|
const result = await ctx.run('move red frog 2 0 2 1');
|
|||
|
|
expect(result.success).toBe(false);
|
|||
|
|
if (!result.success) {
|
|||
|
|
expect(result.error).toContain('不拥有卡牌');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should validate pawn ownership', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Red tries to move a black pawn (at 2,4)
|
|||
|
|
const result = await ctx.run('move red tiger 2 4 2 2');
|
|||
|
|
expect(result.success).toBe(false);
|
|||
|
|
if (!result.success) {
|
|||
|
|
expect(result.error).toContain('不属于玩家');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should validate move is in card pattern', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Tiger card only allows specific moves, try invalid move
|
|||
|
|
const result = await ctx.run('move red tiger 2 0 3 0');
|
|||
|
|
expect(result.success).toBe(false);
|
|||
|
|
if (!result.success) {
|
|||
|
|
expect(result.error).toContain('不支持移动');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should prevent moving to position with own pawn', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Tiger allows dy=-2 or dy=1. Try to move to position with own pawn
|
|||
|
|
// Move student from 1,0 to 0,0 (occupied by another red student)
|
|||
|
|
// This requires dx=-1, dy=0 which tiger doesn't support
|
|||
|
|
const result = await ctx.run('move red tiger 1 0 0 0');
|
|||
|
|
expect(result.success).toBe(false);
|
|||
|
|
if (!result.success) {
|
|||
|
|
expect(result.error).toContain('不支持移动');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('Move Execution', () => {
|
|||
|
|
it('should move pawn correctly', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Move red student from 0,0 using tiger card (tiger allows dy=2)
|
|||
|
|
// From y=0, dy=2 goes to y=2
|
|||
|
|
const result = await ctx.run('move red tiger 0 0 0 2');
|
|||
|
|
expect(result.success).toBe(true);
|
|||
|
|
|
|||
|
|
const state = ctx.value;
|
|||
|
|
const pawn = state.pawns['red-student-1'];
|
|||
|
|
expect(pawn.position).toEqual([0, 2]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should capture enemy pawn', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Setup: place black student at 0,2
|
|||
|
|
ctx.produce(state => {
|
|||
|
|
const blackStudent = state.pawns['black-student-1'];
|
|||
|
|
blackStudent.position = [0, 2];
|
|||
|
|
state.regions.board.partMap['0,2'] = blackStudent.id;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Red captures with tiger card
|
|||
|
|
const result = await ctx.run('move red tiger 0 0 0 2');
|
|||
|
|
expect(result.success).toBe(true);
|
|||
|
|
|
|||
|
|
const state = ctx.value;
|
|||
|
|
// Black student should be removed from board
|
|||
|
|
const blackStudent = state.pawns['black-student-1'];
|
|||
|
|
expect(blackStudent.regionId).not.toBe('board');
|
|||
|
|
|
|||
|
|
// Red student should be at 0,2
|
|||
|
|
const redStudent = state.pawns['red-student-1'];
|
|||
|
|
expect(redStudent.position).toEqual([0, 2]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should swap card after move', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
const cardsBefore = ctx.value;
|
|||
|
|
expect(cardsBefore.redCards).toContain('tiger');
|
|||
|
|
expect(cardsBefore.spareCard).toBe('crab');
|
|||
|
|
|
|||
|
|
// Move using tiger card
|
|||
|
|
await ctx.run('move red tiger 0 0 0 2');
|
|||
|
|
|
|||
|
|
const state = ctx.value;
|
|||
|
|
// Tiger should now be spare, crab should be with red
|
|||
|
|
expect(state.redCards).toContain('crab');
|
|||
|
|
expect(state.redCards).not.toContain('tiger');
|
|||
|
|
expect(state.spareCard).toBe('tiger');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('Win Conditions', () => {
|
|||
|
|
it('should detect conquest win for red (master reaches y=4)', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Move red master to y=4
|
|||
|
|
ctx.produce(state => {
|
|||
|
|
const redMaster = state.pawns['red-master'];
|
|||
|
|
redMaster.position = [2, 4];
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const result = await ctx.run('check-win');
|
|||
|
|
expect(result.success).toBe(true);
|
|||
|
|
if (result.success) {
|
|||
|
|
expect(result.result).toBe('red');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should detect conquest win for black (master reaches y=0)', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Move black master to y=0
|
|||
|
|
ctx.produce(state => {
|
|||
|
|
const blackMaster = state.pawns['black-master'];
|
|||
|
|
blackMaster.position = [2, 0];
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const result = await ctx.run('check-win');
|
|||
|
|
expect(result.success).toBe(true);
|
|||
|
|
if (result.success) {
|
|||
|
|
expect(result.result).toBe('black');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should detect capture win when red master is captured', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Remove red master from board
|
|||
|
|
ctx.produce(state => {
|
|||
|
|
const redMaster = state.pawns['red-master'];
|
|||
|
|
redMaster.regionId = '';
|
|||
|
|
delete state.regions.board.partMap['2,0'];
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const result = await ctx.run('check-win');
|
|||
|
|
expect(result.success).toBe(true);
|
|||
|
|
if (result.success) {
|
|||
|
|
expect(result.result).toBe('black');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should detect capture win when black master is captured', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Remove black master from board
|
|||
|
|
ctx.produce(state => {
|
|||
|
|
const blackMaster = state.pawns['black-master'];
|
|||
|
|
blackMaster.regionId = '';
|
|||
|
|
delete state.regions.board.partMap['2,4'];
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const result = await ctx.run('check-win');
|
|||
|
|
expect(result.success).toBe(true);
|
|||
|
|
if (result.success) {
|
|||
|
|
expect(result.result).toBe('red');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('Card Swap', () => {
|
|||
|
|
it('should swap card between player and spare', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
const stateBefore = ctx.value;
|
|||
|
|
expect(stateBefore.redCards).toContain('tiger');
|
|||
|
|
expect(stateBefore.spareCard).toBe('crab');
|
|||
|
|
|
|||
|
|
const result = await ctx.run('swap-card red tiger');
|
|||
|
|
expect(result.success).toBe(true);
|
|||
|
|
|
|||
|
|
const state = ctx.value;
|
|||
|
|
expect(state.redCards).toContain('crab');
|
|||
|
|
expect(state.redCards).not.toContain('tiger');
|
|||
|
|
expect(state.spareCard).toBe('tiger');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('Turn Flow', () => {
|
|||
|
|
it('should switch player after turn', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Force red to be current player
|
|||
|
|
ctx.produce(state => {
|
|||
|
|
state.currentPlayer = 'red';
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Start turn
|
|||
|
|
const promptPromise = waitForPrompt(ctx);
|
|||
|
|
const runPromise = ctx.run('turn red');
|
|||
|
|
const promptEvent = await promptPromise;
|
|||
|
|
|
|||
|
|
// Make a valid move - tiger allows dy=2, move student from 0,0 to 0,2
|
|||
|
|
const error = promptEvent.tryCommit({
|
|||
|
|
name: 'move',
|
|||
|
|
params: ['red', 'tiger', 0, 0, 0, 2],
|
|||
|
|
options: {},
|
|||
|
|
flags: {}
|
|||
|
|
});
|
|||
|
|
expect(error).toBeNull();
|
|||
|
|
|
|||
|
|
const result = await runPromise;
|
|||
|
|
expect(result.success).toBe(true);
|
|||
|
|
|
|||
|
|
const state = ctx.value;
|
|||
|
|
// Should now be black's turn
|
|||
|
|
expect(state.currentPlayer).toBe('black');
|
|||
|
|
expect(state.turn).toBe(1);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should end game when win condition met', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Set up winning scenario - move red master to y=2, one step from winning
|
|||
|
|
ctx.produce(state => {
|
|||
|
|
state.currentPlayer = 'red';
|
|||
|
|
const redMaster = state.pawns['red-master'];
|
|||
|
|
// Clear old position
|
|||
|
|
delete state.regions.board.partMap['2,0'];
|
|||
|
|
// Set new position
|
|||
|
|
redMaster.position = [2, 2];
|
|||
|
|
state.regions.board.partMap['2,2'] = redMaster.id;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Red moves master to winning position (y=4)
|
|||
|
|
// Tiger allows dy=2
|
|||
|
|
const promptPromise = waitForPrompt(ctx);
|
|||
|
|
const runPromise = ctx.run('turn red');
|
|||
|
|
const promptEvent = await promptPromise;
|
|||
|
|
|
|||
|
|
const error = promptEvent.tryCommit({
|
|||
|
|
name: 'move',
|
|||
|
|
params: ['red', 'tiger', 2, 2, 2, 4],
|
|||
|
|
options: {},
|
|||
|
|
flags: {}
|
|||
|
|
});
|
|||
|
|
expect(error).toBeNull();
|
|||
|
|
|
|||
|
|
const result = await runPromise;
|
|||
|
|
expect(result.success).toBe(true);
|
|||
|
|
|
|||
|
|
const state = ctx.value;
|
|||
|
|
expect(state.winner).toBe('red');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('Available Moves', () => {
|
|||
|
|
it('should calculate valid moves for player', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Give red cards and verify they can make moves
|
|||
|
|
ctx.produce(state => {
|
|||
|
|
state.redCards = ['tiger', 'dragon'];
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Tiger from 0,0 can go to 0,2 (dy=2)
|
|||
|
|
// Just verify the move is valid without triggering prompt
|
|||
|
|
const result = await ctx.run('move red tiger 0 0 0 2');
|
|||
|
|
expect(result.success).toBe(true);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('No Available Moves', () => {
|
|||
|
|
it('should allow card swap when no moves available', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Setup a scenario where red has no valid moves
|
|||
|
|
// Move all red pawns to positions where they can't move with available cards
|
|||
|
|
ctx.produce(state => {
|
|||
|
|
// Move all red students to back rank or blocked positions
|
|||
|
|
for (let i = 1; i <= 4; i++) {
|
|||
|
|
const student = state.pawns[`red-student-${i}`];
|
|||
|
|
student.position = [i === 5 ? 4 : i - 1, 1]; // All at y=1
|
|||
|
|
}
|
|||
|
|
state.pawns['red-master'].position = [2, 1];
|
|||
|
|
|
|||
|
|
// Give only cards that move backwards (negative dy)
|
|||
|
|
state.redCards = ['tiger'];
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const promptPromise = waitForPrompt(ctx);
|
|||
|
|
const runPromise = ctx.run('turn red');
|
|||
|
|
const promptEvent = await promptPromise;
|
|||
|
|
|
|||
|
|
// Should prompt for card swap
|
|||
|
|
expect(promptEvent).toBeDefined();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('Edge Cases', () => {
|
|||
|
|
it('should handle capturing master ending the game', async () => {
|
|||
|
|
const { ctx } = createDeterministicContext();
|
|||
|
|
|
|||
|
|
// Setup: black student adjacent to red master
|
|||
|
|
ctx.produce(state => {
|
|||
|
|
state.currentPlayer = 'black';
|
|||
|
|
// Move red master to 2,2
|
|||
|
|
const redMaster = state.pawns['red-master'];
|
|||
|
|
delete state.regions.board.partMap['2,0'];
|
|||
|
|
redMaster.position = [2, 2];
|
|||
|
|
state.regions.board.partMap['2,2'] = redMaster.id;
|
|||
|
|
|
|||
|
|
// Move black student to 2,4
|
|||
|
|
const blackStudent = state.pawns['black-student-1'];
|
|||
|
|
delete state.regions.board.partMap['0,4'];
|
|||
|
|
blackStudent.position = [2, 4];
|
|||
|
|
state.regions.board.partMap['2,4'] = blackStudent.id;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Move from 2,4 to 2,3 (dx=0, dy=-1) - but frog doesn't support this!
|
|||
|
|
// Let's use goose instead which supports dx=-1,dy=-1
|
|||
|
|
ctx.produce(state => {
|
|||
|
|
state.blackCards = ['goose'];
|
|||
|
|
state.regions.black.childIds = ['goose'];
|
|||
|
|
state.cards['goose'].regionId = 'black';
|
|||
|
|
// Move red master to 1,3
|
|||
|
|
const redMaster = state.pawns['red-master'];
|
|||
|
|
delete state.regions.board.partMap['2,3'];
|
|||
|
|
redMaster.position = [1, 3];
|
|||
|
|
state.regions.board.partMap['1,3'] = redMaster.id;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const promptPromise = waitForPrompt(ctx);
|
|||
|
|
const runPromise = ctx.run('turn black');
|
|||
|
|
const promptEvent = await promptPromise;
|
|||
|
|
|
|||
|
|
// Goose: dx=-1,dy=-1; dx=-1,dy=0; dx=1,dy=0; dx=1,dy=1
|
|||
|
|
// Move from 2,4 to 1,3 (dx=-1, dy=-1) - captures red master
|
|||
|
|
const error = promptEvent.tryCommit({
|
|||
|
|
name: 'move',
|
|||
|
|
params: ['black', 'goose', 2, 4, 1, 3],
|
|||
|
|
options: {},
|
|||
|
|
flags: {}
|
|||
|
|
});
|
|||
|
|
expect(error).toBeNull();
|
|||
|
|
|
|||
|
|
const result = await runPromise;
|
|||
|
|
expect(result.success).toBe(true);
|
|||
|
|
|
|||
|
|
const state = ctx.value;
|
|||
|
|
// Red master should be removed from board
|
|||
|
|
const redMaster = state.pawns['red-master'];
|
|||
|
|
expect(redMaster.regionId).not.toBe('board');
|
|||
|
|
expect(state.winner).toBe('black');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|