Compare commits
No commits in common. "98a12b926639fbc7b0d495b7be5002bcbaba4662" and "10393f45b6fc2d4b09a75f31f8ba514a7d4c3e5a" have entirely different histories.
98a12b9266
...
10393f45b6
|
|
@ -0,0 +1,259 @@
|
|||
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
|
||||
});
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
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 };
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
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]);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
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
|
||||
};
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
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 };
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
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 };
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export const BOARD_SIZE = 6;
|
||||
export const MAX_PIECES_PER_PLAYER = 8;
|
||||
export const WIN_LENGTH = 3;
|
||||
|
|
@ -1,9 +1,25 @@
|
|||
import parts from './parts.csv';
|
||||
import parts from './parts.csv';
|
||||
import {createRegion, moveToRegion, Region} from "@/core/region";
|
||||
import {createPartsFromTable} from "@/core/part-factory";
|
||||
import {BoopPart} from "@/samples/boop/types";
|
||||
import {BOARD_SIZE} from "@/samples/boop/constants";
|
||||
import {PlayerType, PieceType, RegionType} from "@/samples/boop/types";
|
||||
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>')
|
||||
}
|
||||
|
||||
export function createInitialState() {
|
||||
const pieces = createPartsFromTable(
|
||||
|
|
@ -11,7 +27,7 @@ export function createInitialState() {
|
|||
(item, index) => `${item.player}-${item.type}-${index + 1}`,
|
||||
(item) => item.count
|
||||
) as Record<string, BoopPart>;
|
||||
|
||||
|
||||
// Initialize region childIds
|
||||
const whiteRegion = createRegion('white', []);
|
||||
const blackRegion = createRegion('black', []);
|
||||
|
|
@ -19,7 +35,7 @@ export function createInitialState() {
|
|||
{ name: 'x', min: 0, max: BOARD_SIZE - 1 },
|
||||
{ name: 'y', min: 0, max: BOARD_SIZE - 1 },
|
||||
]);
|
||||
|
||||
|
||||
// Populate region childIds based on piece regionId
|
||||
for (const part of Object.values(pieces)) {
|
||||
if(part.type !== 'kitten') continue;
|
||||
|
|
@ -29,7 +45,7 @@ export function createInitialState() {
|
|||
moveToRegion(part, null, blackRegion);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
regions: {
|
||||
white: whiteRegion,
|
||||
|
|
@ -38,7 +54,8 @@ export function createInitialState() {
|
|||
} as Record<RegionType, Region>,
|
||||
pieces,
|
||||
currentPlayer: 'white' as PlayerType,
|
||||
winner: null as PlayerType | 'draw' | null,
|
||||
winner: null as WinnerType,
|
||||
};
|
||||
}
|
||||
export type BoopState = ReturnType<typeof createInitialState>;
|
||||
export type BoopGame = IGameContext<BoopState>;
|
||||
|
|
@ -1,49 +1,2 @@
|
|||
// 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';
|
||||
export * from './data';
|
||||
export * from './commands';
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
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>')
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import {IGameContext} from "@/core/game";
|
||||
import {BoopState} from "@/samples/boop/state";
|
||||
|
||||
export type BoopGame = IGameContext<BoopState>;
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
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>;
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
import {BOARD_SIZE, WIN_LENGTH} from "@/samples/boop/constants";
|
||||
import {BoopState} from "@/samples/boop/state";
|
||||
import {BoopGame} from "@/samples/boop/types-extensions";
|
||||
import {
|
||||
BOARD_SIZE,
|
||||
BoopGame,
|
||||
BoopPart,
|
||||
BoopState,
|
||||
PieceType,
|
||||
PlayerType,
|
||||
RegionType,
|
||||
WIN_LENGTH
|
||||
} from "@/samples/boop/data";
|
||||
|
||||
const DIRS = [
|
||||
[0, 1],
|
||||
|
|
@ -10,10 +17,6 @@ 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 ++)
|
||||
|
|
@ -35,18 +38,11 @@ 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 state.regions.board.partMap[id] !== undefined;
|
||||
return getState(game).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 ++)
|
||||
|
|
@ -54,9 +50,27 @@ 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;
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
export {
|
||||
getLineCandidates,
|
||||
isInBounds,
|
||||
isCellOccupied,
|
||||
getNeighborPositions
|
||||
} from './board';
|
||||
|
||||
export {
|
||||
findPartInRegion,
|
||||
findPartAtPosition
|
||||
} from './pieces';
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import {
|
|||
findPartInRegion,
|
||||
findPartAtPosition
|
||||
} from '@/samples/boop/utils';
|
||||
import { createInitialState, BOARD_SIZE, WIN_LENGTH } from '@/samples/boop';
|
||||
import { createInitialState, BOARD_SIZE, WIN_LENGTH } from '@/samples/boop/data';
|
||||
import { createGameContext } from '@/core/game';
|
||||
import { registry } from '@/samples/boop';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ 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));
|
||||
|
|
@ -31,32 +30,6 @@ 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'],
|
||||
|
|
@ -65,5 +38,10 @@ export default defineConfig({
|
|||
sourcemap: true,
|
||||
outDir: 'dist/samples',
|
||||
external: ['@preact/signals-core', 'mutative', 'inline-schema', 'boardgame-core'],
|
||||
esbuildPlugins: [csvLoader(), rewriteBoardgameImports()],
|
||||
esbuildPlugins: [csvLoader()],
|
||||
esbuildOptions(options) {
|
||||
options.alias = {
|
||||
'@': srcDir,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue