Compare commits
2 Commits
15122defcc
...
94e021974f
| Author | SHA1 | Date |
|---|---|---|
|
|
94e021974f | |
|
|
793c7d834b |
|
|
@ -12,17 +12,20 @@ type BoopPart = Part & { player: PlayerType; pieceType: PieceType };
|
||||||
|
|
||||||
type PieceSupply = { supply: number; placed: number };
|
type PieceSupply = { supply: number; placed: number };
|
||||||
|
|
||||||
type PlayerSupply = {
|
type Player = {
|
||||||
|
id: PlayerType;
|
||||||
kitten: PieceSupply;
|
kitten: PieceSupply;
|
||||||
cat: PieceSupply;
|
cat: PieceSupply;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO refactor this into an Entity
|
type PlayerEntity = Entity<Player>;
|
||||||
function createPlayerSupply(): PlayerSupply {
|
|
||||||
return {
|
function createPlayer(id: PlayerType): PlayerEntity {
|
||||||
|
return entity<Player>(id, {
|
||||||
|
id,
|
||||||
kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 },
|
kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 },
|
||||||
cat: { supply: 0, placed: 0 },
|
cat: { supply: 0, placed: 0 },
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createInitialState() {
|
export function createInitialState() {
|
||||||
|
|
@ -38,8 +41,8 @@ export function createInitialState() {
|
||||||
currentPlayer: 'white' as PlayerType,
|
currentPlayer: 'white' as PlayerType,
|
||||||
winner: null as WinnerType,
|
winner: null as WinnerType,
|
||||||
players: {
|
players: {
|
||||||
white: createPlayerSupply(),
|
white: createPlayer('white'),
|
||||||
black: createPlayerSupply(),
|
black: createPlayer('black'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +50,24 @@ export type BoopState = ReturnType<typeof createInitialState>;
|
||||||
const registration = createGameCommandRegistry<BoopState>();
|
const registration = createGameCommandRegistry<BoopState>();
|
||||||
export const registry = registration.registry;
|
export const registry = registration.registry;
|
||||||
|
|
||||||
|
// Player Entity helper functions
|
||||||
|
export function getPlayer(host: Entity<BoopState>, player: PlayerType): PlayerEntity {
|
||||||
|
return host.value.players[player];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrementSupply(player: PlayerEntity, pieceType: PieceType) {
|
||||||
|
player.produce(p => {
|
||||||
|
p[pieceType].supply--;
|
||||||
|
p[pieceType].placed++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function incrementSupply(player: PlayerEntity, pieceType: PieceType, count?: number) {
|
||||||
|
player.produce(p => {
|
||||||
|
p[pieceType].supply += count ?? 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
registration.add('setup', async function() {
|
registration.add('setup', async function() {
|
||||||
const {context} = this;
|
const {context} = this;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -85,7 +106,8 @@ registration.add('turn <player>', async function(cmd) {
|
||||||
return `Cell (${row}, ${col}) is already occupied.`;
|
return `Cell (${row}, ${col}) is already occupied.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const supply = this.context.value.players[player][pieceType].supply;
|
const playerEntity = getPlayer(this.context, player);
|
||||||
|
const supply = playerEntity.value[pieceType].supply;
|
||||||
if (supply <= 0) {
|
if (supply <= 0) {
|
||||||
return `No ${pieceType}s left in ${player}'s supply.`;
|
return `No ${pieceType}s left in ${player}'s supply.`;
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +125,36 @@ registration.add('turn <player>', async function(cmd) {
|
||||||
processGraduation(this.context, turnPlayer, graduatedLines);
|
processGraduation(this.context, turnPlayer, graduatedLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (countPiecesOnBoard(this.context, turnPlayer) >= MAX_PIECES_PER_PLAYER) {
|
||||||
|
const board = getBoardRegion(this.context);
|
||||||
|
const partsMap = board.partsMap.value;
|
||||||
|
const availableKittens: Entity<BoopPart>[] = [];
|
||||||
|
for (const key in partsMap) {
|
||||||
|
const part = partsMap[key] as Entity<BoopPart>;
|
||||||
|
if (part.value.player === turnPlayer && part.value.pieceType === 'kitten') {
|
||||||
|
availableKittens.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availableKittens.length > 0) {
|
||||||
|
const graduateCmd = await this.prompt(
|
||||||
|
'graduate <row:number> <col:number>',
|
||||||
|
(command) => {
|
||||||
|
const [row, col] = command.params as [number, number];
|
||||||
|
const posKey = `${row},${col}`;
|
||||||
|
const part = availableKittens.find(p => `${p.value.position[0]},${p.value.position[1]}` === posKey);
|
||||||
|
if (!part) return `No kitten at (${row}, ${col}).`;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const [row, col] = graduateCmd.params as [number, number];
|
||||||
|
const part = availableKittens.find(p => p.value.position[0] === row && p.value.position[1] === col)!;
|
||||||
|
removePieceFromBoard(this.context, part);
|
||||||
|
const playerEntity = getPlayer(this.context, turnPlayer);
|
||||||
|
incrementSupply(playerEntity, 'cat', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const winner = checkWinner(this.context);
|
const winner = checkWinner(this.context);
|
||||||
if (winner) return { winner };
|
if (winner) return { winner };
|
||||||
|
|
||||||
|
|
@ -129,7 +181,8 @@ export function getPartAt(host: Entity<BoopState>, row: number, col: number): En
|
||||||
|
|
||||||
export function placePiece(host: Entity<BoopState>, row: number, col: number, player: PlayerType, pieceType: PieceType) {
|
export function placePiece(host: Entity<BoopState>, row: number, col: number, player: PlayerType, pieceType: PieceType) {
|
||||||
const board = getBoardRegion(host);
|
const board = getBoardRegion(host);
|
||||||
const count = host.value.players[player][pieceType].placed + 1;
|
const playerEntity = getPlayer(host, player);
|
||||||
|
const count = playerEntity.value[pieceType].placed + 1;
|
||||||
|
|
||||||
const piece: BoopPart = {
|
const piece: BoopPart = {
|
||||||
id: `${player}-${pieceType}-${count}`,
|
id: `${player}-${pieceType}-${count}`,
|
||||||
|
|
@ -140,12 +193,11 @@ export function placePiece(host: Entity<BoopState>, row: number, col: number, pl
|
||||||
};
|
};
|
||||||
host.produce(s => {
|
host.produce(s => {
|
||||||
const e = entity(piece.id, piece);
|
const e = entity(piece.id, piece);
|
||||||
s.players[player][pieceType].supply--;
|
|
||||||
s.players[player][pieceType].placed++;
|
|
||||||
board.produce(draft => {
|
board.produce(draft => {
|
||||||
draft.children.push(e);
|
draft.children.push(e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
decrementSupply(playerEntity, pieceType);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyBoops(host: Entity<BoopState>, placedRow: number, placedCol: number, placedType: PieceType) {
|
export function applyBoops(host: Entity<BoopState>, placedRow: number, placedCol: number, placedType: PieceType) {
|
||||||
|
|
@ -180,10 +232,9 @@ export function applyBoops(host: Entity<BoopState>, placedRow: number, placedCol
|
||||||
if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) {
|
if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) {
|
||||||
const pt = part.value.pieceType;
|
const pt = part.value.pieceType;
|
||||||
const pl = part.value.player;
|
const pl = part.value.player;
|
||||||
|
const playerEntity = getPlayer(host, pl);
|
||||||
removePieceFromBoard(host, part);
|
removePieceFromBoard(host, part);
|
||||||
host.produce(state => {
|
incrementSupply(playerEntity, pt);
|
||||||
state.players[pl][pt].supply++;
|
|
||||||
});
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,9 +248,13 @@ export function applyBoops(host: Entity<BoopState>, placedRow: number, placedCol
|
||||||
|
|
||||||
export function removePieceFromBoard(host: Entity<BoopState>, part: Entity<BoopPart>) {
|
export function removePieceFromBoard(host: Entity<BoopState>, part: Entity<BoopPart>) {
|
||||||
const board = getBoardRegion(host);
|
const board = getBoardRegion(host);
|
||||||
|
const playerEntity = getPlayer(host, part.value.player);
|
||||||
board.produce(draft => {
|
board.produce(draft => {
|
||||||
draft.children = draft.children.filter(p => p.id !== part.id);
|
draft.children = draft.children.filter(p => p.id !== part.id);
|
||||||
});
|
});
|
||||||
|
playerEntity.produce(p => {
|
||||||
|
p[part.value.pieceType].placed--;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIRECTIONS: [number, number][] = [
|
const DIRECTIONS: [number, number][] = [
|
||||||
|
|
@ -298,9 +353,19 @@ export function processGraduation(host: Entity<BoopState>, player: PlayerType, l
|
||||||
}
|
}
|
||||||
|
|
||||||
const count = partsToRemove.length;
|
const count = partsToRemove.length;
|
||||||
host.produce(state => {
|
const playerEntity = getPlayer(host, player);
|
||||||
state.players[player].cat.supply += count;
|
incrementSupply(playerEntity, 'cat', count);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
export function countPiecesOnBoard(host: Entity<BoopState>, player: PlayerType): number {
|
||||||
|
const board = getBoardRegion(host);
|
||||||
|
const partsMap = board.partsMap.value;
|
||||||
|
let count = 0;
|
||||||
|
for (const key in partsMap) {
|
||||||
|
const part = partsMap[key] as Entity<BoopPart>;
|
||||||
|
if (part.value.player === player) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkWinner(host: Entity<BoopState>): WinnerType {
|
export function checkWinner(host: Entity<BoopState>): WinnerType {
|
||||||
|
|
@ -318,13 +383,5 @@ export function checkWinner(host: Entity<BoopState>): WinnerType {
|
||||||
if (hasWinningLine(positions)) return player;
|
if (hasWinningLine(positions)) return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = host.value;
|
|
||||||
const whiteTotal = MAX_PIECES_PER_PLAYER - state.players.white.kitten.supply + state.players.white.cat.supply;
|
|
||||||
const blackTotal = MAX_PIECES_PER_PLAYER - state.players.black.kitten.supply + state.players.black.cat.supply;
|
|
||||||
|
|
||||||
if (whiteTotal >= MAX_PIECES_PER_PLAYER && blackTotal >= MAX_PIECES_PER_PLAYER) {
|
|
||||||
return 'draw';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ On your turn, perform the following steps:
|
||||||
|
|
||||||
### 1. Placing Pieces
|
### 1. Placing Pieces
|
||||||
|
|
||||||
Place one Kitten from your supply onto any empty space on the bed.
|
Place one piece (Kitten or Cat) from your supply onto any empty space on the bed.
|
||||||
|
|
||||||
### 2. The "Boop" Mechanic
|
### 2. The "Boop" Mechanic
|
||||||
|
|
||||||
|
|
@ -51,9 +51,9 @@ Placing a piece causes a **"boop."** Every piece (yours or your opponent's) in t
|
||||||
To win, you need Cats. You obtain Cats by lining up Kittens:
|
To win, you need Cats. You obtain Cats by lining up Kittens:
|
||||||
|
|
||||||
1. **Three in a Row:** If you line up three of your Kittens in a row (horizontally, vertically, or diagonally), they "graduate."
|
1. **Three in a Row:** If you line up three of your Kittens in a row (horizontally, vertically, or diagonally), they "graduate."
|
||||||
2. **The Process:** Remove the three Kittens from the board and return them to the box. Replace them in your personal supply with three **Cats**.
|
2. **The Process:** Remove the three Kittens from the board and return them to the box. Add three **Cats** to your personal supply.
|
||||||
3. **Multiple Rows:** If placing a piece creates multiple rows of three, you graduate all pieces involved in those rows.
|
3. **Multiple Rows:** If placing a piece creates multiple rows of three, you graduate all pieces involved in those rows.
|
||||||
4. **The 8-Piece Rule:** If a player has all 8 of their pieces on the board (a mix of Kittens and Cats) and no one has three-in-a-row, the player must graduate one of their Kittens on the board into a Cat to free up a piece.
|
4. **The 8-Piece Rule:** If a player has all 8 of their pieces on the board (a mix of Kittens and Cats) and no one has three-in-a-row, the player must choose one of their Kittens on the board to graduate into a Cat to free up a piece.
|
||||||
|
|
||||||
## How to Win
|
## How to Win
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,23 +130,23 @@ describe('Boop - helper functions', () => {
|
||||||
const state = getState(ctx);
|
const state = getState(ctx);
|
||||||
|
|
||||||
placePiece(state, 0, 0, 'white', 'kitten');
|
placePiece(state, 0, 0, 'white', 'kitten');
|
||||||
expect(state.value.players.white.kitten.supply).toBe(7);
|
expect(state.value.players.white.value.kitten.supply).toBe(7);
|
||||||
expect(state.value.players.black.kitten.supply).toBe(8);
|
expect(state.value.players.black.value.kitten.supply).toBe(8);
|
||||||
|
|
||||||
placePiece(state, 0, 1, 'black', 'kitten');
|
placePiece(state, 0, 1, 'black', 'kitten');
|
||||||
expect(state.value.players.white.kitten.supply).toBe(7);
|
expect(state.value.players.white.value.kitten.supply).toBe(7);
|
||||||
expect(state.value.players.black.kitten.supply).toBe(7);
|
expect(state.value.players.black.value.kitten.supply).toBe(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should decrement the correct player cat supply', () => {
|
it('should decrement the correct player cat supply', () => {
|
||||||
const { ctx } = createTestContext();
|
const { ctx } = createTestContext();
|
||||||
const state = getState(ctx);
|
const state = getState(ctx);
|
||||||
state.produce(s => {
|
state.produce(s => {
|
||||||
s.players.white.cat.supply = 3;
|
s.players.white.value.cat.supply = 3;
|
||||||
});
|
});
|
||||||
|
|
||||||
placePiece(state, 0, 0, 'white', 'cat');
|
placePiece(state, 0, 0, 'white', 'cat');
|
||||||
expect(state.value.players.white.cat.supply).toBe(2);
|
expect(state.value.players.white.value.cat.supply).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add piece to board region children', () => {
|
it('should add piece to board region children', () => {
|
||||||
|
|
@ -211,7 +211,7 @@ describe('Boop - helper functions', () => {
|
||||||
|
|
||||||
expect(getParts(state).length).toBe(1);
|
expect(getParts(state).length).toBe(1);
|
||||||
expect(getParts(state)[0].value.player).toBe('black');
|
expect(getParts(state)[0].value.player).toBe('black');
|
||||||
expect(state.value.players.white.kitten.supply).toBe(8);
|
expect(state.value.players.white.value.kitten.supply).toBe(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not boop piece if target cell is occupied', () => {
|
it('should not boop piece if target cell is occupied', () => {
|
||||||
|
|
@ -367,7 +367,7 @@ describe('Boop - helper functions', () => {
|
||||||
processGraduation(state, 'white', lines);
|
processGraduation(state, 'white', lines);
|
||||||
|
|
||||||
expect(getParts(state).length).toBe(0);
|
expect(getParts(state).length).toBe(0);
|
||||||
expect(state.value.players.white.cat.supply).toBe(3);
|
expect(state.value.players.white.value.cat.supply).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only graduate pieces on the winning lines', () => {
|
it('should only graduate pieces on the winning lines', () => {
|
||||||
|
|
@ -384,7 +384,7 @@ describe('Boop - helper functions', () => {
|
||||||
|
|
||||||
expect(getParts(state).length).toBe(1);
|
expect(getParts(state).length).toBe(1);
|
||||||
expect(getParts(state)[0].value.position).toEqual([3, 3]);
|
expect(getParts(state)[0].value.position).toEqual([3, 3]);
|
||||||
expect(state.value.players.white.cat.supply).toBe(3);
|
expect(state.value.players.white.value.cat.supply).toBe(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -537,9 +537,7 @@ describe('Boop - game flow', () => {
|
||||||
const { ctx } = createTestContext();
|
const { ctx } = createTestContext();
|
||||||
const state = getState(ctx);
|
const state = getState(ctx);
|
||||||
|
|
||||||
state.produce(s => {
|
state.value.players.white.value.kitten.supply = 0;
|
||||||
s.players.white.kitten.supply = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const promptPromise = waitForPrompt(ctx);
|
const promptPromise = waitForPrompt(ctx);
|
||||||
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
||||||
|
|
@ -598,16 +596,14 @@ describe('Boop - game flow', () => {
|
||||||
processGraduation(state, 'white', lines);
|
processGraduation(state, 'white', lines);
|
||||||
|
|
||||||
expect(getParts(state).length).toBe(0);
|
expect(getParts(state).length).toBe(0);
|
||||||
expect(state.value.players.white.cat.supply).toBe(3);
|
expect(state.value.players.white.value.cat.supply).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept placing a cat via play command', async () => {
|
it('should accept placing a cat via play command', async () => {
|
||||||
const { ctx } = createTestContext();
|
const { ctx } = createTestContext();
|
||||||
const state = getState(ctx);
|
const state = getState(ctx);
|
||||||
|
|
||||||
state.produce(s => {
|
state.value.players.white.value.cat.supply = 3;
|
||||||
s.players.white.cat.supply = 3;
|
|
||||||
});
|
|
||||||
|
|
||||||
const promptPromise = waitForPrompt(ctx);
|
const promptPromise = waitForPrompt(ctx);
|
||||||
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
||||||
|
|
@ -621,16 +617,14 @@ describe('Boop - game flow', () => {
|
||||||
expect(getParts(state).length).toBe(1);
|
expect(getParts(state).length).toBe(1);
|
||||||
expect(getParts(state)[0].id).toBe('white-cat-1');
|
expect(getParts(state)[0].id).toBe('white-cat-1');
|
||||||
expect(getParts(state)[0].value.pieceType).toBe('cat');
|
expect(getParts(state)[0].value.pieceType).toBe('cat');
|
||||||
expect(state.value.players.white.cat.supply).toBe(2);
|
expect(state.value.players.white.value.cat.supply).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject placing a cat when supply is empty', async () => {
|
it('should reject placing a cat when supply is empty', async () => {
|
||||||
const { ctx } = createTestContext();
|
const { ctx } = createTestContext();
|
||||||
const state = getState(ctx);
|
const state = getState(ctx);
|
||||||
|
|
||||||
state.produce(s => {
|
state.value.players.white.value.cat.supply = 0;
|
||||||
s.players.white.cat.supply = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const promptPromise = waitForPrompt(ctx);
|
const promptPromise = waitForPrompt(ctx);
|
||||||
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue