Compare commits

..

2 Commits

Author SHA1 Message Date
hypercross 0e7aaea7da docs: update Agents.md 2026-04-05 09:51:49 +08:00
hypercross acb2fc82ba feat: animate after booped 2026-04-05 09:45:01 +08:00
4 changed files with 22 additions and 12 deletions

1
.qwenignore Normal file
View File

@ -0,0 +1 @@
!node_modules

View File

@ -9,35 +9,34 @@ A Phaser 3 framework for building web board games, built on top of `boardgame-co
## boardgame-core Usage ## boardgame-core Usage
For detailed boardgame-core API documentation and examples, see **[boardgame-core Guide](docs/boardgame-core-guide.md)**. For detailed boardgame-core API documentation and examples, see `packages/framework/node_modules/boardgame-core/`
Key concepts: Key concepts:
- **MutableSignal** — Reactive state container with `.value` and `.produce()` - **MutableSignal** — Reactive state container with `.value` and `.produce()`
- **Command System** — CLI-style parsing with schema validation and prompt support - **Command System** — CLI-style parsing with schema validation and prompt support
- **Region System** — Spatial management with `createRegion()`, `applyAlign()`, `shuffle()`, `moveToRegion()` - **Region System** — Spatial management with `createRegion()`, `applyAlign()`, `shuffle()`, `moveToRegion()`
- **Part System** — Game pieces with `createPart()`, `createPartPool()`, `flip()`, `roll()` - **Part System** — Game pieces with `createPartsFromTable()`, `flip()`, `roll()`
- **RNG** — Deterministic PRNG via `createRNG(seed)` for reproducible game states - **RNG** — Deterministic PRNG via `createRNG(seed)` for reproducible game states
### Quick Example ### Quick Example
```ts ```ts
import { createGameCommandRegistry, createRegion, MutableSignal } from 'boardgame-core'; import { createGameCommandRegistry, createRegion, IGameContext } from 'boardgame-core';
type GameState = { type GameState = {
board: Region; board: Region;
parts: Part<{ player: 'X' | 'O' }>[]; parts: Part<{ player: 'X' | 'O' }>[];
currentPlayer: 'X' | 'O'; currentPlayer: 'X' | 'O';
}; };
type Game = IGameContext<GameState>;
const registration = createGameCommandRegistry<GameState>(); const registry = createGameCommandRegistry<GameState>();
export const registry = registration.registry;
registration.add('place <row:number> <col:number>', async function(cmd) { registry.register('place <row:number> <col:number>', async function(game: Game, row: number, col: number) {
const [row, col] = cmd.params as [number, number]; await game.produceAsync(state => {
this.context.produce(state => {
state.parts.push({ id: `p-${row}-${col}`, regionId: 'board', position: [row, col], player: state.currentPlayer }); state.parts.push({ id: `p-${row}-${col}`, regionId: 'board', position: [row, col], player: state.currentPlayer });
}); });
return { success: true }; return true;
}); });
``` ```
@ -71,7 +70,7 @@ pnpm --filter sample-game typecheck # tsc --noEmit (add to scripts first)
```bash ```bash
# boardgame-core is a local dependency via symlink (link:../../../boardgame-core) # boardgame-core is a local dependency via symlink (link:../../../boardgame-core)
# After changes to boardgame-core, simply rebuild it: # After changes to boardgame-core, simply rebuild it:
cd ../boardgame-core && pnpm build cd ../boardgame-core && npm build
# The symlink automatically resolves to the updated dist/ # The symlink automatically resolves to the updated dist/
``` ```

View File

@ -51,6 +51,7 @@ const placeCommand = registry.register( 'place <row:number> <col:number> <player
async function boop(game: BoopGame, row: number, col: number, type: PieceType) { async function boop(game: BoopGame, row: number, col: number, type: PieceType) {
const booped: string[] = []; const booped: string[] = [];
const toRemove = new Set<string>();
await game.produceAsync((state: BoopState) => { await game.produceAsync((state: BoopState) => {
// 按照远离放置位置的方向推动 // 按照远离放置位置的方向推动
for (const [dr, dc] of getNeighborPositions()) { for (const [dr, dc] of getNeighborPositions()) {
@ -73,8 +74,9 @@ async function boop(game: BoopGame, row: number, col: number, type: PieceType) {
// 检查新位置是否为空或在棋盘外 // 检查新位置是否为空或在棋盘外
if (!isInBounds(newRow, newCol)) { if (!isInBounds(newRow, newCol)) {
// 棋子被推出棋盘,返回玩家supply // 棋子被推出棋盘,返回玩家supply
toRemove.add(part.id);
booped.push(part.id); booped.push(part.id);
moveToRegion(part, state.regions.board, state.regions[part.player]); moveToRegion(part, state.regions.board, state.regions.board, [newRow, newCol]);
} else if (!isCellOccupied(state, newRow, newCol)) { } else if (!isCellOccupied(state, newRow, newCol)) {
// 新位置为空,移动过去 // 新位置为空,移动过去
booped.push(part.id); booped.push(part.id);
@ -83,6 +85,13 @@ async function boop(game: BoopGame, row: number, col: number, type: PieceType) {
// 如果新位置被占用,则不移动(两个棋子都保持原位) // 如果新位置被占用,则不移动(两个棋子都保持原位)
} }
}); });
await game.produceAsync((state: BoopState) => {
// 移除被吃掉的棋子
for (const partId of toRemove) {
const part = state.pieces[partId];
moveToRegion(part, state.regions.board, state.regions[part.player]);
}
});
return { booped }; return { booped };
} }

View File

@ -22,13 +22,14 @@ class BoopPartSpawner implements Spawner<BoopPart, Phaser.GameObjects.Container>
const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2; const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2;
const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2; const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2;
this.scene.tweens.add({ const tween = this.scene.tweens.add({
targets: obj, targets: obj,
x: x, x: x,
y: y, y: y,
duration: 200, duration: 200,
ease: 'Power2', ease: 'Power2',
}); });
this.scene.addTweenInterruption(tween);
} }
onSpawn(part: BoopPart) { onSpawn(part: BoopPart) {