From eb0ebf54118c56bc29754c28f43b44c9ba706b83 Mon Sep 17 00:00:00 2001 From: hypercross Date: Fri, 3 Apr 2026 12:46:02 +0800 Subject: [PATCH] refactor: transition to non child entity format --- src/core/part.ts | 28 +-- src/core/region.ts | 176 +++++++------- src/index.ts | 2 +- src/samples/boop/index.ts | 192 +++++++-------- src/samples/tic-tac-toe.ts | 30 +-- tests/core/region.test.ts | 381 +++++++++++++++--------------- tests/samples/boop.test.ts | 86 +++---- tests/samples/tic-tac-toe.test.ts | 8 +- 8 files changed, 425 insertions(+), 478 deletions(-) diff --git a/src/core/part.ts b/src/core/part.ts index 847debf..962fca7 100644 --- a/src/core/part.ts +++ b/src/core/part.ts @@ -1,5 +1,3 @@ -import {Entity} from "@/utils/entity"; -import {Region} from "./region"; import {RNG} from "@/utils/rng"; export type Part = { @@ -10,27 +8,21 @@ export type Part = { alignments?: string[]; alignment?: string; - region: Entity; + regionId: string; position: number[]; } -export function flip(part: Entity) { - part.produce(draft => { - if(!draft.sides)return; - draft.side = ((draft.side||0) + 1) % draft.sides; - }); +export function flip(part: Part) { + if(!part.sides) return; + part.side = ((part.side || 0) + 1) % part.sides; } -export function flipTo(part: Entity, side: number) { - part.produce(draft => { - if(!draft.sides || side >= draft.sides)return; - draft.side = side; - }); +export function flipTo(part: Part, side: number) { + if(!part.sides || side >= part.sides) return; + part.side = side; } -export function roll(part: Entity, rng: RNG) { - part.produce(draft => { - if(!draft.sides)return; - draft.side = rng.nextInt(draft.sides); - }); +export function roll(part: Part, rng: RNG) { + if(!part.sides) return; + part.side = rng.nextInt(part.sides); } diff --git a/src/core/region.ts b/src/core/region.ts index 9713a20..45c3fd1 100644 --- a/src/core/region.ts +++ b/src/core/region.ts @@ -1,12 +1,11 @@ -import {batch, computed, ReadonlySignal, SignalOptions} from "@preact/signals-core"; -import {Entity} from "@/utils/entity"; import {Part} from "./part"; import {RNG} from "@/utils/rng"; export type Region = { id: string; axes: RegionAxis[]; - children: Entity[]; + childIds: string[]; + partMap: Record; } export type RegionAxis = { @@ -16,38 +15,39 @@ export type RegionAxis = { align?: 'start' | 'end' | 'center'; } -export class RegionEntity extends Entity { - public readonly partsMap: ReadonlySignal>>; +export function createRegion(id: string, axes: RegionAxis[]): Region { + return { + id, + axes, + childIds: [], + partMap: {}, + }; +} - public constructor(id: string, t?: Region, options?: SignalOptions) { - super(id, t, options); - this.partsMap = computed(() => { - const result: Record> = {}; - for (const child of this.value.children) { - const key = child.value.position.join(','); - result[key] = child; - } - return result; - }); +function buildPartMap(region: Region, parts: Record) { + const map: Record = {}; + for (const childId of region.childIds) { + const part = parts[childId]; + if (part) { + map[part.position.join(',')] = childId; + } } + return map; } -export function applyAlign(region: Entity) { - batch(() => { - region.produce(applyAlignCore); - }); -} - -function applyAlignCore(region: Region) { - if (region.children.length === 0) return; +export function applyAlign(region: Region, parts: Record) { + if (region.childIds.length === 0) return; for (let axisIndex = 0; axisIndex < region.axes.length; axisIndex++) { const axis = region.axes[axisIndex]; if (!axis.align) continue; const positionValues = new Set(); - for (const child of region.children) { - positionValues.add(child.value.position[axisIndex] ?? 0); + for (const childId of region.childIds) { + const part = parts[childId]; + if (part) { + positionValues.add(part.position[axisIndex] ?? 0); + } } const sortedPositions = Array.from(positionValues).sort((a, b) => a - b); @@ -75,86 +75,78 @@ function applyAlignCore(region: Region) { }); } - for (const child of region.children) { - child.produce(draft => { - const currentPos = draft.position[axisIndex] ?? 0; - draft.position[axisIndex] = positionMap.get(currentPos) ?? currentPos; - }); + for (const childId of region.childIds) { + const part = parts[childId]; + if (part) { + const currentPos = part.position[axisIndex] ?? 0; + part.position[axisIndex] = positionMap.get(currentPos) ?? currentPos; + } } } - region.children.sort((a, b) => { + region.childIds.sort((aId, bId) => { + const a = parts[aId]; + const b = parts[bId]; + if (!a || !b) return 0; for (let i = 0; i < region.axes.length; i++) { - const diff = (a.value.position[i] ?? 0) - (b.value.position[i] ?? 0); + const diff = (a.position[i] ?? 0) - (b.position[i] ?? 0); if (diff !== 0) return diff; } return 0; }); + + region.partMap = buildPartMap(region, parts); } -export function shuffle(region: Entity, rng: RNG) { - batch(() => { - region.produce(region => shuffleCore(region, rng)); - }); -} +export function shuffle(region: Region, parts: Record, rng: RNG){ + if (region.childIds.length <= 1) return; -function shuffleCore(region: Region, rng: RNG){ - if (region.children.length <= 1) return; - - const children = [...region.children]; - for (let i = children.length - 1; i > 0; i--) { + const childIds = [...region.childIds]; + for (let i = childIds.length - 1; i > 0; i--) { const j = rng.nextInt(i + 1); - const posI = [...children[i].value.position]; - const posJ = [...children[j].value.position]; - children[i].produce(draft => { - draft.position = posJ; - }); - children[j].produce(draft => { - draft.position = posI; - }); + const partI = parts[childIds[i]]; + const partJ = parts[childIds[j]]; + if (!partI || !partJ) continue; + + const posI = [...partI.position]; + const posJ = [...partJ.position]; + partI.position = posJ; + partJ.position = posI; + } + + region.partMap = buildPartMap(region, parts); +} + +export function moveToRegion(part: Part, sourceRegion: Region, targetRegion: Region, position?: number[]) { + sourceRegion.childIds = sourceRegion.childIds.filter(id => id !== part.id); + delete sourceRegion.partMap[part.position.join(',')]; + + targetRegion.childIds.push(part.id); + if (position) { + part.position = position; + } + targetRegion.partMap[part.position.join(',')] = part.id; + + part.regionId = targetRegion.id; +} + +export function moveToRegionAll(parts: Part[], sourceRegion: Region, targetRegion: Region, positions?: number[][]) { + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + sourceRegion.childIds = sourceRegion.childIds.filter(id => id !== part.id); + delete sourceRegion.partMap[part.position.join(',')]; + + targetRegion.childIds.push(part.id); + if (positions && positions[i]) { + part.position = positions[i]; + } + targetRegion.partMap[part.position.join(',')] = part.id; + + part.regionId = targetRegion.id; } } -export function moveToRegion(part: Entity, targetRegion: Entity, position?: number[]) { - const sourceRegion = part.value.region; - batch(() => { - sourceRegion.produce(draft => { - draft.children = draft.children.filter(c => c.id !== part.id); - }); - targetRegion.produce(draft => { - draft.children.push(part); - }); - part.produce(draft => { - draft.region = targetRegion; - if (position) draft.position = position; - }); - }); +export function removeFromRegion(part: Part, region: Region) { + region.childIds = region.childIds.filter(id => id !== part.id); + delete region.partMap[part.position.join(',')]; } - -export function moveToRegionAll(parts: Entity[], targetRegion: Entity, positions?: number[][]) { - batch(() => { - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - const sourceRegion = part.value.region; - sourceRegion.produce(draft => { - draft.children = draft.children.filter(c => c.id !== part.id); - }); - targetRegion.produce(draft => { - draft.children.push(part); - }); - part.produce(draft => { - draft.region = targetRegion; - if (positions && positions[i]) draft.position = positions[i]; - }); - } - }); -} - -export function removeFromRegion(part: Entity) { - const region = part.value.region; - batch(() => { - region.produce(draft => { - draft.children = draft.children.filter(c => c.id !== part.id); - }); - }); -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index fb3f086..29f1c50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ export type { Part } from './core/part'; export { flip, flipTo, roll } from './core/part'; export type { Region, RegionAxis } from './core/region'; -export { applyAlign, shuffle, RegionEntity, moveToRegion, moveToRegionAll, removeFromRegion } from './core/region'; +export { createRegion, applyAlign, shuffle, moveToRegion, moveToRegionAll, removeFromRegion } from './core/region'; // Utils export type { Command, CommandSchema, CommandParamSchema, CommandOptionSchema, CommandFlagSchema } from './utils/command'; diff --git a/src/samples/boop/index.ts b/src/samples/boop/index.ts index f8f1b37..ab9c8c9 100644 --- a/src/samples/boop/index.ts +++ b/src/samples/boop/index.ts @@ -1,4 +1,4 @@ -import {createGameCommandRegistry, Part, Entity, entity, RegionEntity} from '@/index'; +import {createGameCommandRegistry, Part, Entity, createRegion} from '@/index'; const BOARD_SIZE = 6; const MAX_PIECES_PER_PLAYER = 8; @@ -18,26 +18,15 @@ type Player = { cat: PieceSupply; }; -type PlayerEntity = Entity; - -function createPlayer(id: PlayerType): PlayerEntity { - return entity(id, { - id, - kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 }, - cat: { supply: 0, placed: 0 }, - }); -} +type PlayerData = Record; export function createInitialState() { return { - board: new RegionEntity('board', { - id: 'board', - axes: [ - { name: 'x', min: 0, max: BOARD_SIZE - 1 }, - { name: 'y', min: 0, max: BOARD_SIZE - 1 }, - ], - children: [], - }), + board: createRegion('board', [ + { name: 'x', min: 0, max: BOARD_SIZE - 1 }, + { name: 'y', min: 0, max: BOARD_SIZE - 1 }, + ]), + pieces: [] as BoopPart[], currentPlayer: 'white' as PlayerType, winner: null as WinnerType, players: { @@ -46,26 +35,30 @@ export function createInitialState() { }, }; } + +function createPlayer(id: PlayerType): Player { + return { + id, + kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 }, + cat: { supply: 0, placed: 0 }, + }; +} + export type BoopState = ReturnType; const registration = createGameCommandRegistry(); export const registry = registration.registry; -// Player Entity helper functions -export function getPlayer(host: Entity, player: PlayerType): PlayerEntity { +export function getPlayer(host: Entity, player: PlayerType): Player { return host.value.players[player]; } -export function decrementSupply(player: PlayerEntity, pieceType: PieceType) { - player.produce(p => { - p[pieceType].supply--; - p[pieceType].placed++; - }); +export function decrementSupply(player: Player, pieceType: PieceType) { + player[pieceType].supply--; + player[pieceType].placed++; } -export function incrementSupply(player: PlayerEntity, pieceType: PieceType, count?: number) { - player.produce(p => { - p[pieceType].supply += count ?? 1; - }); +export function incrementSupply(player: Player, pieceType: PieceType, count?: number) { + player[pieceType].supply += count ?? 1; } registration.add('setup', async function() { @@ -106,8 +99,8 @@ registration.add('turn ', async function(cmd) { return `Cell (${row}, ${col}) is already occupied.`; } - const playerEntity = getPlayer(this.context, player); - const supply = playerEntity.value[pieceType].supply; + const playerData = getPlayer(this.context, player); + const supply = playerData[pieceType].supply; if (supply <= 0) { return `No ${pieceType}s left in ${player}'s supply.`; } @@ -126,15 +119,9 @@ registration.add('turn ', async function(cmd) { } if (countPiecesOnBoard(this.context, turnPlayer) >= MAX_PIECES_PER_PLAYER) { - const board = getBoardRegion(this.context); - const partsMap = board.partsMap.value; - const availableKittens: Entity[] = []; - for (const key in partsMap) { - const part = partsMap[key] as Entity; - if (part.value.player === turnPlayer && part.value.pieceType === 'kitten') { - availableKittens.push(part); - } - } + const availableKittens = this.context.value.pieces.filter( + p => p.player === turnPlayer && p.pieceType === 'kitten' + ); if (availableKittens.length > 0) { const graduateCmd = await this.prompt( @@ -142,16 +129,16 @@ registration.add('turn ', async function(cmd) { (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); + const part = availableKittens.find(p => `${p.position[0]},${p.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)!; + const part = availableKittens.find(p => p.position[0] === row && p.position[1] === col)!; removePieceFromBoard(this.context, part); - const playerEntity = getPlayer(this.context, turnPlayer); - incrementSupply(playerEntity, 'cat', 1); + const playerData = getPlayer(this.context, turnPlayer); + incrementSupply(playerData, 'cat', 1); } } @@ -171,44 +158,44 @@ export function getBoardRegion(host: Entity) { export function isCellOccupied(host: Entity, row: number, col: number): boolean { const board = getBoardRegion(host); - return board.partsMap.value[`${row},${col}`] !== undefined; + return board.partMap[`${row},${col}`] !== undefined; } -export function getPartAt(host: Entity, row: number, col: number): Entity | null { +export function getPartAt(host: Entity, row: number, col: number): BoopPart | null { const board = getBoardRegion(host); - return (board.partsMap.value[`${row},${col}`] as Entity | undefined) || null; + const partId = board.partMap[`${row},${col}`]; + if (!partId) return null; + return host.value.pieces.find(p => p.id === partId) || null; } export function placePiece(host: Entity, row: number, col: number, player: PlayerType, pieceType: PieceType) { const board = getBoardRegion(host); - const playerEntity = getPlayer(host, player); - const count = playerEntity.value[pieceType].placed + 1; + const playerData = getPlayer(host, player); + const count = playerData[pieceType].placed + 1; const piece: BoopPart = { id: `${player}-${pieceType}-${count}`, - region: board, + regionId: 'board', position: [row, col], player, pieceType, }; host.produce(s => { - const e = entity(piece.id, piece); - board.produce(draft => { - draft.children.push(e); - }); + s.pieces.push(piece); + board.childIds.push(piece.id); + board.partMap[`${row},${col}`] = piece.id; }); - decrementSupply(playerEntity, pieceType); + decrementSupply(playerData, pieceType); } export function applyBoops(host: Entity, placedRow: number, placedCol: number, placedType: PieceType) { const board = getBoardRegion(host); - const partsMap = board.partsMap.value; + const pieces = host.value.pieces; - const piecesToBoop: { part: Entity; dr: number; dc: number }[] = []; + const piecesToBoop: { part: BoopPart; dr: number; dc: number }[] = []; - for (const key in partsMap) { - const part = partsMap[key] as Entity; - const [r, c] = part.value.position; + for (const part of pieces) { + const [r, c] = part.position; if (r === placedRow && c === placedCol) continue; const dr = Math.sign(r - placedRow); @@ -216,7 +203,7 @@ export function applyBoops(host: Entity, placedRow: number, placedCol if (Math.abs(r - placedRow) <= 1 && Math.abs(c - placedCol) <= 1) { const booperIsKitten = placedType === 'kitten'; - const targetIsCat = part.value.pieceType === 'cat'; + const targetIsCat = part.pieceType === 'cat'; if (booperIsKitten && targetIsCat) continue; @@ -225,36 +212,38 @@ export function applyBoops(host: Entity, placedRow: number, placedCol } for (const { part, dr, dc } of piecesToBoop) { - const [r, c] = part.value.position; + const [r, c] = part.position; const newRow = r + dr; const newCol = c + dc; if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) { - const pt = part.value.pieceType; - const pl = part.value.player; - const playerEntity = getPlayer(host, pl); + const pt = part.pieceType; + const pl = part.player; + const playerData = getPlayer(host, pl); removePieceFromBoard(host, part); - incrementSupply(playerEntity, pt); + incrementSupply(playerData, pt); continue; } if (isCellOccupied(host, newRow, newCol)) continue; - part.produce(p => { - p.position = [newRow, newCol]; - }); + part.position = [newRow, newCol]; + board.partMap = Object.fromEntries( + board.childIds.map(id => { + const p = pieces.find(x => x.id === id)!; + return [p.position.join(','), id]; + }) + ); } } -export function removePieceFromBoard(host: Entity, part: Entity) { +export function removePieceFromBoard(host: Entity, part: BoopPart) { const board = getBoardRegion(host); - const playerEntity = getPlayer(host, part.value.player); - board.produce(draft => { - draft.children = draft.children.filter(p => p.id !== part.id); - }); - playerEntity.produce(p => { - p[part.value.pieceType].placed--; - }); + const playerData = getPlayer(host, part.player); + board.childIds = board.childIds.filter(id => id !== part.id); + delete board.partMap[part.position.join(',')]; + host.value.pieces = host.value.pieces.filter(p => p.id !== part.id); + playerData[part.pieceType].placed--; } const DIRECTIONS: [number, number][] = [ @@ -309,14 +298,12 @@ export function hasWinningLine(positions: number[][]): boolean { } export function checkGraduation(host: Entity, player: PlayerType): number[][][] { - const board = getBoardRegion(host); - const partsMap = board.partsMap.value; + const pieces = host.value.pieces; const posSet = new Set(); - for (const key in partsMap) { - const part = partsMap[key] as Entity; - if (part.value.player === player && part.value.pieceType === 'kitten') { - posSet.add(`${part.value.position[0]},${part.value.position[1]}`); + for (const part of pieces) { + if (part.player === player && part.pieceType === 'kitten') { + posSet.add(`${part.position[0]},${part.position[1]}`); } } @@ -338,48 +325,31 @@ export function processGraduation(host: Entity, player: PlayerType, l } const board = getBoardRegion(host); - const partsMap = board.partsMap.value; - const partsToRemove: Entity[] = []; - - for (const key in partsMap) { - const part = partsMap[key] as Entity; - if (part.value.player === player && part.value.pieceType === 'kitten' && allPositions.has(`${part.value.position[0]},${part.value.position[1]}`)) { - partsToRemove.push(part); - } - } + const partsToRemove = host.value.pieces.filter( + p => p.player === player && p.pieceType === 'kitten' && allPositions.has(`${p.position[0]},${p.position[1]}`) + ); for (const part of partsToRemove) { removePieceFromBoard(host, part); } const count = partsToRemove.length; - const playerEntity = getPlayer(host, player); - incrementSupply(playerEntity, 'cat', count); + const playerData = getPlayer(host, player); + incrementSupply(playerData, 'cat', count); } export function countPiecesOnBoard(host: Entity, 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; - if (part.value.player === player) count++; - } - return count; + const pieces = host.value.pieces; + return pieces.filter(p => p.player === player).length; } export function checkWinner(host: Entity): WinnerType { - const board = getBoardRegion(host); - const partsMap = board.partsMap.value; + const pieces = host.value.pieces; for (const player of ['white', 'black'] as PlayerType[]) { - const positions: number[][] = []; - for (const key in partsMap) { - const part = partsMap[key] as Entity; - if (part.value.player === player && part.value.pieceType === 'cat') { - positions.push(part.value.position); - } - } + const positions = pieces + .filter(p => p.player === player && p.pieceType === 'cat') + .map(p => p.position); if (hasWinningLine(positions)) return player; } diff --git a/src/samples/tic-tac-toe.ts b/src/samples/tic-tac-toe.ts index de8cab1..6f1f73b 100644 --- a/src/samples/tic-tac-toe.ts +++ b/src/samples/tic-tac-toe.ts @@ -1,4 +1,4 @@ -import {createGameCommandRegistry, Part, Entity, entity, RegionEntity} from '@/index'; +import {createGameCommandRegistry, Part, Entity, createRegion, moveToRegion} from '@/index'; const BOARD_SIZE = 3; const MAX_TURNS = BOARD_SIZE * BOARD_SIZE; @@ -20,15 +20,11 @@ type TicTacToePart = Part & { player: PlayerType }; export function createInitialState() { return { - board: new RegionEntity('board', { - id: 'board', - axes: [ - { name: 'x', min: 0, max: BOARD_SIZE - 1 }, - { name: 'y', min: 0, max: BOARD_SIZE - 1 }, - ], - children: [], - }), - parts: [] as Entity[], + board: createRegion('board', [ + { name: 'x', min: 0, max: BOARD_SIZE - 1 }, + { name: 'y', min: 0, max: BOARD_SIZE - 1 }, + ]), + parts: [] as TicTacToePart[], currentPlayer: 'X' as PlayerType, winner: null as WinnerType, turn: 0, @@ -96,7 +92,7 @@ function isValidMove(row: number, col: number): boolean { export function isCellOccupied(host: Entity, row: number, col: number): boolean { const board = host.value.board; - return board.partsMap.value[`${row},${col}`] !== undefined; + return board.partMap[`${row},${col}`] !== undefined; } export function hasWinningLine(positions: number[][]): boolean { @@ -108,7 +104,7 @@ export function hasWinningLine(positions: number[][]): boolean { } export function checkWinner(host: Entity): WinnerType { - const parts = host.value.parts.map((e: Entity) => e.value); + const parts = host.value.parts; const xPositions = parts.filter((p: TicTacToePart) => p.player === 'X').map((p: TicTacToePart) => p.position); const oPositions = parts.filter((p: TicTacToePart) => p.player === 'O').map((p: TicTacToePart) => p.position); @@ -125,15 +121,13 @@ export function placePiece(host: Entity, row: number, col: numbe const moveNumber = host.value.parts.length + 1; const piece: TicTacToePart = { id: `piece-${player}-${moveNumber}`, - region: board, + regionId: 'board', position: [row, col], player, }; host.produce(state => { - const e = entity(piece.id, piece) - state.parts.push(e); - board.produce(draft => { - draft.children.push(e); - }); + state.parts.push(piece); + board.childIds.push(piece.id); + board.partMap[`${row},${col}`] = piece.id; }); } diff --git a/tests/core/region.test.ts b/tests/core/region.test.ts index 214e19e..6c246aa 100644 --- a/tests/core/region.test.ts +++ b/tests/core/region.test.ts @@ -1,135 +1,138 @@ import { describe, it, expect } from 'vitest'; -import { applyAlign, shuffle, moveToRegion, moveToRegionAll, removeFromRegion, type Region, type RegionAxis } from '@/core/region'; +import { createRegion, applyAlign, shuffle, moveToRegion, moveToRegionAll, removeFromRegion, type Region, type RegionAxis } from '@/core/region'; import { createRNG } from '@/utils/rng'; -import { entity, Entity } from '@/utils/entity'; import { type Part } from '@/core/part'; describe('Region', () => { - function createTestRegion(axes: RegionAxis[], parts: Part[]): Entity { - const partEntities = parts.map(p => entity(p.id, p)); - return entity('region1', { - id: 'region1', - axes: [...axes], - children: partEntities, - }); + function createTestRegion(axes: RegionAxis[], parts: Part[]): { region: Region; parts: Record } { + const partsMap: Record = {}; + for (const p of parts) { + partsMap[p.id] = { ...p }; + } + const region = createRegion('region1', axes); + region.childIds = parts.map(p => p.id); + region.partMap = Object.fromEntries( + parts.map(p => [p.position.join(','), p.id]) + ); + return { region, parts: partsMap }; } describe('applyAlign', () => { it('should do nothing with empty region', () => { - const region = createTestRegion([{ name: 'x', min: 0, align: 'start' }], []); - applyAlign(region); - expect(region.value.children).toHaveLength(0); + const { region } = createTestRegion([{ name: 'x', min: 0, align: 'start' }], []); + applyAlign(region, {}); + expect(region.childIds).toHaveLength(0); }); it('should align parts to start on first axis', () => { - const part1: Part = { id: 'p1', region: null as any, position: [5, 10] }; - const part2: Part = { id: 'p2', region: null as any, position: [7, 20] }; - const part3: Part = { id: 'p3', region: null as any, position: [2, 30] }; + const part1: Part = { id: 'p1', regionId: 'region1', position: [5, 10] }; + const part2: Part = { id: 'p2', regionId: 'region1', position: [7, 20] }; + const part3: Part = { id: 'p3', regionId: 'region1', position: [2, 30] }; - const region = createTestRegion( + const { region, parts } = createTestRegion( [{ name: 'x', min: 0, align: 'start' }, { name: 'y' }], [part1, part2, part3] ); - applyAlign(region); + applyAlign(region, parts); - expect(region.value.children[0].value.position[0]).toBe(0); - expect(region.value.children[1].value.position[0]).toBe(1); - expect(region.value.children[2].value.position[0]).toBe(2); - expect(region.value.children[0].value.position[1]).toBe(30); - expect(region.value.children[1].value.position[1]).toBe(10); - expect(region.value.children[2].value.position[1]).toBe(20); + expect(parts[region.childIds[0]].position[0]).toBe(0); + expect(parts[region.childIds[1]].position[0]).toBe(1); + expect(parts[region.childIds[2]].position[0]).toBe(2); + expect(parts[region.childIds[0]].position[1]).toBe(30); + expect(parts[region.childIds[1]].position[1]).toBe(10); + expect(parts[region.childIds[2]].position[1]).toBe(20); }); it('should align parts to start with custom min', () => { - const part1: Part = { id: 'p1', region: null as any, position: [5, 100] }; - const part2: Part = { id: 'p2', region: null as any, position: [7, 200] }; + const part1: Part = { id: 'p1', regionId: 'region1', position: [5, 100] }; + const part2: Part = { id: 'p2', regionId: 'region1', position: [7, 200] }; - const region = createTestRegion( + const { region, parts } = createTestRegion( [{ name: 'x', min: 10, align: 'start' }, { name: 'y' }], [part1, part2] ); - applyAlign(region); + applyAlign(region, parts); - expect(region.value.children[0].value.position[0]).toBe(10); - expect(region.value.children[1].value.position[0]).toBe(11); - expect(region.value.children[0].value.position[1]).toBe(100); - expect(region.value.children[1].value.position[1]).toBe(200); + expect(parts[region.childIds[0]].position[0]).toBe(10); + expect(parts[region.childIds[1]].position[0]).toBe(11); + expect(parts[region.childIds[0]].position[1]).toBe(100); + expect(parts[region.childIds[1]].position[1]).toBe(200); }); it('should align parts to end on first axis', () => { - const part1: Part = { id: 'p1', region: null as any, position: [2, 50] }; - const part2: Part = { id: 'p2', region: null as any, position: [4, 60] }; - const part3: Part = { id: 'p3', region: null as any, position: [1, 70] }; + const part1: Part = { id: 'p1', regionId: 'region1', position: [2, 50] }; + const part2: Part = { id: 'p2', regionId: 'region1', position: [4, 60] }; + const part3: Part = { id: 'p3', regionId: 'region1', position: [1, 70] }; - const region = createTestRegion( + const { region, parts } = createTestRegion( [{ name: 'x', max: 10, align: 'end' }, { name: 'y' }], [part1, part2, part3] ); - applyAlign(region); + applyAlign(region, parts); - expect(region.value.children[0].value.position[0]).toBe(8); - expect(region.value.children[1].value.position[0]).toBe(9); - expect(region.value.children[2].value.position[0]).toBe(10); + expect(parts[region.childIds[0]].position[0]).toBe(8); + expect(parts[region.childIds[1]].position[0]).toBe(9); + expect(parts[region.childIds[2]].position[0]).toBe(10); }); it('should align parts to center on first axis', () => { - const part1: Part = { id: 'p1', region: null as any, position: [0, 5] }; - const part2: Part = { id: 'p2', region: null as any, position: [1, 6] }; - const part3: Part = { id: 'p3', region: null as any, position: [2, 7] }; + const part1: Part = { id: 'p1', regionId: 'region1', position: [0, 5] }; + const part2: Part = { id: 'p2', regionId: 'region1', position: [1, 6] }; + const part3: Part = { id: 'p3', regionId: 'region1', position: [2, 7] }; - const region = createTestRegion( + const { region, parts } = createTestRegion( [{ name: 'x', min: 0, max: 10, align: 'center' }, { name: 'y' }], [part1, part2, part3] ); - applyAlign(region); + applyAlign(region, parts); - expect(region.value.children[0].value.position[0]).toBe(4); - expect(region.value.children[1].value.position[0]).toBe(5); - expect(region.value.children[2].value.position[0]).toBe(6); + expect(parts[region.childIds[0]].position[0]).toBe(4); + expect(parts[region.childIds[1]].position[0]).toBe(5); + expect(parts[region.childIds[2]].position[0]).toBe(6); }); it('should handle even count center alignment', () => { - const part1: Part = { id: 'p1', region: null as any, position: [0, 10] }; - const part2: Part = { id: 'p2', region: null as any, position: [1, 20] }; + const part1: Part = { id: 'p1', regionId: 'region1', position: [0, 10] }; + const part2: Part = { id: 'p2', regionId: 'region1', position: [1, 20] }; - const region = createTestRegion( + const { region, parts } = createTestRegion( [{ name: 'x', min: 0, max: 10, align: 'center' }, { name: 'y' }], [part1, part2] ); - applyAlign(region); + applyAlign(region, parts); - expect(region.value.children[0].value.position[0]).toBe(4.5); - expect(region.value.children[1].value.position[0]).toBe(5.5); + expect(parts[region.childIds[0]].position[0]).toBe(4.5); + expect(parts[region.childIds[1]].position[0]).toBe(5.5); }); it('should sort children by position on current axis', () => { - const part1: Part = { id: 'p1', region: null as any, position: [5, 100] }; - const part2: Part = { id: 'p2', region: null as any, position: [1, 200] }; - const part3: Part = { id: 'p3', region: null as any, position: [3, 300] }; + const part1: Part = { id: 'p1', regionId: 'region1', position: [5, 100] }; + const part2: Part = { id: 'p2', regionId: 'region1', position: [1, 200] }; + const part3: Part = { id: 'p3', regionId: 'region1', position: [3, 300] }; - const region = createTestRegion( + const { region, parts } = createTestRegion( [{ name: 'x', min: 0, align: 'start' }, { name: 'y' }], [part1, part2, part3] ); - applyAlign(region); + applyAlign(region, parts); - expect(region.value.children[0].value.id).toBe('p2'); - expect(region.value.children[1].value.id).toBe('p3'); - expect(region.value.children[2].value.id).toBe('p1'); + expect(region.childIds[0]).toBe('p2'); + expect(region.childIds[1]).toBe('p3'); + expect(region.childIds[2]).toBe('p1'); }); it('should align on multiple axes', () => { - const part1: Part = { id: 'p1', region: null as any, position: [5, 10] }; - const part2: Part = { id: 'p2', region: null as any, position: [7, 20] }; - const part3: Part = { id: 'p3', region: null as any, position: [2, 30] }; + const part1: Part = { id: 'p1', regionId: 'region1', position: [5, 10] }; + const part2: Part = { id: 'p2', regionId: 'region1', position: [7, 20] }; + const part3: Part = { id: 'p3', regionId: 'region1', position: [2, 30] }; - const region = createTestRegion( + const { region, parts } = createTestRegion( [ { name: 'x', min: 0, align: 'start' }, { name: 'y', min: 0, align: 'start' } @@ -137,30 +140,25 @@ describe('Region', () => { [part1, part2, part3] ); - applyAlign(region); + applyAlign(region, parts); - const positions = region.value.children.map(c => ({ - id: c.value.id, - position: c.value.position - })); + expect(region.childIds[0]).toBe('p3'); + expect(parts[region.childIds[0]].position).toEqual([0, 2]); - expect(positions[0].id).toBe('p3'); - expect(positions[0].position).toEqual([0, 2]); + expect(region.childIds[1]).toBe('p1'); + expect(parts[region.childIds[1]].position).toEqual([1, 0]); - expect(positions[1].id).toBe('p1'); - expect(positions[1].position).toEqual([1, 0]); - - expect(positions[2].id).toBe('p2'); - expect(positions[2].position).toEqual([2, 1]); + expect(region.childIds[2]).toBe('p2'); + expect(parts[region.childIds[2]].position).toEqual([2, 1]); }); it('should align 4 elements on rectangle corners', () => { - const part1: Part = { id: 'p1', region: null as any, position: [0, 0] }; - const part2: Part = { id: 'p2', region: null as any, position: [10, 0] }; - const part3: Part = { id: 'p3', region: null as any, position: [10, 1] }; - const part4: Part = { id: 'p4', region: null as any, position: [0, 1] }; + const part1: Part = { id: 'p1', regionId: 'region1', position: [0, 0] }; + const part2: Part = { id: 'p2', regionId: 'region1', position: [10, 0] }; + const part3: Part = { id: 'p3', regionId: 'region1', position: [10, 1] }; + const part4: Part = { id: 'p4', regionId: 'region1', position: [0, 1] }; - const region = createTestRegion( + const { region, parts } = createTestRegion( [ { name: 'x', min: 0, max: 10, align: 'start' }, { name: 'y', min: 0, max: 10, align: 'start' } @@ -168,55 +166,50 @@ describe('Region', () => { [part1, part2, part3, part4] ); - applyAlign(region); + applyAlign(region, parts); - const positions = region.value.children.map(c => ({ - id: c.value.id, - position: c.value.position - })); + expect(region.childIds[0]).toBe('p1'); + expect(parts[region.childIds[0]].position).toEqual([0, 0]); - expect(positions[0].id).toBe('p1'); - expect(positions[0].position).toEqual([0, 0]); + expect(region.childIds[1]).toBe('p4'); + expect(parts[region.childIds[1]].position).toEqual([0, 1]); - expect(positions[1].id).toBe('p4'); - expect(positions[1].position).toEqual([0, 1]); + expect(region.childIds[2]).toBe('p2'); + expect(parts[region.childIds[2]].position).toEqual([1, 0]); - expect(positions[2].id).toBe('p2'); - expect(positions[2].position).toEqual([1, 0]); - - expect(positions[3].id).toBe('p3'); - expect(positions[3].position).toEqual([1, 1]); + expect(region.childIds[3]).toBe('p3'); + expect(parts[region.childIds[3]].position).toEqual([1, 1]); }); }); describe('shuffle', () => { it('should do nothing with empty region', () => { - const region = createTestRegion([], []); + const { region, parts } = createTestRegion([], []); const rng = createRNG(42); - shuffle(region, rng); - expect(region.value.children).toHaveLength(0); + shuffle(region, parts, rng); + expect(region.childIds).toHaveLength(0); }); it('should do nothing with single part', () => { - const part: Part = { id: 'p1', region: null as any, position: [0, 0, 0] }; - const region = createTestRegion([], [part]); + const part: Part = { id: 'p1', regionId: 'region1', position: [0, 0, 0] }; + const { region, parts } = createTestRegion([], [part]); const rng = createRNG(42); - shuffle(region, rng); - expect(region.value.children[0].value.position).toEqual([0, 0, 0]); + shuffle(region, parts, rng); + expect(parts['p1'].position).toEqual([0, 0, 0]); }); it('should shuffle positions of multiple parts', () => { - const part1: Part = { id: 'p1', region: null as any, position: [0, 100] }; - const part2: Part = { id: 'p2', region: null as any, position: [1, 200] }; - const part3: Part = { id: 'p3', region: null as any, position: [2, 300] }; + const part1: Part = { id: 'p1', regionId: 'region1', position: [0, 100] }; + const part2: Part = { id: 'p2', regionId: 'region1', position: [1, 200] }; + const part3: Part = { id: 'p3', regionId: 'region1', position: [2, 300] }; - const region = createTestRegion([], [part1, part2, part3]); + const { region, parts } = createTestRegion([], [part1, part2, part3]); const rng = createRNG(42); - const originalPositions = region.value.children.map(c => [...c.value.position]); - shuffle(region, rng); + const originalPositions = region.childIds.map(id => [...parts[id].position]); + shuffle(region, parts, rng); - const newPositions = region.value.children.map(c => c.value.position); + const newPositions = region.childIds.map(id => parts[id].position); originalPositions.forEach(origPos => { const found = newPositions.some(newPos => @@ -227,46 +220,44 @@ describe('Region', () => { }); it('should be deterministic with same seed', () => { - const createRegionForTest = () => { - const part1: Part = { id: 'p1', region: null as any, position: [0, 10] }; - const part2: Part = { id: 'p2', region: null as any, position: [1, 20] }; - const part3: Part = { id: 'p3', region: null as any, position: [2, 30] }; - return createTestRegion([], [part1, part2, part3]); - }; + const createPartsForTest = (): Part[] => [ + { id: 'p1', regionId: 'region1', position: [0, 10] }, + { id: 'p2', regionId: 'region1', position: [1, 20] }, + { id: 'p3', regionId: 'region1', position: [2, 30] }, + ]; - const setup1 = createRegionForTest(); - const setup2 = createRegionForTest(); + const setup1 = createTestRegion([], createPartsForTest()); + const setup2 = createTestRegion([], createPartsForTest()); const rng1 = createRNG(42); const rng2 = createRNG(42); - shuffle(setup1, rng1); - shuffle(setup2, rng2); + shuffle(setup1.region, setup1.parts, rng1); + shuffle(setup2.region, setup2.parts, rng2); - const positions1 = setup1.value.children.map(c => c.value.position); - const positions2 = setup2.value.children.map(c => c.value.position); + const positions1 = setup1.region.childIds.map(id => setup1.parts[id].position); + const positions2 = setup2.region.childIds.map(id => setup2.parts[id].position); expect(positions1).toEqual(positions2); }); it('should produce different results with different seeds', () => { - const createRegionForTest = () => { - const part1: Part = { id: 'p1', region: null as any, position: [0, 10] }; - const part2: Part = { id: 'p2', region: null as any, position: [1, 20] }; - const part3: Part = { id: 'p3', region: null as any, position: [2, 30] }; - const part4: Part = { id: 'p4', region: null as any, position: [3, 40] }; - const part5: Part = { id: 'p5', region: null as any, position: [4, 50] }; - return createTestRegion([], [part1, part2, part3, part4, part5]); - }; + const createPartsForTest = (): Part[] => [ + { id: 'p1', regionId: 'region1', position: [0, 10] }, + { id: 'p2', regionId: 'region1', position: [1, 20] }, + { id: 'p3', regionId: 'region1', position: [2, 30] }, + { id: 'p4', regionId: 'region1', position: [3, 40] }, + { id: 'p5', regionId: 'region1', position: [4, 50] }, + ]; const results = new Set(); for (let seed = 1; seed <= 10; seed++) { - const setup = createRegionForTest(); + const setup = createTestRegion([], createPartsForTest()); const rng = createRNG(seed); - shuffle(setup, rng); + shuffle(setup.region, setup.parts, rng); - const positions = JSON.stringify(setup.value.children.map(c => c.value.position)); + const positions = JSON.stringify(setup.region.childIds.map(id => setup.parts[id].position)); results.add(positions); } @@ -278,104 +269,110 @@ describe('Region', () => { it('should move a part from one region to another', () => { const sourceAxes: RegionAxis[] = [{ name: 'x', min: 0, max: 5 }]; const targetAxes: RegionAxis[] = [{ name: 'x', min: 0, max: 5 }]; - const sourceRegion = createTestRegion(sourceAxes, []); - const targetRegion = createTestRegion(targetAxes, []); + const sourceRegion = createRegion('source', sourceAxes); + const targetRegion = createRegion('target', targetAxes); - const part: Part = { id: 'p1', region: sourceRegion, position: [2] }; - const partEntity = entity(part.id, part); - sourceRegion.value.children.push(partEntity); + const part: Part = { id: 'p1', regionId: 'source', position: [2] }; + const parts: Record = { p1: part }; + sourceRegion.childIds.push('p1'); + sourceRegion.partMap['2'] = 'p1'; - expect(sourceRegion.value.children).toHaveLength(1); - expect(targetRegion.value.children).toHaveLength(0); - expect(partEntity.value.region.value.id).toBe('region1'); + expect(sourceRegion.childIds).toHaveLength(1); + expect(targetRegion.childIds).toHaveLength(0); + expect(part.regionId).toBe('source'); - moveToRegion(partEntity, targetRegion, [0]); + moveToRegion(part, sourceRegion, targetRegion, [0]); - expect(sourceRegion.value.children).toHaveLength(0); - expect(targetRegion.value.children).toHaveLength(1); - expect(partEntity.value.region.value.id).toBe('region1'); - expect(partEntity.value.position).toEqual([0]); + expect(sourceRegion.childIds).toHaveLength(0); + expect(targetRegion.childIds).toHaveLength(1); + expect(part.regionId).toBe('target'); + expect(part.position).toEqual([0]); }); it('should keep existing position if no position provided', () => { - const sourceRegion = createTestRegion([{ name: 'x' }], []); - const targetRegion = createTestRegion([{ name: 'x' }], []); + const sourceRegion = createRegion('source', [{ name: 'x' }]); + const targetRegion = createRegion('target', [{ name: 'x' }]); - const part: Part = { id: 'p1', region: sourceRegion, position: [3] }; - const partEntity = entity(part.id, part); - sourceRegion.value.children.push(partEntity); + const part: Part = { id: 'p1', regionId: 'source', position: [3] }; + const parts: Record = { p1: part }; + sourceRegion.childIds.push('p1'); + sourceRegion.partMap['3'] = 'p1'; - moveToRegion(partEntity, targetRegion); + moveToRegion(part, sourceRegion, targetRegion); - expect(partEntity.value.position).toEqual([3]); + expect(part.position).toEqual([3]); }); }); describe('moveToRegionAll', () => { it('should move multiple parts to a target region', () => { - const sourceRegion = createTestRegion([{ name: 'x' }], []); - const targetRegion = createTestRegion([{ name: 'x' }], []); + const sourceRegion = createRegion('source', [{ name: 'x' }]); + const targetRegion = createRegion('target', [{ name: 'x' }]); - const parts = [ - entity('p1', { id: 'p1', region: sourceRegion, position: [0] } as Part), - entity('p2', { id: 'p2', region: sourceRegion, position: [1] } as Part), - entity('p3', { id: 'p3', region: sourceRegion, position: [2] } as Part), - ]; - sourceRegion.value.children.push(...parts); + const parts = { + p1: { id: 'p1', regionId: 'source', position: [0] } as Part, + p2: { id: 'p2', regionId: 'source', position: [1] } as Part, + p3: { id: 'p3', regionId: 'source', position: [2] } as Part, + }; + sourceRegion.childIds.push('p1', 'p2', 'p3'); + sourceRegion.partMap = { '0': 'p1', '1': 'p2', '2': 'p3' }; - moveToRegionAll(parts, targetRegion, [[0], [1], [2]]); + moveToRegionAll([parts.p1, parts.p2, parts.p3], sourceRegion, targetRegion, [[0], [1], [2]]); - expect(sourceRegion.value.children).toHaveLength(0); - expect(targetRegion.value.children).toHaveLength(3); - expect(parts[0].value.position).toEqual([0]); - expect(parts[1].value.position).toEqual([1]); - expect(parts[2].value.position).toEqual([2]); + expect(sourceRegion.childIds).toHaveLength(0); + expect(targetRegion.childIds).toHaveLength(3); + expect(parts.p1.position).toEqual([0]); + expect(parts.p2.position).toEqual([1]); + expect(parts.p3.position).toEqual([2]); }); it('should keep existing positions if no positions provided', () => { - const sourceRegion = createTestRegion([{ name: 'x' }], []); - const targetRegion = createTestRegion([{ name: 'x' }], []); + const sourceRegion = createRegion('source', [{ name: 'x' }]); + const targetRegion = createRegion('target', [{ name: 'x' }]); - const parts = [ - entity('p1', { id: 'p1', region: sourceRegion, position: [5] } as Part), - entity('p2', { id: 'p2', region: sourceRegion, position: [8] } as Part), - ]; - sourceRegion.value.children.push(...parts); + const parts = { + p1: { id: 'p1', regionId: 'source', position: [5] } as Part, + p2: { id: 'p2', regionId: 'source', position: [8] } as Part, + }; + sourceRegion.childIds.push('p1', 'p2'); + sourceRegion.partMap = { '5': 'p1', '8': 'p2' }; - moveToRegionAll(parts, targetRegion); + moveToRegionAll([parts.p1, parts.p2], sourceRegion, targetRegion); - expect(parts[0].value.position).toEqual([5]); - expect(parts[1].value.position).toEqual([8]); + expect(parts.p1.position).toEqual([5]); + expect(parts.p2.position).toEqual([8]); }); }); describe('removeFromRegion', () => { it('should remove a part from its region', () => { - const region = createTestRegion([{ name: 'x' }], []); + const region = createRegion('region1', [{ name: 'x' }]); - const part: Part = { id: 'p1', region: region, position: [2] }; - const partEntity = entity(part.id, part); - region.value.children.push(partEntity); + const part: Part = { id: 'p1', regionId: 'region1', position: [2] }; + const parts: Record = { p1: part }; + region.childIds.push('p1'); + region.partMap['2'] = 'p1'; - expect(region.value.children).toHaveLength(1); + expect(region.childIds).toHaveLength(1); - removeFromRegion(partEntity); + removeFromRegion(part, region); - expect(region.value.children).toHaveLength(0); + expect(region.childIds).toHaveLength(0); }); it('should leave other parts unaffected', () => { - const region = createTestRegion([{ name: 'x' }], []); + const region = createRegion('region1', [{ name: 'x' }]); - const p1 = entity('p1', { id: 'p1', region: region, position: [0] } as Part); - const p2 = entity('p2', { id: 'p2', region: region, position: [1] } as Part); - const p3 = entity('p3', { id: 'p3', region: region, position: [2] } as Part); - region.value.children.push(p1, p2, p3); + const p1 = { id: 'p1', regionId: 'region1', position: [0] } as Part; + const p2 = { id: 'p2', regionId: 'region1', position: [1] } as Part; + const p3 = { id: 'p3', regionId: 'region1', position: [2] } as Part; + region.childIds.push('p1', 'p2', 'p3'); + region.partMap = { '0': 'p1', '1': 'p2', '2': 'p3' }; - removeFromRegion(p2); + removeFromRegion(p2, region); - expect(region.value.children).toHaveLength(2); - expect(region.value.children.map(c => c.value.id)).toEqual(['p1', 'p3']); + expect(region.childIds).toHaveLength(2); + expect(region.childIds).toEqual(['p1', 'p3']); }); }); }); diff --git a/tests/samples/boop.test.ts b/tests/samples/boop.test.ts index f2bf9fb..61a1175 100644 --- a/tests/samples/boop.test.ts +++ b/tests/samples/boop.test.ts @@ -36,7 +36,7 @@ function waitForPrompt(ctx: ReturnType['ctx']): Promis } function getParts(state: Entity) { - return state.value.board.value.children; + return state.value.pieces; } describe('Boop - helper functions', () => { @@ -81,8 +81,8 @@ describe('Boop - helper functions', () => { const part = getPartAt(state, 2, 2); expect(part).not.toBeNull(); if (part) { - expect(part.value.player).toBe('black'); - expect(part.value.pieceType).toBe('kitten'); + expect(part.player).toBe('black'); + expect(part.pieceType).toBe('kitten'); } }); }); @@ -95,9 +95,9 @@ describe('Boop - helper functions', () => { const parts = getParts(state); expect(parts.length).toBe(1); - expect(parts[0].value.position).toEqual([2, 3]); - expect(parts[0].value.player).toBe('white'); - expect(parts[0].value.pieceType).toBe('kitten'); + expect(parts[0].position).toEqual([2, 3]); + expect(parts[0].player).toBe('white'); + expect(parts[0].pieceType).toBe('kitten'); }); it('should name piece white-kitten-1', () => { @@ -130,23 +130,23 @@ describe('Boop - helper functions', () => { const state = getState(ctx); placePiece(state, 0, 0, 'white', 'kitten'); - expect(state.value.players.white.value.kitten.supply).toBe(7); - expect(state.value.players.black.value.kitten.supply).toBe(8); + expect(state.value.players.white.kitten.supply).toBe(7); + expect(state.value.players.black.kitten.supply).toBe(8); placePiece(state, 0, 1, 'black', 'kitten'); - expect(state.value.players.white.value.kitten.supply).toBe(7); - expect(state.value.players.black.value.kitten.supply).toBe(7); + expect(state.value.players.white.kitten.supply).toBe(7); + expect(state.value.players.black.kitten.supply).toBe(7); }); it('should decrement the correct player cat supply', () => { const { ctx } = createTestContext(); const state = getState(ctx); state.produce(s => { - s.players.white.value.cat.supply = 3; + s.players.white.cat.supply = 3; }); placePiece(state, 0, 0, 'white', 'cat'); - expect(state.value.players.white.value.cat.supply).toBe(2); + expect(state.value.players.white.cat.supply).toBe(2); }); it('should add piece to board region children', () => { @@ -155,7 +155,7 @@ describe('Boop - helper functions', () => { placePiece(state, 1, 1, 'white', 'kitten'); const board = getBoardRegion(state); - expect(board.value.children.length).toBe(1); + expect(board.childIds.length).toBe(1); }); it('should generate unique IDs for pieces', () => { @@ -178,11 +178,11 @@ describe('Boop - helper functions', () => { placePiece(state, 2, 2, 'white', 'kitten'); const whitePart = getParts(state)[1]; - expect(whitePart.value.position).toEqual([2, 2]); + expect(whitePart.position).toEqual([2, 2]); applyBoops(state, 3, 3, 'kitten'); - expect(whitePart.value.position).toEqual([1, 1]); + expect(whitePart.position).toEqual([1, 1]); }); it('should not boop a cat when a kitten is placed', () => { @@ -191,13 +191,11 @@ describe('Boop - helper functions', () => { placePiece(state, 3, 3, 'black', 'kitten'); const whitePart = getParts(state)[0]; - whitePart.produce(p => { - p.pieceType = 'cat'; - }); + whitePart.pieceType = 'cat'; applyBoops(state, 3, 3, 'kitten'); - expect(whitePart.value.position).toEqual([3, 3]); + expect(whitePart.position).toEqual([3, 3]); }); it('should remove piece that is booped off the board', () => { @@ -210,8 +208,8 @@ describe('Boop - helper functions', () => { applyBoops(state, 1, 1, 'kitten'); expect(getParts(state).length).toBe(1); - expect(getParts(state)[0].value.player).toBe('black'); - expect(state.value.players.white.value.kitten.supply).toBe(8); + expect(getParts(state)[0].player).toBe('black'); + expect(state.value.players.white.kitten.supply).toBe(8); }); it('should not boop piece if target cell is occupied', () => { @@ -224,10 +222,10 @@ describe('Boop - helper functions', () => { applyBoops(state, 0, 1, 'kitten'); - const whitePart = getParts(state).find(p => p.value.player === 'white'); + const whitePart = getParts(state).find(p => p.player === 'white'); expect(whitePart).toBeDefined(); if (whitePart) { - expect(whitePart.value.position).toEqual([1, 1]); + expect(whitePart.position).toEqual([1, 1]); } }); @@ -241,8 +239,8 @@ describe('Boop - helper functions', () => { applyBoops(state, 3, 3, 'kitten'); - expect(getParts(state)[1].value.position).toEqual([1, 1]); - expect(getParts(state)[2].value.position).toEqual([1, 3]); + expect(getParts(state)[1].position).toEqual([1, 1]); + expect(getParts(state)[2].position).toEqual([1, 3]); }); it('should not boop the placed piece itself', () => { @@ -253,7 +251,7 @@ describe('Boop - helper functions', () => { applyBoops(state, 3, 3, 'kitten'); - expect(getParts(state)[0].value.position).toEqual([3, 3]); + expect(getParts(state)[0].position).toEqual([3, 3]); }); }); @@ -267,7 +265,7 @@ describe('Boop - helper functions', () => { removePieceFromBoard(state, part); const board = getBoardRegion(state); - expect(board.value.children.length).toBe(0); + expect(board.childIds.length).toBe(0); }); }); @@ -343,9 +341,7 @@ describe('Boop - helper functions', () => { placePiece(state, 0, 1, 'white', 'kitten'); placePiece(state, 0, 2, 'white', 'kitten'); - getParts(state)[1].produce(p => { - p.pieceType = 'cat'; - }); + getParts(state)[1].pieceType = 'cat'; const lines = checkGraduation(state, 'white'); expect(lines.length).toBe(0); @@ -367,7 +363,7 @@ describe('Boop - helper functions', () => { processGraduation(state, 'white', lines); expect(getParts(state).length).toBe(0); - expect(state.value.players.white.value.cat.supply).toBe(3); + expect(state.value.players.white.cat.supply).toBe(3); }); it('should only graduate pieces on the winning lines', () => { @@ -383,8 +379,8 @@ describe('Boop - helper functions', () => { processGraduation(state, 'white', lines); expect(getParts(state).length).toBe(1); - expect(getParts(state)[0].value.position).toEqual([3, 3]); - expect(state.value.players.white.value.cat.supply).toBe(3); + expect(getParts(state)[0].position).toEqual([3, 3]); + expect(state.value.players.white.cat.supply).toBe(3); }); }); @@ -487,7 +483,7 @@ describe('Boop - game flow', () => { expect(result.success).toBe(true); if (result.success) expect(result.result.winner).toBeNull(); expect(getParts(ctx.state).length).toBe(1); - expect(getParts(ctx.state)[0].value.position).toEqual([2, 2]); + expect(getParts(ctx.state)[0].position).toEqual([2, 2]); expect(getParts(ctx.state)[0].id).toBe('white-kitten-1'); }); @@ -537,7 +533,9 @@ describe('Boop - game flow', () => { const { ctx } = createTestContext(); const state = getState(ctx); - state.value.players.white.value.kitten.supply = 0; + state.produce(s => { + s.players.white.kitten.supply = 0; + }); const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white'); @@ -575,10 +573,10 @@ describe('Boop - game flow', () => { expect(result.success).toBe(true); expect(getParts(state).length).toBe(2); - const whitePart = getParts(state).find(p => p.value.player === 'white'); + const whitePart = getParts(state).find(p => p.player === 'white'); expect(whitePart).toBeDefined(); if (whitePart) { - expect(whitePart.value.position).not.toEqual([3, 3]); + expect(whitePart.position).not.toEqual([3, 3]); } }); @@ -596,14 +594,16 @@ describe('Boop - game flow', () => { processGraduation(state, 'white', lines); expect(getParts(state).length).toBe(0); - expect(state.value.players.white.value.cat.supply).toBe(3); + expect(state.value.players.white.cat.supply).toBe(3); }); it('should accept placing a cat via play command', async () => { const { ctx } = createTestContext(); const state = getState(ctx); - state.value.players.white.value.cat.supply = 3; + state.produce(s => { + s.players.white.cat.supply = 3; + }); const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white'); @@ -616,15 +616,17 @@ describe('Boop - game flow', () => { expect(result.success).toBe(true); expect(getParts(state).length).toBe(1); expect(getParts(state)[0].id).toBe('white-cat-1'); - expect(getParts(state)[0].value.pieceType).toBe('cat'); - expect(state.value.players.white.value.cat.supply).toBe(2); + expect(getParts(state)[0].pieceType).toBe('cat'); + expect(state.value.players.white.cat.supply).toBe(2); }); it('should reject placing a cat when supply is empty', async () => { const { ctx } = createTestContext(); const state = getState(ctx); - state.value.players.white.value.cat.supply = 0; + state.produce(s => { + s.players.white.cat.supply = 0; + }); const promptPromise = waitForPrompt(ctx); const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white'); diff --git a/tests/samples/tic-tac-toe.test.ts b/tests/samples/tic-tac-toe.test.ts index 03a89a1..150ea0e 100644 --- a/tests/samples/tic-tac-toe.test.ts +++ b/tests/samples/tic-tac-toe.test.ts @@ -164,8 +164,8 @@ describe('TicTacToe - helper functions', () => { placePiece(state, 1, 1, 'X'); expect(state.value.parts.length).toBe(1); - expect(state.value.parts[0].value.position).toEqual([1, 1]); - expect(state.value.parts[0].value.player).toBe('X'); + expect(state.value.parts[0].position).toEqual([1, 1]); + expect(state.value.parts[0].player).toBe('X'); }); it('should add piece to board region children', () => { @@ -174,7 +174,7 @@ describe('TicTacToe - helper functions', () => { placePiece(state, 0, 0, 'O'); const board = state.value.board; - expect(board.value.children.length).toBe(1); + expect(board.childIds.length).toBe(1); }); it('should generate unique IDs for pieces', () => { @@ -230,7 +230,7 @@ describe('TicTacToe - game flow', () => { expect(result.success).toBe(true); if (result.success) expect(result.result.winner).toBeNull(); expect(ctx.state.value.parts.length).toBe(1); - expect(ctx.state.value.parts[0].value.position).toEqual([1, 1]); + expect(ctx.state.value.parts[0].position).toEqual([1, 1]); }); it('should reject move for wrong player and re-prompt', async () => {