はじめに
ポケモンバトルをDDDで実装していきます。
モデリング
どこまで実施していくかなどは明確に定義していない。というよりもできないので、ざっくりまずは作っていきます。必要に応じて拡張していく形にしていきます。
marmaid.jsのコード
classDiagram
class Battle {
+Turn : int <<ターン数>>
+PlayerA : Party <<プレーヤーA>>
+PlayerB : Party <<プレーヤーB>>
}
note for Battle "バトル全体を表す集約ルート"
class Party {
+TrainerID : string <<トレーナーID>>
+Active : Pokemon <<バトルに出ているポケモン>>
+Bench : List~Pokemon~ <<控えポケモンのリスト>>
}
note for Party "トレーナーのポケモンパーティを表す"
class Pokemon {
+Species : string <<種族名>>
+Stats : Stats <<能力値>>
+HP : HP <<体力>>
+Status : Status <<状態異常>>
+Moves : Move[4] <<技スロット>>
}
note for Pokemon "個々のポケモンを表す"
class Move {
+Name : string <<技名>>
+Type : string <<タイプ>>
+Power : int <<威力>>
+Accuracy : float <<命中率>>
}
note for Move "ポケモンが使う技を表す"
class HP {
+Max : int <<最大HP>>
+Cur : int <<現在HP>>
}
note for HP "ポケモンのHP(体力)を表す"
class Stats {
+Atk : int <<攻撃>>
+Def : int <<防御>>
+Spe : int <<素早さ>>
}
note for Stats "ポケモンの能力値(攻撃/防御/素早さ など)"
Battle "1" --> "2" Party : has
Party "1" --> "1..6" Pokemon : owns
Pokemon "1" --> "1..4" Move : has
Pokemon "1" --> "1" HP
Pokemon "1" --> "1" Stats
フェーズ1でやりたいこと(MVP:1ターンの戦闘)
🎯 目的
- ポケモンバトルの最小単位をDDDで動かす
- まずは「1ターンの攻防」が正しく流れるかを確認する
🧩 スコープ(やること)
-
バトル全体(Battle集約)
- 2人のプレーヤー(Party)を持つ
-
TakeTurnで1ターン進行
-
ポケモン(Pokemonエンティティ)
- HP・能力値・技を持つ
- ダメージを受けたらHPを減らす
-
技(Move値オブジェクト)
- 威力・命中率・優先度だけを保持
- タイプや追加効果はまだ入れない
-
DomainService(戦闘ルール)
- OrderService : 先制技 → 素早さで行動順を決める
- AccuracyService : 命中判定(この段階では100%命中固定)
- DamageService : 超シンプルなダメージ式(相性・乱数なし)
-
イベントログ
- 「○○の○○!→××ダメージ」「××はひんしになった!」など
- バトルの進行を後から追えるようにする
🚫 スコープ外(まだやらない)
- 複数ターンのループ
- 勝敗判定
- タイプ相性・STAB・乱数
- 状態異常・天候
- 交代や瀕死時の入れ替え
✅ ゴール(Doneの定義)
- コマンドで お互いの技を1つ選んで1ターン進行できる
- 行動順が優先度→素早さで決まる
- HPが減る、0なら「ひんし」のイベントが出る
- 単体のテストで再現可能(決定的な動作)
全体概要/クラス図
ディレクトリ構成
.
├── cmd
│ └── battle_cli.go
├── docs
│ └── domain.puml
├── domain
│ ├── battle
│ │ └── battle.go
│ ├── move
│ │ └── move.go
│ ├── pokemon
│ │ ├── hp.go
│ │ ├── moveset.go
│ │ ├── pokemon.go
│ │ ├── species.go
│ │ └── stats.go
│ └── services
│ └── services.go
├── domain.md
└── go.mod
7 directories, 12 files
コード詳細
domain/move/move.go
package move
// Move は技を表す値オブジェクト
type Move struct {
Name string // 技名
Type string // 技タイプ(例: Fire, Water, Grass)
Power int // 威力
Accuracy float64 // 命中率(0.0〜1.0)
Priority int // 優先度(でんこうせっか=1、通常技=0)
}
domain/pokemon/hp.go
// domain/pokemon/hp.go
package pokemon
// HP はポケモンの体力を表す値オブジェクト
type HP struct {
Max int
Cur int
}
// Apply ダメージを適用して新しいHPを返す(0未満にならない)
func (h HP) Apply(dmg int) HP {
if dmg < 0 {
dmg = 0
}
cur := h.Cur - dmg
if cur < 0 {
cur = 0
}
return HP{Max: h.Max, Cur: cur}
}
// IsFainted 瀕死状態かどうか
func (h HP) IsFainted() bool {
return h.Cur == 0
}
domain/pokemon/moveset.go
package pokemon
import (
"errors"
"pokemon_ddd_v2/domain/move"
)
const MaxMoves = 4
// MoveSet は最大4つの技を保持する値オブジェクト風の薄い入れ物
type MoveSet struct {
list []move.Move
}
func NewMoveSet(ms ...move.Move) (MoveSet, error) {
if len(ms) > MaxMoves {
return MoveSet{}, errors.New("too many moves")
}
cp := make([]move.Move, len(ms))
copy(cp, ms)
return MoveSet{list: cp}, nil
}
func (s MoveSet) Len() int { return len(s.list) }
func (s MoveSet) At(i int) (move.Move, error) {
if i < 0 || i >= len(s.list) {
return move.Move{}, errors.New("move index out of range")
}
return s.list[i], nil
}
func (s MoveSet) All() []move.Move {
cp := make([]move.Move, len(s.list))
copy(cp, s.list)
return cp
}
// Learn は空きスロットがあれば追加、満杯ならエラー
func (s *MoveSet) Learn(m move.Move) error {
if len(s.list) >= MaxMoves {
return errors.New("move slots are full")
}
s.list = append(s.list, m)
return nil
}
// Replace は指定スロットを別の技に差し替える
func (s *MoveSet) Replace(i int, m move.Move) error {
if i < 0 || i >= len(s.list) {
return errors.New("move index out of range")
}
s.list[i] = m
return nil
}
domain/pokemon/species.go
package pokemon
// Species はポケモンの種族(型)情報(MVPでは名前と単タイプのみ)
type Species struct {
Name string
Type string // 例: "Fire", "Water", "Grass"
}
domain/pokemon/stats.go
package pokemon
// Stats はポケモンの能力値(最小セット)
type Stats struct {
Atk int // 攻撃
Def int // 防御
Spe int // 素早さ
}
domain/pokemon/stats.go
package pokemon
import (
"errors"
"pokemon_ddd_v2/domain/move"
)
// Pokemon はバトルで戦う個体(Entity)
type Pokemon struct {
Species Species
Stats Stats
HP HP
Moves MoveSet
}
// NewPokemon は不変条件をチェックして生成
func NewPokemon(sp Species, st Stats, hp HP, moves ...move.Move) (*Pokemon, error) {
if hp.Max <= 0 || hp.Cur <= 0 || hp.Cur > hp.Max {
return nil, errors.New("invalid HP")
}
ms, err := NewMoveSet(moves...)
if err != nil {
return nil, err
}
return &Pokemon{
Species: sp,
Stats: st,
HP: hp,
Moves: ms,
}, nil
}
// ApplyDamage は被ダメ処理(下限0)— 自身の整合性を守る責務
func (p *Pokemon) ApplyDamage(dmg int) {
p.HP = p.HP.Apply(dmg)
}
// IsFainted は瀕死判定
func (p *Pokemon) IsFainted() bool {
return p.HP.IsFainted()
}
// MoveAt は i 番目の技を取り出す(Battle から参照される前提の読み取り専用)
func (p *Pokemon) MoveAt(i int) (move.Move, error) {
return p.Moves.At(i)
}
// Learn/Replace は育成などの操作で利用(MVPでは未使用でもOK)
func (p *Pokemon) Learn(m move.Move) error { return p.Moves.Learn(m) }
func (p *Pokemon) Replace(i int, m move.Move) error { return p.Moves.Replace(i, m) }
domain/services/services.go
package services
import (
"pokemon_ddd_v2/domain/move"
"pokemon_ddd_v2/domain/pokemon"
)
// Acting: 1回の行動に必要な情報
type Acting struct {
Attacker *pokemon.Pokemon
Defender *pokemon.Pokemon
Move move.Move
}
// ルール用インターフェイス
type OrderService interface{ Decide(a, b Acting) []Acting } // 行動順
type AccuracyService interface{ Hit(a Acting) bool } // 命中判定
type DamageService interface{ Calc(a Acting) int } // ダメージ計算
// まとめて渡すための構造体
type Services struct {
Order OrderService
Accuracy AccuracyService
Damage DamageService
}
// ---------- 最小実装 ----------
// SimpleOrder: ①優先度 ②素早さ(同速は先に渡された側を先行に簡略)
type SimpleOrder struct{}
func (SimpleOrder) Decide(a, b Acting) []Acting {
if a.Move.Priority != b.Move.Priority {
if a.Move.Priority > b.Move.Priority {
return []Acting{a, b}
}
return []Acting{b, a}
}
if a.Attacker.Stats.Spe >= b.Attacker.Stats.Spe {
return []Acting{a, b}
}
return []Acting{b, a}
}
// AlwaysHit: 常に命中(フェーズ1のMVP用)
type AlwaysHit struct{}
func (AlwaysHit) Hit(_ Acting) bool { return true }
// SimpleDamage: 相性/STAB/乱数なしの最小式
type SimpleDamage struct{}
func (SimpleDamage) Calc(x Acting) int {
atk := x.Attacker.Stats.Atk
def := x.Defender.Stats.Def
if def <= 0 {
def = 1
}
base := (2*atk/5 + 2) * x.Move.Power / def / 5
if base < 1 {
base = 1
}
return base
}
domain/battle/battle.go
package battle
import (
"fmt"
"pokemon_ddd_v2/domain/pokemon"
"pokemon_ddd_v2/domain/services"
)
type Party struct {
TrainerID string
Active *pokemon.Pokemon
Bench []*pokemon.Pokemon
}
type Battle struct {
Turn int
SideA Party
SideB Party
Log []string
}
type MoveChoice struct{ Index int }
type TurnCommand struct {
A MoveChoice
B MoveChoice
}
func (b *Battle) TakeTurn(cmd TurnCommand, sv services.Services) []string {
evs := []string{fmt.Sprintf("Turn %d start", b.Turn+1)}
// 技を素直に取得(ここでエラーなら “no move” ログにする)
am, aErr := b.SideA.Active.MoveAt(cmd.A.Index)
bm, bErr := b.SideB.Active.MoveAt(cmd.B.Index)
a := services.Acting{Attacker: b.SideA.Active, Defender: b.SideB.Active, Move: am}
d := services.Acting{Attacker: b.SideB.Active, Defender: b.SideA.Active, Move: bm}
for _, act := range sv.Order.Decide(a, d) {
if act.Attacker.IsFainted() || act.Defender.IsFainted() {
continue
}
// どちら側の行動かを判断して、取得エラーの詳細を出す
if act.Attacker == b.SideA.Active && aErr != nil {
evs = append(evs, fmt.Sprintf("%s tried to act but failed (no move): idx=%d err=%v",
act.Attacker.Species.Name, cmd.A.Index, aErr))
continue
}
if act.Attacker == b.SideB.Active && bErr != nil {
evs = append(evs, fmt.Sprintf("%s tried to act but failed (no move): idx=%d err=%v",
act.Attacker.Species.Name, cmd.B.Index, bErr))
continue
}
if !sv.Accuracy.Hit(act) {
evs = append(evs, fmt.Sprintf("%s の %s は外れた",
act.Attacker.Species.Name, act.Move.Name))
continue
}
dmg := sv.Damage.Calc(act)
act.Defender.ApplyDamage(dmg)
evs = append(evs, fmt.Sprintf("%s の %s → %d ダメージ (相手HP: %d/%d)",
act.Attacker.Species.Name, act.Move.Name, dmg,
act.Defender.HP.Cur, act.Defender.HP.Max))
if act.Defender.IsFainted() {
evs = append(evs, fmt.Sprintf("%s は ひんし になった", act.Defender.Species.Name))
}
}
b.Turn++
b.Log = append(b.Log, evs...)
return evs
}
cmd/battle_cli.go
package main
import (
"fmt"
"pokemon_ddd_v2/domain/battle"
"pokemon_ddd_v2/domain/move"
"pokemon_ddd_v2/domain/pokemon"
"pokemon_ddd_v2/domain/services"
)
func main() {
// --- 技の定義 ---
// 技①:たいあたり
tackle := move.Move{
Name: "たいあたり",
Type: "ノーマル",
Power: 40, // 威力
Accuracy: 1.0, // 命中率100%
Priority: 0, // 通常の優先度
}
// 技②:でんこうせっか(先制技)
quickAttack := move.Move{
Name: "でんこうせっか",
Type: "ノーマル",
Power: 40,
Accuracy: 1.0,
Priority: 1, // 優先度1(通常より先に行動)
}
// --- ポケモンを生成 ---
// プレイヤーAのポケモン:ヒトカゲ
a, _ := pokemon.NewPokemon(
pokemon.Species{Name: "ヒトカゲ", Type: "ほのお"},
pokemon.Stats{Atk: 52, Def: 43, Spe: 65}, // 攻撃・防御・素早さ
pokemon.HP{Max: 100, Cur: 100}, // HP
tackle, quickAttack, // 技
)
// プレイヤーBのポケモン:フシギダネ
b, _ := pokemon.NewPokemon(
pokemon.Species{Name: "フシギダネ", Type: "くさ"},
pokemon.Stats{Atk: 49, Def: 49, Spe: 45},
pokemon.HP{Max: 100, Cur: 100},
tackle,
)
fmt.Printf("A moves len=%d: %+v\n", a.Moves.Len(), a.Moves.All())
fmt.Printf("B moves len=%d: %+v\n", b.Moves.Len(), b.Moves.All())
fmt.Println("A choose index=1, B choose index=0")
// --- バトルを作成 ---
bt := &battle.Battle{
SideA: battle.Party{TrainerID: "プレイヤーA", Active: a},
SideB: battle.Party{TrainerID: "プレイヤーB", Active: b},
}
// --- 戦闘ルールサービスを注入 ---
sv := services.Services{
Order: services.SimpleOrder{}, // 行動順ルール(優先度→素早さ)
Accuracy: services.AlwaysHit{}, // 命中100%
Damage: services.SimpleDamage{}, // 簡易ダメージ計算
}
// --- 1ターン実行 ---
events := bt.TakeTurn(
battle.TurnCommand{
A: battle.MoveChoice{Index: 1}, // Aは「でんこうせっか」を選択
B: battle.MoveChoice{Index: 0}, // Bは「たいあたり」を選択
},
sv,
)
// --- 結果を出力 ---
for _, e := range events {
fmt.Println(e)
}
}
実行結果
~/develop/pokemon_ddd_v2 (feat/pattern1)$ go run cmd/battle_cli.go
A moves len=2: [{Name:たいあたり Type:ノーマル Power:40 Accuracy:1 Priority:0} {Name:でんこうせっか Type:ノーマル Power:40 Accuracy:1 Priority:1}]
B moves len=1: [{Name:たいあたり Type:ノーマル Power:40 Accuracy:1 Priority:0}]
A choose index=1, B choose index=0
Turn 1 start
ヒトカゲ の でんこうせっか → 3 ダメージ (相手HP: 97/100)
フシギダネ の たいあたり → 3 ダメージ (相手HP: 97/100)
どうでしたか??意味のある塊で固まっているのがいいですね。
DDDは最初実装するものが多いので初速は出ないんですが、徐々に速度が上がっていきます。
コードがそのまま仕様書になるので、だんだん良くなっていきます!

