【1日1プロダクト - コンウェイのGame of Life 】
物理シュミレーション系のゲームは楽しいですね。EdTech需要ありそう。
Vibe Coding by Claude Sonnet 4 , 所要時間5分
https://claude.ai/.../4f32fb58-014f-4697-bebe-1d0c37cb5749
Conway's Game of Life 完全実装ガイド - 3分で作る生命シミュレーション
はじめに
「たった4つのルールで宇宙が生まれる」
これがConway's Game of Lifeの魅力です。1970年にジョン・コンウェイが考案したこのシミュレーションは、シンプルなルールから驚くほど複雑で美しいパターンを生み出します。
🚀 Vibe Coding - AI時代の新しい開発スタイル
この記事は「Vibe Coding」の実践例です。Vibe Codingとは、Claude Sonnet 4のような高性能AIを活用して、アイデアから完成品まで数分で作り上げる新しい開発アプローチ。従来の「コーディング」から「AI協働による創造」へのパラダイムシフトを体現しています。
この記事で得られること
- ライフゲームの基本概念の理解
- AIとの協働で3分で完成したJavaScriptアプリ
- 効果的なプロンプトエンジニアリングの実例
- 生物学・数学・哲学的な楽しみ方の発見
- Vibe Codingによる爆速プロトタイピング手法
対象読者
- JavaScriptの基本文法を理解している方
- AI活用による効率的な開発に興味がある方
- 生命シミュレーションに興味がある方
- 創発現象について学びたい方
Conway's Game of Life とは?
4つのシンプルなルール
Conway's Game of Lifeは、2次元グリッド上のセル(細胞)が以下のルールに従って生存・死亡・誕生を繰り返すシミュレーションです:
- 生存:生きているセルで隣接する8マスに2~3個の生きたセルがある
- 誕生:死んでいるセルで隣接する8マスにちょうど3個の生きたセルがある
- 過疎死:生きているセルで隣接する生きたセルが1個以下
- 過密死:生きているセルで隣接する生きたセルが4個以上
なぜ「ゲーム」なのか?
実はプレイヤーが操作する普通のゲームではありません。「ゼロプレイヤーゲーム」と呼ばれ、初期状態を設定したら後は自動的に進行します。まさに「生命の法則」を観察するシミュレーションです。
Vibe Coding実践:3分で完成までの全プロセス
開発プロセス全体像
時間配分
- アイデア整理:30秒
- プロンプト設計:30秒
- AI実装:120秒
- 検証・調整:30秒
合計:3分で完成
実際に使用したプロンプト
Phase 1: 基本要求(最初のプロンプト)
Conway's Game of Lifeの完全なJavaScriptアプリを作ってください。
要件:
- インタラクティブなキャンバス(クリックでセル編集)
- 再生/一時停止機能
- 速度調整
- 有名パターンのプリセット(Glider、Glider Gun、Pulsar等)
- リアルタイム統計表示
- 美しいモダンなUI
Phase 2: UI改善(フォローアップ)
UIをもっと現代的にしてください:
- グラスモーフィズム効果
- グラデーション背景
- ホバーエフェクト
- 影とアニメーション
Phase 3: 機能拡張(最終調整)
以下の機能を追加してください:
- マウスドラッグでの連続描画
- 世代数と個体数のリアルタイム表示
- より多くのプリセットパターン
🎯 効果的なプロンプト設計のコツ
1. 具体的な要件を箇条書きで
❌ 悪い例:「ライフゲームを作って」
✅ 良い例:「インタラクティブなキャンバス(クリックでセル編集)」
2. 段階的な改善アプローチ
基本機能 → UI改善 → 機能拡張
一度に全てを求めず、段階的にブラッシュアップ
3. 技術的制約の明示
「HTMLファイル1つで完結」
「外部ライブラリなし」
「モダンブラウザ対応」
完成したアプリケーション
以下が実装されたアプリです:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Conway's Game of Life</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1e3c72, #2a5298);
color: white;
margin: 0;
padding: 20px;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
.container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
}
h1 {
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
button {
background: linear-gradient(45deg, #4CAF50, #45a049);
color: white;
border: none;
padding: 12px 24px;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4);
}
.preset-btn {
background: linear-gradient(45deg, #9c27b0, #7b1fa2);
padding: 8px 16px;
font-size: 14px;
box-shadow: 0 4px 15px rgba(156, 39, 176, 0.3);
}
canvas {
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 10px;
cursor: crosshair;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}
.stats {
text-align: center;
font-size: 18px;
margin-top: 15px;
}
</style>
</head>
<body>
<div class="container">
<h1>Conway's Game of Life</h1>
<div class="controls">
<button id="playPause">▶ Start</button>
<button id="step">Step</button>
<button id="clear">Clear</button>
<button id="random">Random</button>
<div>
<label for="speed">Speed:</label>
<input type="range" id="speed" min="1" max="20" value="10">
<span id="speedValue">10</span>
</div>
</div>
<div style="display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-bottom: 20px;">
<button class="preset-btn" data-preset="glider">Glider</button>
<button class="preset-btn" data-preset="gliderGun">Glider Gun</button>
<button class="preset-btn" data-preset="pulsar">Pulsar</button>
<button class="preset-btn" data-preset="pentomino">R-Pentomino</button>
<button class="preset-btn" data-preset="beacon">Beacon</button>
</div>
<div style="display: flex; justify-content: center; margin-bottom: 20px;">
<canvas id="gameCanvas" width="600" height="400"></canvas>
</div>
<div class="stats">
<div>Generation: <span id="generation">0</span> | Living Cells: <span id="population">0</span></div>
</div>
</div>
<script>
class GameOfLife {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.cellSize = 8;
this.cols = Math.floor(canvas.width / this.cellSize);
this.rows = Math.floor(canvas.height / this.cellSize);
this.grid = this.createGrid();
this.isRunning = false;
this.generation = 0;
this.speed = 10;
this.setupEventListeners();
this.draw();
}
createGrid() {
return Array(this.rows).fill().map(() => Array(this.cols).fill(false));
}
setupEventListeners() {
this.canvas.addEventListener('click', (e) => this.handleClick(e));
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
this.mouseDown = false;
this.canvas.addEventListener('mousedown', () => this.mouseDown = true);
this.canvas.addEventListener('mouseup', () => this.mouseDown = false);
}
handleClick(e) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const col = Math.floor(x / this.cellSize);
const row = Math.floor(y / this.cellSize);
if (row >= 0 && row < this.rows && col >= 0 && col < this.cols) {
this.grid[row][col] = !this.grid[row][col];
this.draw();
this.updateStats();
}
}
handleMouseMove(e) {
if (this.mouseDown) {
this.handleClick(e);
}
}
countNeighbors(row, col) {
let count = 0;
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
if (i === 0 && j === 0) continue;
const newRow = row + i;
const newCol = col + j;
if (newRow >= 0 && newRow < this.rows && newCol >= 0 && newCol < this.cols) {
if (this.grid[newRow][newCol]) count++;
}
}
}
return count;
}
nextGeneration() {
const newGrid = this.createGrid();
for (let row = 0; row < this.rows; row++) {
for (let col = 0; col < this.cols; col++) {
const neighbors = this.countNeighbors(row, col);
const currentCell = this.grid[row][col];
// Conway's Game of Life ルール
if (currentCell && (neighbors === 2 || neighbors === 3)) {
newGrid[row][col] = true; // 生存
} else if (!currentCell && neighbors === 3) {
newGrid[row][col] = true; // 誕生
}
// その他は死亡または死んだまま
}
}
this.grid = newGrid;
this.generation++;
this.draw();
this.updateStats();
}
draw() {
// 背景を黒で塗りつぶし
this.ctx.fillStyle = '#0a0a0a';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// グリッド線を描画
this.ctx.strokeStyle = '#333';
this.ctx.lineWidth = 0.5;
for (let i = 0; i <= this.cols; i++) {
this.ctx.beginPath();
this.ctx.moveTo(i * this.cellSize, 0);
this.ctx.lineTo(i * this.cellSize, this.canvas.height);
this.ctx.stroke();
}
for (let i = 0; i <= this.rows; i++) {
this.ctx.beginPath();
this.ctx.moveTo(0, i * this.cellSize);
this.ctx.lineTo(this.canvas.width, i * this.cellSize);
this.ctx.stroke();
}
// 生きているセルを緑で描画
this.ctx.fillStyle = '#00ff88';
for (let row = 0; row < this.rows; row++) {
for (let col = 0; col < this.cols; col++) {
if (this.grid[row][col]) {
this.ctx.fillRect(
col * this.cellSize + 1,
row * this.cellSize + 1,
this.cellSize - 2,
this.cellSize - 2
);
}
}
}
}
clear() {
this.grid = this.createGrid();
this.generation = 0;
this.draw();
this.updateStats();
}
random() {
for (let row = 0; row < this.rows; row++) {
for (let col = 0; col < this.cols; col++) {
this.grid[row][col] = Math.random() < 0.3;
}
}
this.generation = 0;
this.draw();
this.updateStats();
}
updateStats() {
const population = this.grid.flat().filter(cell => cell).length;
document.getElementById('generation').textContent = this.generation;
document.getElementById('population').textContent = population;
}
loadPreset(preset) {
this.clear();
const centerRow = Math.floor(this.rows / 2);
const centerCol = Math.floor(this.cols / 2);
switch (preset) {
case 'glider':
this.setPattern([
[0, 1, 0],
[0, 0, 1],
[1, 1, 1]
], centerRow - 10, centerCol - 10);
break;
case 'gliderGun':
this.setPattern([
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
[0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
[1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,1,0,0,0,0,0,0,0,0,1,0,0,0,1,0,1,1,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
], centerRow - 4, centerCol - 18);
break;
case 'pulsar':
this.setPattern([
[0,0,1,1,1,0,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,0,0,0,0,1,0,1,0,0,0,0,1],
[1,0,0,0,0,1,0,1,0,0,0,0,1],
[1,0,0,0,0,1,0,1,0,0,0,0,1],
[0,0,1,1,1,0,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,1,1,1,0,0,0,1,1,1,0,0],
[1,0,0,0,0,1,0,1,0,0,0,0,1],
[1,0,0,0,0,1,0,1,0,0,0,0,1],
[1,0,0,0,0,1,0,1,0,0,0,0,1],
[0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,1,1,1,0,0,0,1,1,1,0,0]
], centerRow - 6, centerCol - 6);
break;
case 'pentomino':
this.setPattern([
[0,1,1],
[1,1,0],
[0,1,0]
], centerRow, centerCol);
break;
case 'beacon':
this.setPattern([
[1,1,0,0],
[1,1,0,0],
[0,0,1,1],
[0,0,1,1]
], centerRow, centerCol);
break;
}
this.draw();
this.updateStats();
}
setPattern(pattern, startRow, startCol) {
for (let row = 0; row < pattern.length; row++) {
for (let col = 0; col < pattern[row].length; col++) {
const gridRow = startRow + row;
const gridCol = startCol + col;
if (gridRow >= 0 && gridRow < this.rows && gridCol >= 0 && gridCol < this.cols) {
this.grid[gridRow][gridCol] = pattern[row][col] === 1;
}
}
}
}
}
// ゲーム初期化
const canvas = document.getElementById('gameCanvas');
const game = new GameOfLife(canvas);
let animationId;
function gameLoop() {
game.nextGeneration();
const delay = 1100 - (game.speed * 50);
setTimeout(() => {
if (game.isRunning) {
animationId = requestAnimationFrame(gameLoop);
}
}, delay);
}
// イベントリスナー
document.getElementById('playPause').addEventListener('click', () => {
const btn = document.getElementById('playPause');
if (game.isRunning) {
game.isRunning = false;
btn.textContent = '▶ Start';
if (animationId) {
cancelAnimationFrame(animationId);
}
} else {
game.isRunning = true;
btn.textContent = '⏸ Pause';
gameLoop();
}
});
document.getElementById('step').addEventListener('click', () => {
game.nextGeneration();
});
document.getElementById('clear').addEventListener('click', () => {
game.clear();
});
document.getElementById('random').addEventListener('click', () => {
game.random();
});
document.getElementById('speed').addEventListener('input', (e) => {
game.speed = parseInt(e.target.value);
document.getElementById('speedValue').textContent = e.target.value;
});
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', () => {
game.loadPreset(btn.dataset.preset);
});
});
// 初期統計更新
game.updateStats();
</script>
</body>
</html>
実装のポイント
1. コア機能の実装
// 隣接セルのカウント(ライフゲームの心臓部)
countNeighbors(row, col) {
let count = 0;
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
if (i === 0 && j === 0) continue; // 自分自身は除外
const newRow = row + i;
const newCol = col + j;
if (newRow >= 0 && newRow < this.rows && newCol >= 0 && newCol < this.cols) {
if (this.grid[newRow][newCol]) count++;
}
}
}
return count;
}
2. ルールの実装
// Conway's Game of Life の4つのルール
if (currentCell && (neighbors === 2 || neighbors === 3)) {
newGrid[row][col] = true; // 生存
} else if (!currentCell && neighbors === 3) {
newGrid[row][col] = true; // 誕生
}
// その他は死亡(過疎死・過密死)
3. 有名パターンの実装
- Glider: 斜めに移動する最小の移動パターン
- Glider Gun: 30世代周期でGliderを無限生成
- Pulsar: 美しい周期3の振動子
- R-Pentomino: 1103世代で安定化する「メトセラパターン」
実際に体験してみよう
基本的な使い方
-
観察から始める
- 「Glider」ボタンを押して小さな生き物が歩く様子を観察
- 「Pulsar」で美しい振動パターンを体験
-
創作してみる
- キャンバスをクリックして自分だけのパターンを描画
- 「Start」ボタンでどんな進化をするか確認
-
実験してみる
- 「Random」で生命の偶然性を体験
- 「Speed」で時間の流れを調整
特に注目すべきパターン
Glider Gun(グライダー銃)
無限にGliderを生成し続ける驚異的なパターン
30世代周期で安定しながら永続的に「子供」を産む
まさに「生命の源泉」を表現
R-Pentomino
わずか5個のセルから1103世代後に安定
カオス理論の「初期条件への敏感依存性」を体現
「小さな変化が大きな結果を生む」ことの具体例
多角的な楽しみ方
生物学的視点での観察
細胞生物学のシミュレーション
- パターンの分裂 → 細胞分裂のメタファー
- 過密死 → 接触阻害現象
- Glider Gun → 幹細胞の概念
発生生物学の可視化
- R-Pentominoの初期成長 → 胚発生の初期段階
- 複雑なパターンの創発 → モルフォジェネシス
数学的意義の理解
計算理論
- チューリング完全性:理論上あらゆる計算が可能
- PSPACE完全:予測問題の計算複雑性
創発現象
- 単純なルールから複雑なパターンが「創発」
- 階層構造:セル → パターン → メタパターン
哲学的思考の材料
決定論と予測可能性
- 完全に決定的だが予測困難
- 「運命は決まっているが分からない」という人生観
存在論的な問い
- セルは「生きている」のか?
- パターンに「意味」はあるのか?
Vibe Codingのメリット・デメリット
🚀 メリット
1. 爆速プロトタイピング
- アイデアから動作アプリまで3分
- 従来の開発時間を1/10に短縮
- 即座に検証・フィードバック取得可能
2. 高品質なコード生成
- ベストプラクティスを自動適用
- バグの少ない安定したコード
- モダンな技術スタックの活用
3. 学習効果
- AIが生成したコードから学習
- 新しい実装パターンの発見
- 技術的視野の拡大
⚠️ 注意点・課題
1. プロンプト設計スキルが必要
- 適切な要件定義能力
- 技術的な理解
- 段階的な改善アプローチ
2. コードの理解・保守性
- 生成されたコードの内容理解必須
- 後からの修正・拡張時の考慮
- チーム開発での知識共有
3. 創造性とのバランス
- AIに依存しすぎない思考
- 独自性のあるアイデア創出
- 技術的なチャレンジ精神の維持
🎯 Vibe Coding成功の秘訣
1. 明確な要件定義
- 何を作りたいか(機能要件)
- どう動作させたいか(動作要件)
- どう見せたいか(UI/UX要件)
- どんな技術で作るか(技術要件)
2. 段階的アプローチ
MVP作成 → 機能追加 → UI改善 → 最適化
一度に完璧を求めず、反復的に改善
3. コードレビューの習慣
- 生成されたコードを必ず理解
- セキュリティ観点でのチェック
- パフォーマンス観点での検証
- 保守性の確認
パフォーマンス最適化のコツ
大規模グリッドでの最適化
1. 境界チェックの改善
// 境界を予め設定して計算量を削減
const safeCountNeighbors = (row, col, minRow, maxRow, minCol, maxCol) => {
// 実装詳細...
};
2. スパース表現の採用
// 生きているセルのみを記録
class SparseGrid {
constructor() {
this.livingCells = new Set();
}
// 実装詳細...
}
3. WebWorkerの活用
// バックグラウンドでの計算
const worker = new Worker('life-worker.js');
worker.postMessage({grid: currentGrid});
まとめ
Conway's Game of Lifeは、「シンプルなルールから複雑な美しさが生まれる」という自然界の根本原理を体験できる素晴らしいシミュレーションです。そしてVibe Codingによって、この複雑なアプリケーションを3分で実装できることを実証しました。
🎓 学習できること
プログラミングスキル
- 2次元配列操作、アニメーション制御
- AIとの協働による効率的な開発手法
学際的知識
- 数学:創発現象、計算理論、複雑系科学
- 生物学:細胞動態、個体群生態学、進化過程
- 哲学:決定論、存在論、生命の定義
AI時代のスキル
- 効果的なプロンプトエンジニアリング
- AIとの協働による価値創造
- 従来開発との使い分け判断
🚀 Vibe Codingの未来
個人開発者にとって
- アイデアの即座な検証
- 学習効率の飛躍的向上
- 創造的作業への集中
チーム開発にとって
- プロトタイピング工程の革新
- 技術検証の高速化
- ドキュメント・教材作成の効率化
技術教育にとって
- 実践的な学習体験の提供
- 複雑な概念の可視化
- 学習者の興味喚起
🎯 次のステップ
技術的発展
- ルールをカスタマイズして独自の「生命」を作る
- 3次元版やより複雑な近傍ルールに挑戦
- 機械学習で最適なパターンを探索
- 他のセルオートマトン(Langton's Ant等)を実装
Vibe Coding スキル向上
- より複雑なアプリケーションでの実践
- プロンプトパターンの体系化
- AIとの協働ワークフローの確立
- コード品質管理手法の習得
ぜひ実際にアプリを動かして、デジタル生命の不思議な世界を探求してみてください。そしてVibe Codingで、あなた自身の創造的なアイデアを3分で形にしてみてください。きっと「開発」に対する新しい視点が得られるはずです。