Claude Opus4.6 でコーディングを試す.


以前の投稿で, Claude Opus 4.5でのコーディング能力を試すためにWeasel Program (Dawkins, 1986) を組んだ. 今回は, さらにその最新版であるClaude Opus 4.6でコーディング能力を試してみたい.

プロンプトと出力結果は以下の通りである.

Weasel Program を可能な限りリッチな表現を用いたコードで書いて.
その過程をアニメーションとして出力できるようにして.
import { useState, useEffect, useRef, useCallback } from "react";

const TARGET = "METHINKS IT IS LIKE A WEASEL";
const CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
const MUTATION_RATE = 0.04;
const OFFSPRING_COUNT = 100;

function randomChar() {
  return CHARSET[Math.floor(Math.random() * CHARSET.length)];
}

function randomString(len) {
  return Array.from({ length: len }, () => randomChar()).join("");
}

function fitness(str) {
  let score = 0;
  for (let i = 0; i < str.length; i++) {
    if (str[i] === TARGET[i]) score++;
  }
  return score;
}

function mutate(parent, rate) {
  return parent
    .split("")
    .map((ch) => (Math.random() < rate ? randomChar() : ch))
    .join("");
}

function evolveStep(parent) {
  let bestChild = parent;
  let bestFit = fitness(parent);
  for (let i = 0; i < OFFSPRING_COUNT; i++) {
    const child = mutate(parent, MUTATION_RATE);
    const f = fitness(child);
    if (f >= bestFit) {
      bestFit = f;
      bestChild = child;
    }
  }
  return { best: bestChild, fitness: bestFit };
}

// DNA helix decorative component
function DNAHelix({ height = 200 }) {
  const steps = 20;
  return (
    <svg width="60" height={height} viewBox={`0 0 60 ${height}`} style={{ opacity: 0.15 }}>
      {Array.from({ length: steps }).map((_, i) => {
        const y = (i / steps) * height;
        const x1 = 30 + Math.sin((i / steps) * Math.PI * 4) * 20;
        const x2 = 30 - Math.sin((i / steps) * Math.PI * 4) * 20;
        return (
          <g key={i}>
            <circle cx={x1} cy={y} r="3" fill="#00ffc8" />
            <circle cx={x2} cy={y} r="3" fill="#ff6b9d" />
            {i % 2 === 0 && (
              <line x1={x1} y1={y} x2={x2} y2={y} stroke="#ffffff" strokeWidth="0.5" opacity="0.3" />
            )}
          </g>
        );
      })}
    </svg>
  );
}

export default function WeaselProgram() {
  const [state, setState] = useState("idle"); // idle | running | complete
  const [current, setCurrent] = useState("");
  const [generation, setGeneration] = useState(0);
  const [currentFitness, setCurrentFitness] = useState(0);
  const [history, setHistory] = useState([]);
  const [speed, setSpeed] = useState(60);
  const [charStates, setCharStates] = useState([]);
  const [particles, setParticles] = useState([]);
  const intervalRef = useRef(null);
  const parentRef = useRef("");
  const genRef = useRef(0);
  const historyRef = useRef([]);
  const particleId = useRef(0);

  const spawnParticles = useCallback((indices) => {
    const newParticles = indices.map((idx) => ({
      id: particleId.current++,
      x: idx * 26 + 13,
      born: Date.now(),
    }));
    setParticles((prev) => [...prev.slice(-30), ...newParticles]);
  }, []);

  const start = useCallback(() => {
    const initial = randomString(TARGET.length);
    parentRef.current = initial;
    genRef.current = 0;
    historyRef.current = [{ gen: 0, str: initial, fitness: fitness(initial) }];

    setCurrent(initial);
    setGeneration(0);
    setCurrentFitness(fitness(initial));
    setHistory(historyRef.current);
    setState("running");
    setParticles([]);

    setCharStates(
      initial.split("").map((ch, i) => ({
        char: ch,
        matched: ch === TARGET[i],
        justMatched: false,
      }))
    );
  }, []);

  useEffect(() => {
    if (state !== "running") {
      if (intervalRef.current) clearInterval(intervalRef.current);
      return;
    }

    intervalRef.current = setInterval(() => {
      const prev = parentRef.current;
      const { best, fitness: fit } = evolveStep(prev);

      const newlyMatched = [];
      const chars = best.split("").map((ch, i) => {
        const wasMatched = prev[i] === TARGET[i];
        const isMatched = ch === TARGET[i];
        if (isMatched && !wasMatched) newlyMatched.push(i);
        return { char: ch, matched: isMatched, justMatched: isMatched && !wasMatched };
      });

      if (newlyMatched.length > 0) spawnParticles(newlyMatched);

      parentRef.current = best;
      genRef.current += 1;

      if (genRef.current % 1 === 0 || fit === TARGET.length) {
        historyRef.current = [
          ...historyRef.current,
          { gen: genRef.current, str: best, fitness: fit },
        ];
      }

      setCurrent(best);
      setGeneration(genRef.current);
      setCurrentFitness(fit);
      setHistory([...historyRef.current]);
      setCharStates(chars);

      if (fit === TARGET.length) {
        clearInterval(intervalRef.current);
        setState("complete");
      }
    }, Math.max(10, 200 - speed * 2));

    return () => clearInterval(intervalRef.current);
  }, [state, speed, spawnParticles]);

  // Clean old particles
  useEffect(() => {
    const timer = setInterval(() => {
      setParticles((prev) => prev.filter((p) => Date.now() - p.born < 1500));
    }, 500);
    return () => clearInterval(timer);
  }, []);

  const progressPct = (currentFitness / TARGET.length) * 100;

  const fitnessData = history.map((h) => ({
    gen: h.gen,
    pct: (h.fitness / TARGET.length) * 100,
  }));

  return (
    <div style={{
      minHeight: "100vh",
      background: "linear-gradient(160deg, #0a0a0f 0%, #0d1117 30%, #0f0a1a 60%, #0a0a0f 100%)",
      color: "#e0e0e0",
      fontFamily: "'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace",
      overflow: "hidden",
      position: "relative",
    }}>
      {/* Animated background grid */}
      <div style={{
        position: "fixed", inset: 0, zIndex: 0, opacity: 0.03,
        backgroundImage: `
          linear-gradient(rgba(0,255,200,0.3) 1px, transparent 1px),
          linear-gradient(90deg, rgba(0,255,200,0.3) 1px, transparent 1px)
        `,
        backgroundSize: "40px 40px",
      }} />

      {/* Floating DNA helices */}
      <div style={{ position: "fixed", top: 40, left: 20, zIndex: 0 }}>
        <DNAHelix height={600} />
      </div>
      <div style={{ position: "fixed", top: 100, right: 30, zIndex: 0 }}>
        <DNAHelix height={500} />
      </div>

      <div style={{ position: "relative", zIndex: 1, maxWidth: 900, margin: "0 auto", padding: "40px 20px" }}>
        {/* Header */}
        <div style={{ textAlign: "center", marginBottom: 48 }}>
          <div style={{
            fontSize: 11, letterSpacing: 6, color: "#00ffc8", marginBottom: 12,
            textTransform: "uppercase", fontWeight: 600,
          }}>
            Richard Dawkins' Evolutionary Algorithm
          </div>
          <h1 style={{
            fontSize: "clamp(28px, 5vw, 52px)", fontWeight: 800, margin: 0,
            background: "linear-gradient(135deg, #00ffc8 0%, #7b68ee 40%, #ff6b9d 80%, #ffd700 100%)",
            WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent",
            letterSpacing: -1, lineHeight: 1.1,
          }}>
            Weasel Program
          </h1>
          <p style={{ color: "#666", fontSize: 13, marginTop: 12, maxWidth: 500, margin: "12px auto 0", lineHeight: 1.6 }}>
            Cumulative selection demonstrates how non-random selection of random variation can produce complex results — far faster than pure chance.
          </p>
        </div>

        {/* Target display */}
        <div style={{
          textAlign: "center", marginBottom: 32, padding: "16px 24px",
          background: "rgba(0,255,200,0.03)", borderRadius: 12,
          border: "1px solid rgba(0,255,200,0.08)",
        }}>
          <div style={{ fontSize: 10, letterSpacing: 4, color: "#555", marginBottom: 8, textTransform: "uppercase" }}>
            Target Phenotype
          </div>
          <div style={{ fontSize: "clamp(14px, 2.5vw, 22px)", letterSpacing: 4, color: "#00ffc8", fontWeight: 700 }}>
            {TARGET}
          </div>
        </div>

        {/* Main evolution display */}
        <div style={{
          background: "rgba(255,255,255,0.02)",
          border: "1px solid rgba(255,255,255,0.06)",
          borderRadius: 16, padding: "32px 24px", marginBottom: 24,
          position: "relative", overflow: "hidden",
          backdropFilter: "blur(10px)",
          boxShadow: state === "complete"
            ? "0 0 60px rgba(0,255,200,0.15), inset 0 0 60px rgba(0,255,200,0.03)"
            : "0 4px 40px rgba(0,0,0,0.3)",
          transition: "box-shadow 0.8s ease",
        }}>
          {/* Particle effects */}
          {particles.map((p) => {
            const age = (Date.now() - p.born) / 1500;
            return (
              <div key={p.id} style={{
                position: "absolute",
                left: `calc(50% - ${(TARGET.length * 26) / 2}px + ${p.x}px)`,
                top: "50%",
                width: 4, height: 4, borderRadius: "50%",
                background: "#00ffc8",
                opacity: Math.max(0, 1 - age),
                transform: `translateY(${-age * 60}px) scale(${1 + age * 2})`,
                boxShadow: "0 0 12px #00ffc8",
                pointerEvents: "none",
                transition: "none",
              }} />
            );
          })}

          <div style={{ fontSize: 10, letterSpacing: 3, color: "#555", marginBottom: 16, textTransform: "uppercase", textAlign: "center" }}>
            Current Genome — Generation {generation}
          </div>

          {/* Character display */}
          <div style={{
            display: "flex", justifyContent: "center", flexWrap: "wrap",
            gap: 4, marginBottom: 24,
          }}>
            {(charStates.length > 0 ? charStates : TARGET.split("").map((ch) => ({ char: " ", matched: false, justMatched: false }))).map((cs, i) => (
              <div key={i} style={{
                width: 26, height: 36,
                display: "flex", alignItems: "center", justifyContent: "center",
                fontSize: 16, fontWeight: 700,
                borderRadius: 4,
                background: cs.matched
                  ? "rgba(0,255,200,0.15)"
                  : "rgba(255,255,255,0.03)",
                border: cs.matched
                  ? "1px solid rgba(0,255,200,0.4)"
                  : "1px solid rgba(255,255,255,0.06)",
                color: cs.matched ? "#00ffc8" : "#666",
                transition: "all 0.3s ease",
                animation: cs.justMatched ? "pop 0.4s ease" : "none",
                boxShadow: cs.justMatched ? "0 0 20px rgba(0,255,200,0.5)" : "none",
              }}>
                {cs.char}
              </div>
            ))}
          </div>

          {/* Progress bar */}
          <div style={{ marginBottom: 16 }}>
            <div style={{
              display: "flex", justifyContent: "space-between", fontSize: 11, color: "#555", marginBottom: 6,
            }}>
              <span>Fitness</span>
              <span>{currentFitness}/{TARGET.length} ({progressPct.toFixed(1)}%)</span>
            </div>
            <div style={{
              height: 6, borderRadius: 3,
              background: "rgba(255,255,255,0.05)",
              overflow: "hidden",
            }}>
              <div style={{
                height: "100%", borderRadius: 3,
                width: `${progressPct}%`,
                background: progressPct === 100
                  ? "linear-gradient(90deg, #00ffc8, #7b68ee, #ff6b9d)"
                  : "linear-gradient(90deg, #00ffc8, #7b68ee)",
                transition: "width 0.3s ease",
                boxShadow: "0 0 12px rgba(0,255,200,0.3)",
              }} />
            </div>
          </div>

          {/* Stats row */}
          <div style={{ display: "flex", justifyContent: "center", gap: 32, fontSize: 12 }}>
            {[
              { label: "Generation", value: generation },
              { label: "Mutation Rate", value: `${(MUTATION_RATE * 100).toFixed(0)}%` },
              { label: "Offspring/Gen", value: OFFSPRING_COUNT },
            ].map(({ label, value }) => (
              <div key={label} style={{ textAlign: "center" }}>
                <div style={{ color: "#444", fontSize: 10, letterSpacing: 2, textTransform: "uppercase", marginBottom: 4 }}>
                  {label}
                </div>
                <div style={{ color: "#aaa", fontWeight: 700, fontSize: 18 }}>{value}</div>
              </div>
            ))}
          </div>
        </div>

        {/* Fitness graph */}
        {fitnessData.length > 1 && (
          <div style={{
            background: "rgba(255,255,255,0.02)",
            border: "1px solid rgba(255,255,255,0.06)",
            borderRadius: 16, padding: "24px", marginBottom: 24,
          }}>
            <div style={{ fontSize: 10, letterSpacing: 3, color: "#555", marginBottom: 16, textTransform: "uppercase" }}>
              Fitness Over Generations
            </div>
            <svg width="100%" height="120" viewBox={`0 0 ${Math.max(fitnessData.length * 3, 200)} 120`} preserveAspectRatio="none" style={{ display: "block" }}>
              <defs>
                <linearGradient id="graphGrad" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="0%" stopColor="#00ffc8" stopOpacity="0.3" />
                  <stop offset="100%" stopColor="#00ffc8" stopOpacity="0" />
                </linearGradient>
              </defs>
              {/* Area fill */}
              <path
                d={
                  `M0,120 ` +
                  fitnessData.map((d, i) => {
                    const x = (i / Math.max(fitnessData.length - 1, 1)) * Math.max(fitnessData.length * 3, 200);
                    const y = 120 - (d.pct / 100) * 110;
                    return `L${x},${y}`;
                  }).join(" ") +
                  ` L${Math.max(fitnessData.length * 3, 200)},120 Z`
                }
                fill="url(#graphGrad)"
              />
              {/* Line */}
              <path
                d={fitnessData.map((d, i) => {
                  const x = (i / Math.max(fitnessData.length - 1, 1)) * Math.max(fitnessData.length * 3, 200);
                  const y = 120 - (d.pct / 100) * 110;
                  return `${i === 0 ? "M" : "L"}${x},${y}`;
                }).join(" ")}
                fill="none" stroke="#00ffc8" strokeWidth="2"
              />
            </svg>
          </div>
        )}

        {/* Evolution log */}
        {history.length > 0 && (
          <div style={{
            background: "rgba(255,255,255,0.02)",
            border: "1px solid rgba(255,255,255,0.06)",
            borderRadius: 16, padding: "24px", marginBottom: 24,
            maxHeight: 280, overflowY: "auto",
          }}>
            <div style={{ fontSize: 10, letterSpacing: 3, color: "#555", marginBottom: 12, textTransform: "uppercase" }}>
              Evolution Log
            </div>
            <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
              {history.slice(-40).map((h, idx) => (
                <div key={idx} style={{
                  display: "flex", gap: 16, alignItems: "center",
                  fontSize: 12, padding: "4px 8px", borderRadius: 4,
                  background: h.fitness === TARGET.length ? "rgba(0,255,200,0.05)" : "transparent",
                }}>
                  <span style={{ color: "#444", minWidth: 60, fontSize: 10 }}>
                    Gen {String(h.gen).padStart(4, "0")}
                  </span>
                  <span style={{ letterSpacing: 2, flex: 1 }}>
                    {h.str.split("").map((ch, ci) => (
                      <span key={ci} style={{
                        color: ch === TARGET[ci] ? "#00ffc8" : "#3a3a4a",
                      }}>
                        {ch}
                      </span>
                    ))}
                  </span>
                  <span style={{ color: "#555", fontSize: 10, minWidth: 40, textAlign: "right" }}>
                    {((h.fitness / TARGET.length) * 100).toFixed(0)}%
                  </span>
                </div>
              ))}
            </div>
          </div>
        )}

        {/* Controls */}
        <div style={{
          display: "flex", gap: 12, justifyContent: "center", alignItems: "center", flexWrap: "wrap",
        }}>
          <button
            onClick={start}
            style={{
              padding: "12px 32px", fontSize: 13, fontWeight: 700,
              fontFamily: "inherit", letterSpacing: 2, textTransform: "uppercase",
              border: "none", borderRadius: 8, cursor: "pointer",
              background: state === "complete"
                ? "linear-gradient(135deg, #ffd700, #ff6b9d)"
                : "linear-gradient(135deg, #00ffc8, #7b68ee)",
              color: "#0a0a0f",
              boxShadow: "0 4px 20px rgba(0,255,200,0.2)",
              transition: "all 0.3s ease",
            }}
          >
            {state === "idle" ? "▶ Begin Evolution" : state === "running" ? "⟳ Restart" : "✦ Evolve Again"}
          </button>

          {state === "running" && (
            <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
              <span style={{ fontSize: 10, color: "#555", letterSpacing: 2, textTransform: "uppercase" }}>Speed</span>
              <input
                type="range" min="10" max="100" value={speed}
                onChange={(e) => setSpeed(Number(e.target.value))}
                style={{ width: 100, accentColor: "#00ffc8" }}
              />
            </div>
          )}
        </div>

        {/* Completion celebration */}
        {state === "complete" && (
          <div style={{
            textAlign: "center", marginTop: 32, padding: 24,
            animation: "fadeIn 0.6s ease",
          }}>
            <div style={{
              fontSize: 32, marginBottom: 8,
              background: "linear-gradient(135deg, #ffd700, #ff6b9d, #00ffc8)",
              WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent",
              fontWeight: 800,
            }}>
              Evolution Complete
            </div>
            <p style={{ color: "#555", fontSize: 13 }}>
              Target reached in <strong style={{ color: "#00ffc8" }}>{generation}</strong> generations.
              Pure chance would require ~27<sup>{TARGET.length}</sup> attempts.
            </p>
          </div>
        )}
      </div>

      <style>{`
        @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700;800&display=swap');
        @keyframes pop {
          0% { transform: scale(1); }
          50% { transform: scale(1.3); }
          100% { transform: scale(1); }
        }
        @keyframes fadeIn {
          from { opacity: 0; transform: translateY(10px); }
          to { opacity: 1; transform: translateY(0); }
        }
        ::-webkit-scrollbar { width: 4px; }
        ::-webkit-scrollbar-track { background: transparent; }
        ::-webkit-scrollbar-thumb { background: rgba(0,255,200,0.2); border-radius: 2px; }
        * { box-sizing: border-box; }
      `}</style>
    </div>
  );
}

参考文献

  • Dawkins, R. (1996). The blind watchmaker: Why the evidence of evolution reveals a universe without design. WW Norton & Company.

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です