1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ChatGPT/Claude活用術:Conway's Game of Life の人工生命を3分で実装する方法

1
Posted at

【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次元グリッド上のセル(細胞)が以下のルールに従って生存・死亡・誕生を繰り返すシミュレーションです:

  1. 生存:生きているセルで隣接する8マスに2~3個の生きたセルがある
  2. 誕生:死んでいるセルで隣接する8マスにちょうど3個の生きたセルがある
  3. 過疎死:生きているセルで隣接する生きたセルが1個以下
  4. 過密死:生きているセルで隣接する生きたセルが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世代で安定化する「メトセラパターン」

実際に体験してみよう

基本的な使い方

  1. 観察から始める

    • 「Glider」ボタンを押して小さな生き物が歩く様子を観察
    • 「Pulsar」で美しい振動パターンを体験
  2. 創作してみる

    • キャンバスをクリックして自分だけのパターンを描画
    • 「Start」ボタンでどんな進化をするか確認
  3. 実験してみる

    • 「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の未来

個人開発者にとって

  • アイデアの即座な検証
  • 学習効率の飛躍的向上
  • 創造的作業への集中

チーム開発にとって

  • プロトタイピング工程の革新
  • 技術検証の高速化
  • ドキュメント・教材作成の効率化

技術教育にとって

  • 実践的な学習体験の提供
  • 複雑な概念の可視化
  • 学習者の興味喚起

🎯 次のステップ

技術的発展

  1. ルールをカスタマイズして独自の「生命」を作る
  2. 3次元版やより複雑な近傍ルールに挑戦
  3. 機械学習で最適なパターンを探索
  4. 他のセルオートマトン(Langton's Ant等)を実装

Vibe Coding スキル向上

  1. より複雑なアプリケーションでの実践
  2. プロンプトパターンの体系化
  3. AIとの協働ワークフローの確立
  4. コード品質管理手法の習得

ぜひ実際にアプリを動かして、デジタル生命の不思議な世界を探求してみてください。そしてVibe Codingで、あなた自身の創造的なアイデアを3分で形にしてみてください。きっと「開発」に対する新しい視点が得られるはずです。

🔗 参考資料


1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?