第19章: Wa-Tor シミュレーション¶
はじめに¶
本章では、Wa-Tor(Water Torus)シミュレーションを実装します。これは魚とサメの捕食-被食関係をシミュレートするセルラーオートマトンです。関数型のアプローチで、不変データ構造と純粋関数を使って実装します。
1. データ構造¶
/// 座標
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Position {
pub x: i32,
pub y: i32,
}
impl Position {
pub fn new(x: i32, y: i32) -> Position {
Position { x, y }
}
pub fn wrap(&self, width: i32, height: i32) -> Position {
Position {
x: ((self.x % width) + width) % width,
y: ((self.y % height) + height) % height,
}
}
}
/// 生物の種類
#[derive(Debug, Clone, PartialEq)]
pub enum CreatureType {
Fish,
Shark,
}
/// 生物
#[derive(Debug, Clone)]
pub struct Creature {
pub creature_type: CreatureType,
pub position: Position,
pub energy: i32, // サメのエネルギー
pub breed_time: i32, // 繁殖までの時間
}
impl Creature {
pub fn fish(position: Position, breed_time: i32) -> Creature {
Creature {
creature_type: CreatureType::Fish,
position,
energy: 0,
breed_time,
}
}
pub fn shark(position: Position, energy: i32, breed_time: i32) -> Creature {
Creature {
creature_type: CreatureType::Shark,
position,
energy,
breed_time,
}
}
}
/// ワールド
#[derive(Debug, Clone)]
pub struct World {
pub width: i32,
pub height: i32,
pub creatures: Vec<Creature>,
pub fish_breed_time: i32,
pub shark_breed_time: i32,
pub shark_energy_gain: i32,
pub shark_initial_energy: i32,
}
2. 移動ロジック¶
impl World {
/// 隣接セルを取得
pub fn neighbors(&self, pos: Position) -> Vec<Position> {
let directions = vec![
Position::new(0, -1), // 上
Position::new(0, 1), // 下
Position::new(-1, 0), // 左
Position::new(1, 0), // 右
];
directions
.into_iter()
.map(|d| Position::new(pos.x + d.x, pos.y + d.y).wrap(self.width, self.height))
.collect()
}
/// 空のセルを探す
pub fn find_empty_neighbors(&self, pos: Position) -> Vec<Position> {
self.neighbors(pos)
.into_iter()
.filter(|p| !self.creatures.iter().any(|c| c.position == *p))
.collect()
}
/// 魚がいるセルを探す
pub fn find_fish_neighbors(&self, pos: Position) -> Vec<Position> {
self.neighbors(pos)
.into_iter()
.filter(|p| {
self.creatures
.iter()
.any(|c| c.position == *p && c.creature_type == CreatureType::Fish)
})
.collect()
}
}
3. シミュレーションステップ¶
impl World {
/// 魚を移動
fn move_fish(&self, fish: &Creature, rng: &mut impl Rng) -> (Option<Creature>, Option<Creature>) {
let empty = self.find_empty_neighbors(fish.position);
if empty.is_empty() {
// 移動できない
let new_fish = Creature {
breed_time: fish.breed_time - 1,
..fish.clone()
};
(Some(new_fish), None)
} else {
// ランダムに移動
let new_pos = empty[rng.gen_range(0..empty.len())];
if fish.breed_time <= 1 {
// 繁殖
let parent = Creature::fish(new_pos, self.fish_breed_time);
let child = Creature::fish(fish.position, self.fish_breed_time);
(Some(parent), Some(child))
} else {
let new_fish = Creature {
position: new_pos,
breed_time: fish.breed_time - 1,
..fish.clone()
};
(Some(new_fish), None)
}
}
}
/// サメを移動
fn move_shark(&self, shark: &Creature, rng: &mut impl Rng) -> (Option<Creature>, Option<Creature>, Option<Position>) {
let fish_neighbors = self.find_fish_neighbors(shark.position);
let empty_neighbors = self.find_empty_neighbors(shark.position);
// エネルギーがなくなったら死亡
if shark.energy <= 0 {
return (None, None, None);
}
if !fish_neighbors.is_empty() {
// 魚を食べる
let target = fish_neighbors[rng.gen_range(0..fish_neighbors.len())];
let new_energy = shark.energy + self.shark_energy_gain;
if shark.breed_time <= 1 {
// 繁殖
let parent = Creature::shark(target, new_energy - 1, self.shark_breed_time);
let child = Creature::shark(shark.position, self.shark_initial_energy, self.shark_breed_time);
(Some(parent), Some(child), Some(target))
} else {
let new_shark = Creature {
position: target,
energy: new_energy - 1,
breed_time: shark.breed_time - 1,
..shark.clone()
};
(Some(new_shark), None, Some(target))
}
} else if !empty_neighbors.is_empty() {
// 空のセルに移動
let new_pos = empty_neighbors[rng.gen_range(0..empty_neighbors.len())];
if shark.breed_time <= 1 {
let parent = Creature::shark(new_pos, shark.energy - 1, self.shark_breed_time);
let child = Creature::shark(shark.position, self.shark_initial_energy, self.shark_breed_time);
(Some(parent), Some(child), None)
} else {
let new_shark = Creature {
position: new_pos,
energy: shark.energy - 1,
breed_time: shark.breed_time - 1,
..shark.clone()
};
(Some(new_shark), None, None)
}
} else {
// 移動できない
let new_shark = Creature {
energy: shark.energy - 1,
breed_time: shark.breed_time - 1,
..shark.clone()
};
(Some(new_shark), None, None)
}
}
/// 1ステップ実行
pub fn step(&self, rng: &mut impl Rng) -> World {
// 実装省略(全生物を順番に処理)
self.clone()
}
}
4. シミュレーション実行¶
/// シミュレーション結果
#[derive(Debug, Clone)]
pub struct SimulationStats {
pub step: usize,
pub fish_count: usize,
pub shark_count: usize,
}
/// シミュレーションを実行
pub fn run_simulation(
initial_world: World,
steps: usize,
mut rng: impl Rng,
) -> Vec<SimulationStats> {
let mut world = initial_world;
let mut stats = Vec::new();
for step in 0..steps {
let fish_count = world.creatures
.iter()
.filter(|c| c.creature_type == CreatureType::Fish)
.count();
let shark_count = world.creatures
.iter()
.filter(|c| c.creature_type == CreatureType::Shark)
.count();
stats.push(SimulationStats {
step,
fish_count,
shark_count,
});
world = world.step(&mut rng);
}
stats
}
5. 可視化¶
impl World {
/// ASCII アートで表示
pub fn to_ascii(&self) -> String {
let mut grid = vec![vec!['.'; self.width as usize]; self.height as usize];
for creature in &self.creatures {
let ch = match creature.creature_type {
CreatureType::Fish => 'f',
CreatureType::Shark => 'S',
};
grid[creature.position.y as usize][creature.position.x as usize] = ch;
}
grid.into_iter()
.map(|row| row.into_iter().collect::<String>())
.collect::<Vec<_>>()
.join("\n")
}
}
6. パターンの適用¶
- 不変データ: World, Creature は不変
- 純粋関数: step() は新しい World を返す
- ADT: CreatureType による生物種別
- イテレータ: 効率的な集計処理
まとめ¶
本章では、Wa-Tor シミュレーションを通じて:
- 不変データ構造によるシミュレーション状態管理
- 純粋関数による状態遷移
- ADT による生物種別の表現
- イテレータによる効率的なデータ処理
を学びました。関数型のアプローチにより、再現性があり、テストしやすいシミュレーションが実現できます。
参考コード¶
- ソースコード:
apps/rust/part6/src/chapter19.rs
次章予告¶
次章では、パターン間の相互作用について学びます。複数のパターンを組み合わせた設計を探ります。