diff --git a/packages/sample-game/src/game/tic-tac-toe.ts b/packages/sample-game/src/game/tic-tac-toe.ts index d777be5..580e3d7 100644 --- a/packages/sample-game/src/game/tic-tac-toe.ts +++ b/packages/sample-game/src/game/tic-tac-toe.ts @@ -1 +1,19 @@ -export * from "boardgame-core/samples/tic-tac-toe"; \ No newline at end of file +/** + * Re-export tic-tac-toe game module from boardgame-core + * This provides a convenient import path within sample-game + */ +export { + registry, + prompts, + start, + createInitialState, + isCellOccupied, + hasWinningLine, + checkWinner, + placePiece, + type TicTacToePart, + type TicTacToeState, + type TicTacToeGame, + type PlayerType, + type WinnerType, +} from "boardgame-core/samples/tic-tac-toe"; \ No newline at end of file diff --git a/packages/sample-game/src/scenes/GameScene.ts b/packages/sample-game/src/scenes/GameScene.ts index 7eb0b4e..5e493a8 100644 --- a/packages/sample-game/src/scenes/GameScene.ts +++ b/packages/sample-game/src/scenes/GameScene.ts @@ -4,18 +4,39 @@ import { GameHostScene } from 'boardgame-phaser'; import { spawnEffect, type Spawner } from 'boardgame-phaser'; import {prompts} from "@/game/tic-tac-toe"; -const CELL_SIZE = 120; -const BOARD_OFFSET = { x: 100, y: 100 }; -const BOARD_SIZE = 3; +// 棋盘配置常量 +export const BOARD_CONFIG = { + cellSize: 120, + boardOffset: { x: 100, y: 100 }, + boardSize: 3, + colors: { + grid: 0x6b7280, + x: '#3b82f6', + o: '#ef4444', + title: '#1f2937', + turn: '#4b5563', + menuButton: 0x6b7280, + menuButtonHover: 0x4b5563, + overlay: 0x000000, + winText: '#fbbf24', + }, + fontSize: { + title: '28px', + turn: '20px', + cell: '64px', + menuButton: '18px', + winText: '36px', + }, +} as const; export class GameScene extends GameHostScene { private boardContainer!: Phaser.GameObjects.Container; private gridGraphics!: Phaser.GameObjects.Graphics; private turnText!: Phaser.GameObjects.Text; private winnerOverlay?: Phaser.GameObjects.Container; - private menuButton!: Phaser.GameObjects.Container; - private menuButtonText!: Phaser.GameObjects.Text; + private menuButtonContainer!: Phaser.GameObjects.Container; private menuButtonBg!: Phaser.GameObjects.Rectangle; + private menuButtonText!: Phaser.GameObjects.Text; constructor() { super('GameScene'); @@ -26,8 +47,10 @@ export class GameScene extends GameHostScene { this.boardContainer = this.add.container(0, 0); this.gridGraphics = this.add.graphics(); - this.drawGrid(); + + this.createBoardVisuals(); this.createMenuButton(); + this.createInputZones(); this.disposables.add(spawnEffect(new TicTacToePartSpawner(this))); @@ -45,39 +68,86 @@ export class GameScene extends GameHostScene { const currentPlayer = this.state.currentPlayer; this.updateTurnText(currentPlayer); }); - - this.setupInput(); } - private isCellOccupied(row: number, col: number): boolean { - return !!this.state.board.partMap[`${row},${col}`]; + /** 创建棋盘视觉元素(网格、标题、回合提示) */ + private createBoardVisuals(): void { + this.drawGrid(); + + const { boardSize, cellSize, boardOffset } = BOARD_CONFIG; + const centerX = boardOffset.x + (boardSize * cellSize) / 2; + + this.add.text(centerX, boardOffset.y - 40, 'Tic-Tac-Toe', { + fontSize: BOARD_CONFIG.fontSize.title, + fontFamily: 'Arial', + color: BOARD_CONFIG.colors.title, + }).setOrigin(0.5); + + this.turnText = this.add.text( + centerX, + boardOffset.y + boardSize * cellSize + 20, + '', + { + fontSize: BOARD_CONFIG.fontSize.turn, + fontFamily: 'Arial', + color: BOARD_CONFIG.colors.turn, + } + ).setOrigin(0.5); + + this.updateTurnText(this.state.currentPlayer); } + /** 绘制棋盘网格 */ + private drawGrid(): void { + const g = this.gridGraphics; + const { boardSize, cellSize, boardOffset } = BOARD_CONFIG; + + g.lineStyle(3, BOARD_CONFIG.colors.grid); + + for (let i = 1; i < boardSize; i++) { + g.lineBetween( + boardOffset.x + i * cellSize, + boardOffset.y, + boardOffset.x + i * cellSize, + boardOffset.y + boardSize * cellSize, + ); + g.lineBetween( + boardOffset.x, + boardOffset.y + i * cellSize, + boardOffset.x + boardSize * cellSize, + boardOffset.y + i * cellSize, + ); + } + + g.strokePath(); + } + + /** 创建菜单按钮 */ private createMenuButton(): void { const buttonX = this.game.scale.width - 80; const buttonY = 30; - this.menuButtonBg = this.add.rectangle(buttonX, buttonY, 120, 40, 0x6b7280) + this.menuButtonBg = this.add.rectangle(buttonX, buttonY, 120, 40, BOARD_CONFIG.colors.menuButton) .setInteractive({ useHandCursor: true }); this.menuButtonText = this.add.text(buttonX, buttonY, 'Menu', { - fontSize: '18px', + fontSize: BOARD_CONFIG.fontSize.menuButton, fontFamily: 'Arial', color: '#ffffff', }).setOrigin(0.5); - this.menuButton = this.add.container(buttonX, buttonY, [ + this.menuButtonContainer = this.add.container(buttonX, buttonY, [ this.menuButtonBg, this.menuButtonText, ]); // 按钮交互 this.menuButtonBg.on('pointerover', () => { - this.menuButtonBg.setFillStyle(0x4b5563); + this.menuButtonBg.setFillStyle(BOARD_CONFIG.colors.menuButtonHover); }); this.menuButtonBg.on('pointerout', () => { - this.menuButtonBg.setFillStyle(0x6b7280); + this.menuButtonBg.setFillStyle(BOARD_CONFIG.colors.menuButton); }); this.menuButtonBg.on('pointerdown', () => { @@ -85,73 +155,48 @@ export class GameScene extends GameHostScene { }); } - private async goToMenu(): Promise { - await this.sceneController.launch('MenuScene'); - } + /** 创建输入区域 */ + private createInputZones(): void { + const { boardSize, cellSize, boardOffset } = BOARD_CONFIG; + + for (let row = 0; row < boardSize; row++) { + for (let col = 0; col < boardSize; col++) { + const x = boardOffset.x + col * cellSize + cellSize / 2; + const y = boardOffset.y + row * cellSize + cellSize / 2; - private setupInput(): void { - for (let row = 0; row < BOARD_SIZE; row++) { - for (let col = 0; col < BOARD_SIZE; col++) { - const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2; - const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2; - - const zone = this.add.zone(x, y, CELL_SIZE, CELL_SIZE).setInteractive(); + const zone = this.add.zone(x, y, cellSize, cellSize).setInteractive(); zone.on('pointerdown', () => { - if (this.state.winner) return; - if (this.isCellOccupied(row, col)) return; - - const error = this.gameHost.tryAnswerPrompt(prompts.play, this.state.currentPlayer, row, col); - if (error) { - console.warn('Invalid move:', error); - } + this.handleCellClick(row, col); }); } } } - private drawGrid(): void { - const g = this.gridGraphics; - g.lineStyle(3, 0x6b7280); + /** 处理格子点击 */ + private handleCellClick(row: number, col: number): void { + if (this.state.winner) return; + if (this.isCellOccupied(row, col)) return; - for (let i = 1; i < BOARD_SIZE; i++) { - g.lineBetween( - BOARD_OFFSET.x + i * CELL_SIZE, - BOARD_OFFSET.y, - BOARD_OFFSET.x + i * CELL_SIZE, - BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE, - ); - g.lineBetween( - BOARD_OFFSET.x, - BOARD_OFFSET.y + i * CELL_SIZE, - BOARD_OFFSET.x + BOARD_SIZE * CELL_SIZE, - BOARD_OFFSET.y + i * CELL_SIZE, - ); + const error = this.gameHost.tryAnswerPrompt(prompts.play, this.state.currentPlayer, row, col); + if (error) { + console.warn('Invalid move:', error); } - - g.strokePath(); - - this.add.text(BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, BOARD_OFFSET.y - 40, 'Tic-Tac-Toe', { - fontSize: '28px', - fontFamily: 'Arial', - color: '#1f2937', - }).setOrigin(0.5); - - this.turnText = this.add.text(BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 20, '', { - fontSize: '20px', - fontFamily: 'Arial', - color: '#4b5563', - }).setOrigin(0.5); - - this.updateTurnText(this.state.currentPlayer); } + /** 检查格子是否被占用 */ + private isCellOccupied(row: number, col: number): boolean { + return !!this.state.board.partMap[`${row},${col}`]; + } + + /** 更新回合提示文本 */ private updateTurnText(player: string): void { if (this.turnText) { this.turnText.setText(`${player}'s turn`); } } + /** 显示获胜者 */ private showWinner(winner: string): void { // 清理旧的覆盖层防止叠加 if (this.winnerOverlay) { @@ -161,13 +206,16 @@ export class GameScene extends GameHostScene { this.winnerOverlay = this.add.container(); const text = winner === 'draw' ? "It's a draw!" : `${winner} wins!`; + const { boardSize, cellSize, boardOffset } = BOARD_CONFIG; + const centerX = boardOffset.x + (boardSize * cellSize) / 2; + const centerY = boardOffset.y + (boardSize * cellSize) / 2; const bg = this.add.rectangle( - BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, - BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2, - BOARD_SIZE * CELL_SIZE, - BOARD_SIZE * CELL_SIZE, - 0x000000, + centerX, + centerY, + boardSize * cellSize, + boardSize * cellSize, + BOARD_CONFIG.colors.overlay, 0.6, ).setInteractive({ useHandCursor: true }); @@ -177,16 +225,11 @@ export class GameScene extends GameHostScene { this.winnerOverlay.add(bg); - const winText = this.add.text( - BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, - BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2, - text, - { - fontSize: '36px', - fontFamily: 'Arial', - color: '#fbbf24', - }, - ).setOrigin(0.5); + const winText = this.add.text(centerX, centerY, text, { + fontSize: BOARD_CONFIG.fontSize.winText, + fontFamily: 'Arial', + color: BOARD_CONFIG.colors.winText, + }).setOrigin(0.5); this.winnerOverlay.add(winText); @@ -198,8 +241,14 @@ export class GameScene extends GameHostScene { repeat: 1, }); } + + /** 跳转到菜单场景 */ + private async goToMenu(): Promise { + await this.sceneController.launch('MenuScene'); + } } +/** 棋子生成器 */ class TicTacToePartSpawner implements Spawner { constructor(public readonly scene: GameScene) {} @@ -208,24 +257,23 @@ class TicTacToePartSpawner implements Spawner obj.destroy(), }); } + + /** 计算格子的屏幕位置 */ + private calculatePosition(position: number[]): { x: number; y: number } { + const { cellSize, boardOffset } = BOARD_CONFIG; + const [yIndex, xIndex] = position; + return { + x: xIndex * cellSize + boardOffset.x + cellSize / 2, + y: yIndex * cellSize + boardOffset.y + cellSize / 2, + }; + } + + /** 更新对象位置 */ + private updatePosition(part: TicTacToePart, obj: Phaser.GameObjects.Text): void { + const pos = this.calculatePosition(part.position); + obj.x = pos.x; + obj.y = pos.y; + } } \ No newline at end of file diff --git a/packages/sample-game/src/scenes/MenuScene.ts b/packages/sample-game/src/scenes/MenuScene.ts index df5a95e..60c6f26 100644 --- a/packages/sample-game/src/scenes/MenuScene.ts +++ b/packages/sample-game/src/scenes/MenuScene.ts @@ -1,10 +1,35 @@ import { ReactiveScene } from 'boardgame-phaser'; +/** 菜单场景配置 */ +const MENU_CONFIG = { + colors: { + title: '#1f2937', + buttonText: '#ffffff', + buttonBg: 0x3b82f6, + buttonBgHover: 0x2563eb, + subtitle: '#6b7280', + }, + fontSize: { + title: '48px', + button: '24px', + subtitle: '16px', + }, + button: { + width: 200, + height: 60, + }, + positions: { + titleY: -100, + buttonY: 40, + subtitleY: 140, + }, +} as const; + export class MenuScene extends ReactiveScene { private titleText!: Phaser.GameObjects.Text; - private startButton!: Phaser.GameObjects.Container; - private startButtonText!: Phaser.GameObjects.Text; + private startButtonContainer!: Phaser.GameObjects.Container; private startButtonBg!: Phaser.GameObjects.Rectangle; + private startButtonText!: Phaser.GameObjects.Text; constructor() { super('MenuScene'); @@ -13,17 +38,35 @@ export class MenuScene extends ReactiveScene { create(): void { super.create(); - const centerX = this.game.scale.width / 2; - const centerY = this.game.scale.height / 2; + const center = this.getCenterPosition(); + + this.createTitle(center); + this.createStartButton(center); + this.createSubtitle(center); + } - // 标题 - this.titleText = this.add.text(centerX, centerY - 100, 'Tic-Tac-Toe', { - fontSize: '48px', - fontFamily: 'Arial', - color: '#1f2937', - }).setOrigin(0.5); + /** 获取屏幕中心位置 */ + private getCenterPosition(): { x: number; y: number } { + return { + x: this.game.scale.width / 2, + y: this.game.scale.height / 2, + }; + } - // 添加标题动画 + /** 创建标题文本 */ + private createTitle(center: { x: number; y: number }): void { + this.titleText = this.add.text( + center.x, + center.y + MENU_CONFIG.positions.titleY, + 'Tic-Tac-Toe', + { + fontSize: MENU_CONFIG.fontSize.title, + fontFamily: 'Arial', + color: MENU_CONFIG.colors.title, + } + ).setOrigin(0.5); + + // 标题入场动画 this.titleText.setScale(0); this.tweens.add({ targets: this.titleText, @@ -31,36 +74,56 @@ export class MenuScene extends ReactiveScene { duration: 600, ease: 'Back.easeOut', }); + } - // 开始按钮 - this.startButtonBg = this.add.rectangle(centerX, centerY + 40, 200, 60, 0x3b82f6) - .setInteractive({ useHandCursor: true }); + /** 创建开始按钮 */ + private createStartButton(center: { x: number; y: number }): void { + const { button, colors } = MENU_CONFIG; - this.startButtonText = this.add.text(centerX, centerY + 40, 'Start Game', { - fontSize: '24px', - fontFamily: 'Arial', - color: '#ffffff', - }).setOrigin(0.5); + this.startButtonBg = this.add.rectangle( + center.x, + center.y + MENU_CONFIG.positions.buttonY, + button.width, + button.height, + colors.buttonBg + ).setInteractive({ useHandCursor: true }); - this.startButton = this.add.container(centerX, centerY + 40, [ - this.startButtonBg, - this.startButtonText, - ]); + this.startButtonText = this.add.text( + center.x, + center.y + MENU_CONFIG.positions.buttonY, + 'Start Game', + { + fontSize: MENU_CONFIG.fontSize.button, + fontFamily: 'Arial', + color: colors.buttonText, + } + ).setOrigin(0.5); + + this.startButtonContainer = this.add.container( + center.x, + center.y + MENU_CONFIG.positions.buttonY, + [this.startButtonBg, this.startButtonText] + ); // 按钮交互 + this.setupButtonInteraction(); + } + + /** 设置按钮交互效果 */ + private setupButtonInteraction(): void { this.startButtonBg.on('pointerover', () => { - this.startButtonBg.setFillStyle(0x2563eb); + this.startButtonBg.setFillStyle(MENU_CONFIG.colors.buttonBgHover); this.tweens.add({ - targets: this.startButton, + targets: this.startButtonContainer, scale: 1.05, duration: 100, }); }); this.startButtonBg.on('pointerout', () => { - this.startButtonBg.setFillStyle(0x3b82f6); + this.startButtonBg.setFillStyle(MENU_CONFIG.colors.buttonBg); this.tweens.add({ - targets: this.startButton, + targets: this.startButtonContainer, scale: 1, duration: 100, }); @@ -69,15 +132,23 @@ export class MenuScene extends ReactiveScene { this.startButtonBg.on('pointerdown', () => { this.startGame(); }); - - // 副标题 - this.add.text(centerX, centerY + 140, 'Click to start playing', { - fontSize: '16px', - fontFamily: 'Arial', - color: '#6b7280', - }).setOrigin(0.5); } + /** 创建副标题 */ + private createSubtitle(center: { x: number; y: number }): void { + this.add.text( + center.x, + center.y + MENU_CONFIG.positions.subtitleY, + 'Click to start playing', + { + fontSize: MENU_CONFIG.fontSize.subtitle, + fontFamily: 'Arial', + color: MENU_CONFIG.colors.subtitle, + } + ).setOrigin(0.5); + } + + /** 开始游戏 */ private async startGame(): Promise { await this.sceneController.launch('GameScene'); }