Compare commits

...

2 Commits

Author SHA1 Message Date
hypercross 98a12b9266 refactor: externalize imports 2026-04-07 16:09:47 +08:00
hypercross 91c993b223 refactor: clean up boop implementation structure 2026-04-07 15:43:17 +08:00
19 changed files with 513 additions and 323 deletions

View File

@ -1,259 +0,0 @@
import {
BOARD_SIZE,
BoopState,
PieceType,
PlayerType,
WinnerType,
WIN_LENGTH,
MAX_PIECES_PER_PLAYER,
BoopGame,
prompts
} from "./data";
import {createGameCommandRegistry} from "@/core/game";
import {moveToRegion} from "@/core/region";
import {
findPartAtPosition,
findPartInRegion,
getLineCandidates,
getNeighborPositions,
isCellOccupied,
isInBounds
} from "./utils";
export const registry = createGameCommandRegistry<BoopState>();
/**
*
*/
async function handlePlace(game: BoopGame, row: number, col: number, player: PlayerType, type: PieceType) {
const value = game.value;
// 从玩家supply中找到对应类型的棋子
const part = findPartInRegion(game, player, type);
if (!part) {
throw new Error(`No ${type} available in ${player}'s supply`);
}
const partId = part.id;
await game.produceAsync(state => {
// 将棋子从supply移动到棋盘
const part = state.pieces[partId];
moveToRegion(part, state.regions[player], state.regions.board, [row, col]);
});
return { row, col, player, type, partId };
}
const place = registry.register({
schema: 'place <row:number> <col:number> <player> <type>',
run: handlePlace
});
/**
* boop -
*/
async function handleBoop(game: BoopGame, row: number, col: number, type: PieceType) {
const booped: string[] = [];
await game.produceAsync(state => {
// 按照远离放置位置的方向推动
for (const [dr, dc] of getNeighborPositions()) {
const nr = row + dr;
const nc = col + dc;
if (!isInBounds(nr, nc)) continue;
// 从 state 中查找,而不是 game
const part = findPartAtPosition(state, nr, nc);
if (!part) continue;
// 小猫不能推动猫
if (type === 'kitten' && part.type === 'cat') continue;
// 计算推动后的位置
const newRow = nr + dr;
const newCol = nc + dc;
// 检查新位置是否为空或在棋盘外
if (!isInBounds(newRow, newCol)) {
// 棋子被推出棋盘,返回玩家supply
booped.push(part.id);
moveToRegion(part, state.regions.board, state.regions[part.player]);
} else if (!isCellOccupied(state, newRow, newCol)) {
// 新位置为空,移动过去
booped.push(part.id);
moveToRegion(part, state.regions.board, state.regions.board, [newRow, newCol]);
}
// 如果新位置被占用,则不移动(两个棋子都保持原位)
}
});
return { booped };
}
const boop = registry.register({
schema: 'boop <row:number> <col:number> <type>',
run: handleBoop
});
/**
* (线)
*/
async function handleCheckWin(game: BoopGame): Promise<WinnerType | null> {
for(const line of getLineCandidates()){
let whites = 0;
let blacks = 0;
for(const [row, col] of line){
const part = findPartAtPosition(game, row, col);
if(part?.type !== 'cat') continue;
if (part.player === 'white') whites++;
else blacks++;
}
if(whites >= WIN_LENGTH) {
return 'white';
}
if(blacks >= WIN_LENGTH) {
return 'black';
}
}
return null;
}
const checkWin = registry.register({
schema: 'check-win',
run: handleCheckWin
});
/**
* (线)
*/
async function handleCheckGraduates(game: BoopGame){
const toUpgrade = new Set<string>();
for(const line of getLineCandidates()){
let whites = 0;
let blacks = 0;
for(const [row, col] of line){
const part = findPartAtPosition(game, row, col);
if (part?.player === 'white') whites++;
else if(part?.player === 'black') blacks++;
}
const player = whites >= WIN_LENGTH ? 'white' : blacks >= WIN_LENGTH ? 'black' : null;
if(!player) continue;
for(const [row, col] of line){
const part = findPartAtPosition(game, row, col);
part && toUpgrade.add(part.id);
}
}
await game.produceAsync(state => {
for(const partId of toUpgrade){
const part = state.pieces[partId];
const [row, col] = part.position;
const player = part.player;
moveToRegion(part, state.regions.board, null);
const newPart = findPartInRegion(state, '', 'cat', player);
moveToRegion(newPart || part, null, state.regions[player], [row, col]);
}
});
}
const checkGraduates = registry.register({
schema: 'check-graduates',
run: handleCheckGraduates
});
export async function start(game: BoopGame) {
while (true) {
const currentPlayer = game.value.currentPlayer;
const turnOutput = await turn(game, currentPlayer);
await game.produceAsync(state => {
state.winner = turnOutput.winner;
if (!state.winner) {
state.currentPlayer = state.currentPlayer === 'white' ? 'black' : 'white';
}
});
if (game.value.winner) break;
}
return game.value;
}
async function handleCheckFullBoard(game: BoopGame, turnPlayer: PlayerType){
// 检查8-piece规则: 如果玩家所有8个棋子都在棋盘上且没有获胜,强制升级一个小猫
const playerPieces = Object.values(game.value.pieces).filter(
p => p.player === turnPlayer && p.regionId === 'board'
);
if(playerPieces.length < MAX_PIECES_PER_PLAYER || game.value.winner !== null){
return;
}
const partId = await game.prompt(
prompts.choose,
(player, row, col) => {
if (player !== turnPlayer) {
throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
}
if (!isInBounds(row, col)) {
throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`;
}
const part = findPartAtPosition(game, row, col);
if (!part || part.player !== turnPlayer) {
throw `No ${player} piece at (${row}, ${col}).`;
}
return part.id;
}
);
await game.produceAsync(state => {
const part = state.pieces[partId];
moveToRegion(part, state.regions.board, null);
const cat = findPartInRegion(state, '', 'cat');
moveToRegion(cat || part, null, state.regions[turnPlayer]);
});
}
const checkFullBoard = registry.register({
schema: 'check-full-board <player:string>',
run: handleCheckFullBoard
});
async function handleTurn(game: BoopGame, turnPlayer: PlayerType) {
const {row, col, type} = await game.prompt(
prompts.play,
(player, row, col, type) => {
const pieceType = type === 'cat' ? 'cat' : 'kitten';
if (player !== turnPlayer) {
throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
}
if (!isInBounds(row, col)) {
throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`;
}
if (isCellOccupied(game, row, col)) {
throw `Cell (${row}, ${col}) is already occupied.`;
}
const found = findPartInRegion(game, player, pieceType);
if (!found) {
throw `No ${pieceType}s left in ${player}'s supply.`;
}
return {player, row,col,type};
},
game.value.currentPlayer
);
const pieceType = type === 'cat' ? 'cat' : 'kitten';
await place(game, row, col, turnPlayer, pieceType);
await boop(game, row, col, pieceType);
const winner = await checkWin(game);
if(winner) return { winner: winner };
await checkGraduates(game);
await checkFullBoard(game, turnPlayer);
return { winner: null };
}
const turn = registry.register({
schema: 'turn <player:string>',
run: handleTurn
});

