ドメインモデル設計¶
概要¶
ぷよぷよゲームのドメインモデル設計について説明します。ドメイン駆動設計(DDD)の戦術的設計パターンを適用し、ビジネスロジックを適切にモデル化します。
ドメイン分析¶
ユビキタス言語¶
主要用語定義:
- ぷよ: 色付きのゲーム要素
- 組ぷよ: 2つのぷよがセットになった操作対象
- フィールド: ぷよが配置される6×12のグリッド空間
- 連鎖: ぷよ消去により新たな消去条件が成立すること
- 消去: 同色のぷよが4つ以上隣接した際の除去処理
- 全消し: フィールドのすべてのぷよが消去されること
- 落下: 重力によるぷよの下方移動
- 固定: 落下中のぷよがフィールドに配置されること
- 回転: 組ぷよの相対位置変更
- 窒息: 新しいぷよが配置できない状態
- 壁蹴り: 回転時の位置調整
境界づけられたコンテキスト¶
集約設計¶
Game集約¶
Chain集約¶
ドメインサービス¶
ゲーム基本サービス¶
AI評価サービス(関数型実装)¶
関数型評価サービス実装例¶
// 純粋関数による評価実装例
export const evaluateMove = (
move: PossibleMove,
gameState: AIGameState,
settings: EvaluationSettings = DEFAULT_EVALUATION_SETTINGS,
): MoveEvaluation => {
if (!move.isValid) {
return createInvalidMoveEvaluation()
}
return createBasicMoveEvaluation(move, gameState, settings)
}
// 関数合成による複合評価
export const evaluateAndSortMoves = (
moves: PossibleMove[],
gameState: AIGameState,
settings: EvaluationSettings = DEFAULT_EVALUATION_SETTINGS,
): Array<PossibleMove & { evaluation: MoveEvaluation }> =>
moves
.map((move) => ({
...move,
evaluation: evaluateMove(move, gameState, settings),
}))
.sort((a, b) => b.evaluation.totalScore - a.evaluation.totalScore)
// 設定の部分更新(イミュータブル)
export const updateEvaluationSettings = (
currentSettings: EvaluationSettings,
updates: Partial<EvaluationSettings>,
): EvaluationSettings => ({
...currentSettings,
...updates,
})
関数型設計の利点¶
ドメインイベント¶
仕様パターン¶
ゲームルール仕様¶
具体的な仕様実装:
class ErasableGroupSpecification implements Specification<PuyoGroup> {
isSatisfiedBy(group: PuyoGroup): boolean {
return group.size() >= 4 &&
group.puyos.every(p => p.color === group.color);
}
}
class ValidPlacementSpecification implements Specification<PuyoPlacement> {
constructor(private field: Field) {}
isSatisfiedBy(placement: PuyoPlacement): boolean {
return this.field.isValidPosition(placement.position) &&
!this.field.isOccupied(placement.position) &&
this.hasSupport(placement);
}
private hasSupport(placement: PuyoPlacement): boolean {
// 重力サポートの確認ロジック
}
}
ファクトリーパターン¶
ゲーム要素の生成¶
リポジトリパターン¶
ドメインオブジェクトの永続化¶
ドメインルールの実装¶
不変条件 (Invariants)¶
class Game {
private constructor(
private readonly id: GameId,
private field: Field,
private currentPuyo: PuyoPair,
private nextPuyo: PuyoPair,
private score: Score,
private state: GameState
) {
this.ensureInvariants();
}
private ensureInvariants(): void {
if (!this.field) {
throw new DomainError('Game must have a field');
}
if (this.state === GameState.PLAYING && !this.currentPuyo) {
throw new DomainError('Playing game must have current puyo');
}
if (this.score.current < 0) {
throw new DomainError('Score cannot be negative');
}
}
handleInput(command: InputCommand): DomainEvent[] {
this.ensureGameIsPlaying();
const events: DomainEvent[] = [];
switch (command.type) {
case InputType.MOVE_LEFT:
if (this.canMoveLeft()) {
this.currentPuyo = this.currentPuyo.move(Direction.LEFT);
events.push(new PuyoMovedEvent(this.id, this.currentPuyo));
}
break;
case InputType.ROTATE:
if (this.canRotate()) {
this.currentPuyo = this.currentPuyo.rotate(RotationDirection.CLOCKWISE);
events.push(new PuyoRotatedEvent(this.id, this.currentPuyo));
}
break;
case InputType.DROP:
events.push(...this.dropCurrentPuyo());
break;
}
this.ensureInvariants();
return events;
}
}
ビジネスロジックの実装¶
class ChainProcessor {
processChain(field: Field): ChainResult {
const erasedGroups: PuyoGroup[] = [];
let chainCount = 0;
let totalScore = 0;
while (true) {
// 消去対象のグループを検索
const currentErasableGroups = this.chainDetectionService
.findErasableGroups(field);
if (currentErasableGroups.length === 0) {
break; // 連鎖終了
}
chainCount++;
// ぷよを消去
for (const group of currentErasableGroups) {
this.eraseGroup(field, group);
erasedGroups.push(group);
}
// スコア計算
const chainScore = this.scoreCalculationService
.calculateChainScore(currentErasableGroups, chainCount);
totalScore += chainScore;
// 重力適用
const gravityResult = this.gravityService.applyGravity(field);
if (!gravityResult.hasMovement) {
break; // これ以上の落下なし
}
}
const isAllClear = field.isEmpty();
if (isAllClear) {
totalScore += this.scoreCalculationService.calculateAllClearBonus();
}
return new ChainResult(erasedGroups, totalScore, chainCount, isAllClear);
}
}
エラーハンドリング¶
ドメイン例外¶
テスト戦略¶
ドメインモデルのテスト¶
テストデータビルダー¶
class GameTestDataBuilder {
private game: Game;
constructor() {
this.game = GameFactory.createNewGame();
}
withField(field: Field): GameTestDataBuilder {
this.game = new Game(
this.game.id,
field,
this.game.currentPuyo,
this.game.nextPuyo,
this.game.score,
this.game.state
);
return this;
}
withScore(score: number): GameTestDataBuilder {
this.game = new Game(
this.game.id,
this.game.field,
this.game.currentPuyo,
this.game.nextPuyo,
new Score(score, this.game.score.high),
this.game.state
);
return this;
}
build(): Game {
return this.game;
}
}
AI関連ドメインモデル¶
AI戦略値オブジェクト¶
AIドメインサービス¶
まとめ¶
このドメインモデル設計により以下を実現:
- 表現力: ビジネスロジックが自然な言語で表現
- 整合性: 不変条件によるデータ整合性保証
- 拡張性: 新しいゲームルールやAI戦略の追加が容易
- テスタビリティ: ドメインロジックの独立したテスト
- 保守性: 複雑なビジネスルールの明確な構造化
- 再利用性: ドメインサービスによる共通ロジックの抽出
- AI統合: 戦略パターンとパフォーマンス分析のドメインモデル化
- 関数型アプローチ: 純粋関数による予測可能で安全な評価ロジック
関数型リファクタリング成果¶
EvaluationService関数型化(2025-08-19実装)¶
- 純粋関数化: 副作用を排除し、予測可能な動作を実現
- テスタビリティ向上: 17のテストケースで100%カバレッジ達成
- コード重複削減: AIService、MLAIServiceの評価ロジックを統合
- 合成可能性: 小さな関数の組み合わせによる柔軟な評価システム
- 保守性向上: 状態管理不要による理解しやすいコード構造
技術的メリット¶
- 並行処理安全性: 状態なしによるスレッドセーフな処理
- デバッグ容易性: 入力と出力の明確な対応関係
- 拡張性: 新しい評価関数の追加が容易
- 一貫性: 関数型パラダイムによる統一されたアプローチ