はじめに
表題通り、ヘキサゴナルアーキテクチャでポケモンバトルを実装します。
今回はドメインのコアとなる部分を中心に実装していきます
要件
enititityの定義
- ポケモンの定義
• 各ポケモンは以下のステータスを持つ。
• 名前: ポケモンの名前を保持する(例: “ピカチュウ”)。
• HP: ポケモンのヒットポイント。0になると戦闘不能となる。
• 攻撃力: ダメージ計算のために使用される攻撃ステータス。
• 防御力: 受けるダメージを軽減するための防御ステータス。
• 素早さ: バトルでの行動順を決定するためのステータス。
• 技のリスト: ポケモンが使用できる技のリスト(複数の技を保持)。 - 技(Move)の定義
• 技は以下のプロパティを持つ。
• 名前: 技の名前(例: “10まんボルト”)。
• 威力: 技が持つ基礎ダメージ値。
• 命中率: 技が命中する確率。0~100%の範囲。
• タイプ: 技の属性(例: 電気、炎、草など)。
• 特殊効果(オプション): 特定の技が持つ追加効果(例: 相手をまひ状態にする)。 - 基本的な操作
• ポケモンは技を使用できる。
• ポケモンは自分の技リストから1つの技を選び、その技を相手に使用することができる。
• 技によるダメージ計算。
• 技を使用すると、攻撃力と技の威力、相手の防御力に基づいてダメージが計算される。
• 計算にはランダムな要素を追加し、クリティカルヒットや技の命中・失敗をシミュレーションする。
ユースケースの定義
1. ユースケース1: ポケモンの生成
• プレイヤーがポケモンを生成し、そのポケモンに名前とステータス(HP、攻撃力、防御力、素早さ)を設定する。
2. ユースケース2: 技の生成
• ポケモンに使わせる技を定義し、技の名前、威力、命中率、属性(タイプ)を設定する。
3. ユースケース3: 技の選択と使用
• ポケモンがバトル中に技を選択し、その技を相手に使用する。
• 技の効果によって相手のHPが減少する(ダメージ計算)。
設計
インターフェース
1. Pokemonインターフェース: ポケモンの基本的なステータスや操作を定義する。
• 名前、HP、攻撃力、防御力、素早さのゲッター/セッター。
• 技のリストの管理メソッド。
• 技を使用するメソッド(例: UseMove(move))。
2. Moveインターフェース: 技の属性やダメージ計算のロジックを定義する。
• 威力、命中率、属性のゲッター。
• ダメージ計算メソッド(例: CalculateDamage(attacker, defender))。
技術的な要件
• ポケモンと技のモデルは、コアロジックであり、外部のインフラストラクチャ(UIやデータベース)には依存しないこと。
• ポケモンや技のオブジェクトは、テストやシミュレーションで簡単に扱えるようにすること。
Step 1 のゴール
• ポケモンと技の基本クラス(または構造体)を定義し、ステータスや技を管理できる状態にする。
• 技の使用とダメージ計算ができるシンプルなメソッドを実装することで、バトルの基礎を作る。
ディレクトリ構成
.
├── cmd
│ └── main.go
├── go.mod
├── go.sum
├── internal
│ └── core
│ ├── battle.go
│ ├── move.go
│ └── pokemon.go
├── pokemon_battle.png
└── pokemon_battle.puml
4 directories, 8 files
ソースコード
package main
import (
"github.com/kouji0705/pokemon-battle/internal/core"
)
func main() {
// ポケモンの作成
pikachu := &core.Pokemon{
Name: "ピカチュウ",
HP: 100,
Attack: 55,
Defense: 40,
Speed: 90,
Moves: []core.Move{
*core.NewMove("10まんボルト", 90, 95, "でんき"),
},
}
charmander := &core.Pokemon{
Name: "ヒトカゲ",
HP: 100,
Attack: 52,
Defense: 43,
Speed: 65,
Moves: []core.Move{
*core.NewMove("かえんほうしゃ", 90, 95, "ほのお"),
},
}
// バトルの開始
battle := &core.Battle{Pokemon1: pikachu, Pokemon2: charmander}
result := battle.Start()
println(result)
}
package core
type Pokemon struct {
Name string
HP int
Attack int
Defense int
Speed int
Moves []Move // 使用可能な技のリスト
}
// ポケモンが技を使用し、相手にダメージを与える
func (p *Pokemon) AttackPokemon(target *Pokemon, move Move) (damage int, err error) {
if !move.CanHit() {
return 0, nil // 技が外れた場合
}
// ダメージ計算
damage = move.CalculateDamage(p, target)
target.HP -= damage
if target.HP < 0 {
target.HP = 0
}
return damage, nil
}
// ポケモンが戦闘不能かどうかを判定
func (p *Pokemon) IsFainted() bool {
return p.HP <= 0
}
注目すべきはAttackPokemonですね。ダメージ計算の細かい処理は技の実装に隠蔽します。
隠蔽することで、本来ここでダメージ計算や攻撃がヒットしたか等の条件分岐やビジネスロジックを書く必要がありません。
注目すべき振る舞いにだけ着眼するので、全体的な見通しが良くなります。
package core
import (
"math/rand"
"time"
)
type Move struct {
Name string
Power int // 技の威力
Accuracy int // 命中率(0-100%)
Type string // 技の属性(例: 電気、炎など)
randSource *rand.Rand // 乱数生成器を保持
}
// NewMove creates a new move and initializes the random source.
func NewMove(name string, power, accuracy int, moveType string) *Move {
return &Move{
Name: name,
Power: power,
Accuracy: accuracy,
Type: moveType,
randSource: rand.New(rand.NewSource(time.Now().UnixNano())), // 新しい乱数生成器を初期化
}
}
// CanHit checks if the move hits based on its accuracy.
func (m *Move) CanHit() bool {
// move に保持している乱数生成器を使って乱数を生成
return m.randSource.Intn(100) < m.Accuracy
}
// / 技(Move)のダメージを計算する
func (m *Move) CalculateDamage(attacker *Pokemon, defender *Pokemon) int {
// ダメージ計算式
damage := (attacker.Attack * m.Power) / defender.Defense
// ダメージにランダム要素を追加(0.85倍~1.0倍)
randomFactor := 0.85 + rand.Float64()*(1.0-0.85)
damage = int(float64(damage) * randomFactor)
if damage < 0 {
damage = 0
}
return damage
}
先ほど隠蔽していたCalculateDamageの実装をしていきます。
ダメージ計算の判定処理がまとまっているので、見やすいですね。
また、ランダム要素での急所攻撃、効果抜群や効果なし、やけどやまひ等の要素を追加する場合もここに追加すれば良さそうですね。
package core
// Battle represents a battle between two Pokemon.
type Battle struct {
Pokemon1 *Pokemon
Pokemon2 *Pokemon
}
// Start initiates the battle loop.
func (b *Battle) Start() string {
for {
// ポケモン1のターン
move := b.Pokemon1.Moves[0] // 簡略化のため最初の技を使用
damage, _ := b.Pokemon1.AttackPokemon(b.Pokemon2, move)
println(b.Pokemon1.Name, "技:", move.Name, ", ", damage, "ダメージ!")
if b.Pokemon2.IsFainted() {
return b.Pokemon1.Name + " 勝利!"
}
// ポケモン2のターン
move = b.Pokemon2.Moves[0] // 同様に、最初の技を使用
damage, _ = b.Pokemon2.AttackPokemon(b.Pokemon1, move)
println(b.Pokemon2.Name, "技:", move.Name, ", ", damage, "ダメージ!")
if b.Pokemon1.IsFainted() {
return b.Pokemon2.Name + " 勝利!"
}
}
}
実行結果
~/develop/pokemon_battle/cmd (feat/initial)$ go run main.go
ピカチュウ 技: 10まんボルト , 98 ダメージ!
ヒトカゲ 技: かえんほうしゃ , 107 ダメージ!
ヒトカゲ 勝利!
~/develop/pokemon_battle/cmd (feat/initial)$ go run main.go
ピカチュウ 技: 10まんボルト , 103 ダメージ!
ピカチュウ 勝利!
~/develop/pokemon_battle/cmd (feat/initial)$ go run main.go
ピカチュウ 技: 10まんボルト , 110 ダメージ!
ピカチュウ 勝利!
~/develop/pokemon_battle/cmd (feat/initial)$ go run main.go
ピカチュウ 技: 10まんボルト , 109 ダメージ!
ピカチュウ 勝利!
~/develop/pokemon_battle/cmd (feat/initial)$ go run main.go
ピカチュウ 技: 10まんボルト , 103 ダメージ!
ピカチュウ 勝利!
~/develop/pokemon_battle/cmd (feat/initial)$ go run main.go
ピカチュウ 技: 10まんボルト , 99 ダメージ!
ヒトカゲ 技: かえんほうしゃ , 102 ダメージ!
ヒトカゲ 勝利!
乱数生成を入れているので、実行のたびに結果が異なります。