View File

@ -0,0 +1,46 @@
import {BoopGame} from "@/samples/boop/types-extensions";
import {PieceType} from "@/samples/boop/types";
import {findPartAtPosition, getNeighborPositions, isCellOccupied, isInBounds} from "@/samples/boop/utils";
import {moveToRegion} from "@/core/region";
/**
* boop -
*/
export async function boop(game: BoopGame, row: number, col: number, type: PieceType) {
const booped: string[] = [];
await game.produceAsync(state => {
// 按照远离放置位置的方向推动
for (const [dr, dc] of getNeighborPositions()) {
const nr = row + dr;
const nc = col + dc;
if (!isInBounds(nr, nc)) continue;
// 从 state 中查找,而不是 game
const part = findPartAtPosition(state, nr, nc);
if (!part) continue;
// 小猫不能推动猫
if (type === 'kitten' && part.type === 'cat') continue;
// 计算推动后的位置
const newRow = nr + dr;
const newCol = nc + dc;
// 检查新位置是否为空或在棋盘外
if (!isInBounds(newRow, newCol)) {
// 棋子被推出棋盘,返回玩家supply
booped.push(part.id);
moveToRegion(part, state.regions.board, state.regions[part.player]);
} else if (!isCellOccupied(state, newRow, newCol)) {
// 新位置为空,移动过去
booped.push(part.id);
moveToRegion(part, state.regions.board, state.regions.board, [newRow, newCol]);
}
// 如果新位置被占用,则不移动(两个棋子都保持原位)
}
});
return { booped };
}

View File

@ -0,0 +1,45 @@
import {BoopGame} from "@/samples/boop/types-extensions";
import {PlayerType} from "@/samples/boop/types";
import {findPartAtPosition, findPartInRegion, isInBounds} from "@/samples/boop/utils";
import {MAX_PIECES_PER_PLAYER} from "@/samples/boop/constants";
import {prompts} from "@/samples/boop/prompts";
import {moveToRegion} from "@/core/region";
/**
* 8-piece
*/
export async function checkFullBoard(game: BoopGame, turnPlayer: PlayerType){
// 检查8-piece规则: 如果玩家所有8个棋子都在棋盘上且没有获胜,强制升级一个小猫
const playerPieces = Object.values(game.value.pieces).filter(
p => p.player === turnPlayer && p.regionId === 'board'
);
if(playerPieces.length < MAX_PIECES_PER_PLAYER || game.value.winner !== null){
return;
}
const partId = await game.prompt(
prompts.choose,
(player, row, col) => {
if (player !== turnPlayer) {
throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
}
if (!isInBounds(row, col)) {
throw `Invalid position: (${row}, ${col}). Must be between 0 and 5.`;
}
const part = findPartAtPosition(game, row, col);
if (!part || part.player !== turnPlayer) {
throw `No ${player} piece at (${row}, ${col}).`;
}
return part.id;
}
);
await game.produceAsync(state => {
const part = state.pieces[partId];
moveToRegion(part, state.regions.board, null);
const cat = findPartInRegion(state, '', 'cat');
moveToRegion(cat || part, null, state.regions[turnPlayer]);
});
}

View File

@ -0,0 +1,40 @@
import {BoopGame} from "@/samples/boop/types-extensions";
import {PlayerType} from "@/samples/boop/types";
import {findPartAtPosition, findPartInRegion, getLineCandidates} from "@/samples/boop/utils";
import {WIN_LENGTH} from "@/samples/boop/constants";
import {moveToRegion} from "@/core/region";
/**
* (线)
*/
export async function checkGraduates(game: BoopGame){
const toUpgrade = new Set<string>();
for(const line of getLineCandidates()){
let whites = 0;
let blacks = 0;
for(const [row, col] of line){
const part = findPartAtPosition(game, row, col);
if (part?.player === 'white') whites++;
else if(part?.player === 'black') blacks++;
}
const player = whites >= WIN_LENGTH ? 'white' : blacks >= WIN_LENGTH ? 'black' : null;
if(!player) continue;
for(const [row, col] of line){
const part = findPartAtPosition(game, row, col);
part && toUpgrade.add(part.id);
}
}
await game.produceAsync(state => {
for(const partId of toUpgrade){
const part = state.pieces[partId];
const [row, col] = part.position;
const player = part.player;
moveToRegion(part, state.regions.board, null);
const newPart = findPartInRegion(state, '', 'cat', player);
moveToRegion(newPart || part, null, state.regions[player], [row, col]);
}
});
}

View File

@ -0,0 +1,101 @@
import {BoopGame} from "@/samples/boop/types-extensions";
import {PlayerType} from "@/samples/boop/types";
import {turn} from "@/samples/boop/commands/turn";
import {createGameCommandRegistry} from "@/core/game";
export const registry = createGameCommandRegistry<BoopGame['value']>();
/**
*
*/
const placeCmd = registry.register({
schema: 'place <row:number> <col:number> <player> <type>',
run: async (game, row, col, player, type) => {
const {place} = await import('./place');
return place(game, row, col, player, type);
}
});
/**
* boop -
*/
const boopCmd = registry.register({
schema: 'boop <row:number> <col:number> <type>',
run: async (game, row, col, type) => {
const {boop} = await import('./boop');
return boop(game, row, col, type);
}
});
/**
* (线)
*/
const checkWinCmd = registry.register({
schema: 'check-win',
run: async (game) => {
const {checkWin} = await import('./win');
return checkWin(game);
}
});
/**
* (线)
*/
const checkGraduatesCmd = registry.register({
schema: 'check-graduates',
run: async (game) => {
const {checkGraduates} = await import('./graduate');
return checkGraduates(game);
}
});
/**
* 8-piece
*/
const checkFullBoardCmd = registry.register({
schema: 'check-full-board <player:string>',
run: async (game, player) => {
const {checkFullBoard} = await import('./full-board');
return checkFullBoard(game, player);
}
});
/**
*
*/
const turnCmd = registry.register({
schema: 'turn <player:string>',
run: async (game, player) => {
const {turn} = await import('./turn');
return turn(game, player);
}
});
/**
*
*/
export async function start(game: BoopGame) {
while (true) {
const currentPlayer = game.value.currentPlayer;
const turnOutput = await turn(game, currentPlayer);
await game.produceAsync(state => {
state.winner = turnOutput.winner;
if (!state.winner) {
state.currentPlayer = state.currentPlayer === 'white' ? 'black' : 'white';
}
});
if (game.value.winner) break;
}
return game.value;
}
export {
placeCmd as place,
boopCmd as boop,
checkWinCmd as checkWin,
checkGraduatesCmd as checkGraduates,
checkFullBoardCmd as checkFullBoard,
turnCmd as turn
};

View File

@ -0,0 +1,27 @@
import {BoopGame} from "@/samples/boop/types-extensions";
import {PieceType, PlayerType} from "@/samples/boop/types";
import {findPartInRegion} from "@/samples/boop/utils";
import {moveToRegion} from "@/core/region";
/**
*
*/
export async function place(game: BoopGame, row: number, col: number, player: PlayerType, type: PieceType) {
const value = game.value;
// 从玩家supply中找到对应类型的棋子
const part = findPartInRegion(game, player, type);
if (!part) {
throw new Error(`No ${type} available in ${player}'s supply`);
}
const partId = part.id;
await game.produceAsync(state => {
// 将棋子从supply移动到棋盘
const part = state.pieces[partId];
moveToRegion(part, state.regions[player], state.regions.board, [row, col]);
});
return { row, col, player, type, partId };
}

View File

@ -0,0 +1,49 @@
import {BoopGame} from "@/samples/boop/types-extensions";
import {PieceType, PlayerType, WinnerType} from "@/samples/boop/types";
import {prompts} from "@/samples/boop/prompts";
import {findPartInRegion, isCellOccupied, isInBounds} from "@/samples/boop/utils";
import {place} from "@/samples/boop/commands/place";
import {boop} from "@/samples/boop/commands/boop";
import {checkWin} from "@/samples/boop/commands/win";
import {checkGraduates} from "@/samples/boop/commands/graduate";
import {checkFullBoard} from "@/samples/boop/commands/full-board";
import {BOARD_SIZE} from "@/samples/boop/constants";
/**
*
*/
export async function turn(game: BoopGame, turnPlayer: PlayerType) {
const {row, col, type} = await game.prompt(
prompts.play,
(player, row, col, type) => {
const pieceType = type === 'cat' ? 'cat' : 'kitten';
if (player !== turnPlayer) {
throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
}
if (!isInBounds(row, col)) {
throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`;
}
if (isCellOccupied(game, row, col)) {
throw `Cell (${row}, ${col}) is already occupied.`;
}
const found = findPartInRegion(game, player, pieceType);
if (!found) {
throw `No ${pieceType}s left in ${player}'s supply.`;
}
return {player, row,col,type};
},
game.value.currentPlayer
);
const pieceType = type === 'cat' ? 'cat' : 'kitten';
await place(game, row, col, turnPlayer, pieceType);
await boop(game, row, col, pieceType);
const winner = await checkWin(game);
if(winner) return { winner: winner };
await checkGraduates(game);
await checkFullBoard(game, turnPlayer);
return { winner: null };
}

View File

@ -0,0 +1,27 @@
import {BoopGame} from "@/samples/boop/types-extensions";
import {PlayerType, WinnerType} from "@/samples/boop/types";
import {findPartAtPosition, getLineCandidates} from "@/samples/boop/utils";
import {WIN_LENGTH} from "@/samples/boop/constants";
/**
* (线)
*/
export async function checkWin(game: BoopGame): Promise<WinnerType | null> {
for(const line of getLineCandidates()){
let whites = 0;
let blacks = 0;
for(const [row, col] of line){
const part = findPartAtPosition(game, row, col);
if(part?.type !== 'cat') continue;
if (part.player === 'white') whites++;
else blacks++;
}
if(whites >= WIN_LENGTH) {
return 'white';
}
if(blacks >= WIN_LENGTH) {
return 'black';
}
}
return null;
}

View File

@ -0,0 +1,3 @@
export const BOARD_SIZE = 6;
export const MAX_PIECES_PER_PLAYER = 8;
export const WIN_LENGTH = 3;

View File

@ -1,2 +1,49 @@
export * from './data';
export * from './commands';
// Types
export type {
PlayerType,
PieceType,
WinnerType,
RegionType,
BoopPartMeta,
BoopPart
} from './types';
export type {BoopGame} from './types-extensions';
// Constants
export {
BOARD_SIZE,
MAX_PIECES_PER_PLAYER,
WIN_LENGTH
} from './constants';
// State
export {
createInitialState,
type BoopState
} from './state';
// Prompts
export {prompts} from './prompts';
// Commands
export {
registry,
start,
place as placeCmd,
boop as boopCmd,
checkWin as checkWinCmd,
checkGraduates as checkGraduatesCmd,
checkFullBoard as checkFullBoardCmd,
turn as turnCmd
} from './commands';
// Utils
export {
getLineCandidates,
isInBounds,
isCellOccupied,
getNeighborPositions,
findPartInRegion,
findPartAtPosition
} from './utils';

View File

@ -0,0 +1,9 @@
import {createPromptDef} from "@/core/game";
import {PieceType, PlayerType} from "@/samples/boop/types";
export const prompts = {
play: createPromptDef<[PlayerType, number, number, PieceType?]>(
'play <player> <row:number> <col:number> [type:string]'),
choose: createPromptDef<[PlayerType, number, number]>(
'choose <player> <row:number> <col:number>')
}

View File

@ -1,25 +1,9 @@
import parts from './parts.csv';
import parts from './parts.csv';
import {createRegion, moveToRegion, Region} from "@/core/region";
import {createPartsFromTable} from "@/core/part-factory";
import {Part} from "@/core/part";
import {createPromptDef, IGameContext} from "@/core/game";
export const BOARD_SIZE = 6;
export const MAX_PIECES_PER_PLAYER = 8;
export const WIN_LENGTH = 3;
export type PlayerType = 'white' | 'black';
export type PieceType = 'kitten' | 'cat';
export type WinnerType = PlayerType | 'draw' | null;
export type RegionType = 'white' | 'black' | 'board' | '';
export type BoopPartMeta = { player: PlayerType; type: PieceType };
export type BoopPart = Part<BoopPartMeta>;
export const prompts = {
play: createPromptDef<[PlayerType, number, number, PieceType?]>(
'play <player> <row:number> <col:number> [type:string]'),
choose: createPromptDef<[PlayerType, number, number]>(
'choose <player> <row:number> <col:number>')
}
import {BoopPart} from "@/samples/boop/types";
import {BOARD_SIZE} from "@/samples/boop/constants";
import {PlayerType, PieceType, RegionType} from "@/samples/boop/types";
export function createInitialState() {
const pieces = createPartsFromTable(
@ -54,8 +38,7 @@ export function createInitialState() {
} as Record<RegionType, Region>,
pieces,
currentPlayer: 'white' as PlayerType,
winner: null as WinnerType,
winner: null as PlayerType | 'draw' | null,
};
}
export type BoopState = ReturnType<typeof createInitialState>;
export type BoopGame = IGameContext<BoopState>;

View File

@ -0,0 +1,4 @@
import {IGameContext} from "@/core/game";
import {BoopState} from "@/samples/boop/state";
export type BoopGame = IGameContext<BoopState>;

View File

@ -0,0 +1,9 @@
import {Part} from "@/core/part";
export type PlayerType = 'white' | 'black';
export type PieceType = 'kitten' | 'cat';
export type WinnerType = PlayerType | 'draw' | null;
export type RegionType = 'white' | 'black' | 'board' | '';
export type BoopPartMeta = { player: PlayerType; type: PieceType };
export type BoopPart = Part<BoopPartMeta>;

View File

@ -1,13 +1,6 @@
import {
BOARD_SIZE,
BoopGame,
BoopPart,
BoopState,
PieceType,
PlayerType,
RegionType,
WIN_LENGTH
} from "@/samples/boop/data";
import {BOARD_SIZE, WIN_LENGTH} from "@/samples/boop/constants";
import {BoopState} from "@/samples/boop/state";
import {BoopGame} from "@/samples/boop/types-extensions";
const DIRS = [
[0, 1],
@ -17,6 +10,10 @@ const DIRS = [
]
type PT = [number, number];
type Line = PT[];
/**
* 线
*/
export function* getLineCandidates(){
for(const [dx, dy] of DIRS){
for(let x = 0; x < BOARD_SIZE; x ++)
@ -38,11 +35,18 @@ export function isInBounds(x: number, y: number): boolean {
return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE;
}
/**
*
*/
export function isCellOccupied(game: BoopGame | BoopState, x: number, y: number): boolean {
const state = getState(game);
const id = `${x},${y}`;
return getState(game).regions.board.partMap[id] !== undefined;
return state.regions.board.partMap[id] !== undefined;
}
/**
*
*/
export function* getNeighborPositions(x: number = 0, y: number = 0){
for(let dx = -1; dx <= 1; dx ++)
for(let dy = -1; dy <= 1; dy ++)
@ -50,24 +54,6 @@ export function* getNeighborPositions(x: number = 0, y: number = 0){
yield [x + dx, y + dy] as PT;
}
export function findPartInRegion(ctx: BoopGame | BoopState, regionId: keyof BoopGame['value']['regions'], type: PieceType, player?: PlayerType): BoopPart | null {
const state = getState(ctx);
if(!regionId){
return Object.values(state.pieces).find(part => match(regionId, part, type, player)) || null;
}
const id = state.regions[regionId].childIds.find(id => match(regionId, state.pieces[id], type, player));
return id ? state.pieces[id] || null : null;
}
function match(regionId: RegionType, part: BoopPart, type: PieceType, player?: PlayerType){
return regionId === part.regionId && part.type === type && (!player || part.player === player);
}
export function findPartAtPosition(ctx: BoopGame | BoopState, row: number, col: number): BoopPart | null {
const state = getState(ctx);
const id = state.regions.board.partMap[`${row},${col}`];
return id ? state.pieces[id] || null : null;
}
function getState(ctx: BoopGame | BoopState): BoopState {
if('value' in ctx){
return ctx.value;

View File

@ -0,0 +1,11 @@
export {
getLineCandidates,
isInBounds,
isCellOccupied,
getNeighborPositions
} from './board';
export {
findPartInRegion,
findPartAtPosition
} from './pieces';

View File

@ -0,0 +1,40 @@
import {BoopPart, PieceType, PlayerType, RegionType} from "@/samples/boop/types";
import {BoopState} from "@/samples/boop/state";
import {BoopGame} from "@/samples/boop/types-extensions";
/**
*
*/
export function findPartInRegion(
ctx: BoopGame | BoopState,
regionId: keyof BoopGame['value']['regions'],
type: PieceType,
player?: PlayerType
): BoopPart | null {
const state = getState(ctx);
if(!regionId){
return Object.values(state.pieces).find(part => match(regionId, part, type, player)) || null;
}
const id = state.regions[regionId].childIds.find(id => match(regionId, state.pieces[id], type, player));
return id ? state.pieces[id] || null : null;
}
/**
*
*/
export function findPartAtPosition(ctx: BoopGame | BoopState, row: number, col: number): BoopPart | null {
const state = getState(ctx);
const id = state.regions.board.partMap[`${row},${col}`];
return id ? state.pieces[id] || null : null;
}
function match(regionId: RegionType, part: BoopPart, type: PieceType, player?: PlayerType){
return regionId === part.regionId && part.type === type && (!player || part.player === player);
}
function getState(ctx: BoopGame | BoopState): BoopState {
if('value' in ctx){
return ctx.value;
}
return ctx;
}

View File

@ -7,7 +7,7 @@ import {
findPartInRegion,
findPartAtPosition
} from '@/samples/boop/utils';
import { createInitialState, BOARD_SIZE, WIN_LENGTH } from '@/samples/boop/data';
import { createInitialState, BOARD_SIZE, WIN_LENGTH } from '@/samples/boop';
import { createGameContext } from '@/core/game';
import { registry } from '@/samples/boop';

View File

@ -3,6 +3,7 @@ import { fileURLToPath } from 'url';
import * as fs from 'fs';
import * as path from 'path';
import {csvLoader} from 'inline-schema/csv-loader/esbuild';
import type { Plugin } from 'esbuild';
const srcDir = fileURLToPath(new URL('./src', import.meta.url));
const samplesDir = fileURLToPath(new URL('./src/samples', import.meta.url));
@ -30,6 +31,32 @@ function getSamplesEntries(): Record<string, string> {
const samplesEntries = getSamplesEntries();
/**
* Plugin to rewrite @/core/* and @/utils/* imports to use 'boardgame-core'
*/
function rewriteBoardgameImports(): Plugin {
return {
name: 'rewrite-boardgame-imports',
setup(build) {
build.onResolve({ filter: /^@\/(core|utils)\// }, args => {
// Mark these as external and rewrite to 'boardgame-core'
return {
path: 'boardgame-core',
external: true,
};
});
// Also handle @/index imports
build.onResolve({ filter: /^@\/index$/ }, args => {
return {
path: 'boardgame-core',
external: true,
};
});
},
};
}
export default defineConfig({
entry: samplesEntries,
format: ['esm'],
@ -38,10 +65,5 @@ export default defineConfig({
sourcemap: true,
outDir: 'dist/samples',
external: ['@preact/signals-core', 'mutative', 'inline-schema', 'boardgame-core'],
esbuildPlugins: [csvLoader()],
esbuildOptions(options) {
options.alias = {
'@': srcDir,
};
},
esbuildPlugins: [csvLoader(), rewriteBoardgameImports()],
});