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?

はじめに

「プログラミングできないけど、ゲーム作ってみたい」

そんな気持ちを持ったことはありませんか?

私もそのひとりでした。
学生の頃、図書室で「ゲームを作ろう」という本を見つけて、
必死に本に書いてあるプログラムを学校のPCへ意味もわからず打ち込んで、
ミスタイプ、エラー、修正を繰り返して動いたときの感動。
これがこの業界に入ったきっかけのひとつでした。

でも、Claude(AI)に話しかけるだけで、本格的なテトリスが完成してしまったのです。コードを1行も書かずに。

この記事では、AIを使ったことがない方に向けて、「AIってこんなことができるんだ」という驚きを共有したいと思います。


作ったもの

Reactで動くテトリスです。機能はこんな感じです。

  • 7種類のテトロミノ(おなじみのブロック)
  • ゴースト表示(落下先の目安が薄く表示される)
  • ライン消去・スコア計算
  • 次のピースのプレビュー
  • キーボード操作 & スマホ向けタッチボタン
  • 一時停止機能

普通にゲームとして遊べるクオリティです。


どうやって作ったか

やったことは、Claudeに日本語で話しかけただけです。

「Reactでテトリスを作ってください。スマホでも遊べるようにタッチ操作のボタンもつけてください。」

たったこれだけ。あとはClaudeが全部考えてくれました。

追加でお願いしたこと:

  • 「ゴースト(落下予測)も表示してほしい」
  • 「見た目をかっこよくしてほしい」

要望を言葉で伝えるだけで、コードを修正してくれます。まるで優秀なエンジニアの友人がいるみたいです。


コードの中身(読まなくてもOK)

一応どんなコードが生成されたか紹介します。難しい部分はスキップしてもらって大丈夫です。

テトロミノの定義

const TETROMINOES = {
  I: { shape: [[1,1,1,1]], color: "#00f5ff" },
  O: { shape: [[1,1],[1,1]], color: "#ffe600" },
  T: { shape: [[0,1,0],[1,1,1]], color: "#bf00ff" },
  // ...他のピースも同様
};

各ブロックの形と色をデータとして定義しています。

ブロックの回転

function rotate(shape) {
  return shape[0].map((_, i) => shape.map(row => row[i]).reverse());
}

行列の転置と反転を組み合わせた、数学的に美しい回転処理です。これをゼロから考えるのは大変ですが、AIはすぐに書いてくれました。

ライン消去

function clearLines(board) {
  const newBoard = board.filter(row => row.some(cell => !cell));
  const cleared = BOARD_HEIGHT - newBoard.length;
  const empty = Array.from({ length: cleared }, () => Array(BOARD_WIDTH).fill(null));
  return { board: [...empty, ...newBoard], cleared };
}

埋まった行を取り除いて、上に空白行を追加するシンプルなロジックです。


AIを使う前に思っていたこと

正直、こんな不安がありました。

  • 「AIって、簡単なことしかできないんじゃ?」
  • 「コードが出てきても、自分には使えないんじゃ?」
  • 「途中でエラーが出たら詰む」

でも、実際に使ってみると全部杞憂でした。

Claudeが出力するコードはそのまま動くし、エラーが出ても「このエラーが出ました」と貼り付ければ直してくれます。まさに会話しながら開発できる感覚です。


AIを使うコツ

初めて使う方へ、私が感じたポイントをまとめます。

1. 具体的にお願いする
「ゲームを作って」より「テトリスをReactで作って。スマホ操作にも対応させて」のほうが良い結果になります。

2. 気に入らなければ追加でお願いする
一発完璧じゃなくて大丈夫。「もっとかっこいい見た目にして」「この機能を追加して」と会話を続ければOKです。

3. エラーはそのままコピペ
うまく動かない時は、エラーメッセージをそのままAIに見せれば解決策を教えてくれます。


まとめ

  • AIに日本語でお願いするだけで、本格的なゲームが作れた
  • コードの知識がなくても、「これを追加して」と言うだけで改善できる
  • エラーが出ても怖くない

プログラミングの敷居が、AIによって劇的に下がっています。「自分には無理」と思っていた方も、ぜひ一度試してみてください。

あなたの「作ってみたいもの」は何ですか?


使用技術

  • Claude(Anthropic)
  • React 18
  • JavaScript(ES2022)

実際に遊んでみよう

コードをそのままコピーして、すぐに動かすことができます。

StackBlitzで今すぐ試す(推奨・インストール不要)

  1. StackBlitz を開く
  2. src/App.js の中身を全部消す
  3. 下のコードをまるごと貼り付ける
  4. 自動で動きます!

ローカルで動かす場合

npx create-react-app tetris
cd tetris
# src/App.js を下のコードで置き換える
npm start

コード全文

import React, { useState, useEffect, useCallback, useRef } from "react";

const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 20;
const TICK_SPEED = 500;

const TETROMINOES = {
  I: { shape: [[1,1,1,1]], color: "#00f5ff" },
  O: { shape: [[1,1],[1,1]], color: "#ffe600" },
  T: { shape: [[0,1,0],[1,1,1]], color: "#bf00ff" },
  S: { shape: [[0,1,1],[1,1,0]], color: "#00ff88" },
  Z: { shape: [[1,1,0],[0,1,1]], color: "#ff2d55" },
  J: { shape: [[1,0,0],[1,1,1]], color: "#ff9f0a" },
  L: { shape: [[0,0,1],[1,1,1]], color: "#0a84ff" },
};

const PIECES = Object.keys(TETROMINOES);

function randomPiece() {
  const key = PIECES[Math.floor(Math.random() * PIECES.length)];
  return { key, ...TETROMINOES[key], x: 3, y: 0 };
}

function rotate(shape) {
  return shape[0].map((_, i) => shape.map(row => row[i]).reverse());
}

function emptyBoard() {
  return Array.from({ length: BOARD_HEIGHT }, () => Array(BOARD_WIDTH).fill(null));
}

function isValid(board, shape, x, y) {
  for (let r = 0; r < shape.length; r++) {
    for (let c = 0; c < shape[r].length; c++) {
      if (!shape[r][c]) continue;
      const nr = y + r, nc = x + c;
      if (nr < 0 || nr >= BOARD_HEIGHT || nc < 0 || nc >= BOARD_WIDTH) return false;
      if (board[nr][nc]) return false;
    }
  }
  return true;
}

function placePiece(board, piece) {
  const newBoard = board.map(r => [...r]);
  for (let r = 0; r < piece.shape.length; r++) {
    for (let c = 0; c < piece.shape[r].length; c++) {
      if (piece.shape[r][c]) {
        newBoard[piece.y + r][piece.x + c] = piece.color;
      }
    }
  }
  return newBoard;
}

function clearLines(board) {
  const newBoard = board.filter(row => row.some(cell => !cell));
  const cleared = BOARD_HEIGHT - newBoard.length;
  const empty = Array.from({ length: cleared }, () => Array(BOARD_WIDTH).fill(null));
  return { board: [...empty, ...newBoard], cleared };
}

function ghostY(board, piece) {
  let y = piece.y;
  while (isValid(board, piece.shape, piece.x, y + 1)) y++;
  return y;
}

export default function Tetris() {
  const [board, setBoard] = useState(emptyBoard());
  const [current, setCurrent] = useState(null);
  const [next, setNext] = useState(null);
  const [score, setScore] = useState(0);
  const [lines, setLines] = useState(0);
  const [gameOver, setGameOver] = useState(false);
  const [started, setStarted] = useState(false);
  const [paused, setPaused] = useState(false);
  const boardRef = useRef(board);
  const currentRef = useRef(current);
  const pausedRef = useRef(paused);
  boardRef.current = board;
  currentRef.current = current;
  pausedRef.current = paused;

  const spawnPiece = useCallback((nextPiece, currentBoard) => {
    const piece = nextPiece || randomPiece();
    const newNext = randomPiece();
    if (!isValid(currentBoard, piece.shape, piece.x, piece.y)) {
      setGameOver(true);
      return;
    }
    setCurrent(piece);
    setNext(newNext);
  }, []);

  const lockPiece = useCallback((piece, currentBoard) => {
    const newBoard = placePiece(currentBoard, piece);
    const { board: cleared, cleared: count } = clearLines(newBoard);
    setBoard(cleared);
    setLines(l => l + count);
    setScore(s => s + [0, 100, 300, 500, 800][count] || 0);
    spawnPiece(null, cleared);
  }, [spawnPiece]);

  const moveDown = useCallback(() => {
    if (pausedRef.current) return;
    const piece = currentRef.current;
    const b = boardRef.current;
    if (!piece) return;
    if (isValid(b, piece.shape, piece.x, piece.y + 1)) {
      setCurrent(p => ({ ...p, y: p.y + 1 }));
    } else {
      lockPiece(piece, b);
    }
  }, [lockPiece]);

  useEffect(() => {
    if (!started || gameOver) return;
    const interval = setInterval(moveDown, TICK_SPEED);
    return () => clearInterval(interval);
  }, [started, gameOver, moveDown]);

  useEffect(() => {
    if (!started || gameOver) return;
    const handleKey = (e) => {
      if (pausedRef.current && e.key !== "p" && e.key !== "Escape") return;
      const piece = currentRef.current;
      const b = boardRef.current;
      if (!piece) return;
      if (e.key === "ArrowLeft" && isValid(b, piece.shape, piece.x - 1, piece.y)) {
        setCurrent(p => ({ ...p, x: p.x - 1 }));
      } else if (e.key === "ArrowRight" && isValid(b, piece.shape, piece.x + 1, piece.y)) {
        setCurrent(p => ({ ...p, x: p.x + 1 }));
      } else if (e.key === "ArrowDown") {
        moveDown();
      } else if (e.key === "ArrowUp") {
        const rotated = rotate(piece.shape);
        if (isValid(b, rotated, piece.x, piece.y)) {
          setCurrent(p => ({ ...p, shape: rotated }));
        }
      } else if (e.key === " ") {
        e.preventDefault();
        const gy = ghostY(b, piece);
        const dropped = { ...piece, y: gy };
        lockPiece(dropped, b);
      } else if (e.key === "p" || e.key === "Escape") {
        setPaused(v => !v);
      }
    };
    window.addEventListener("keydown", handleKey);
    return () => window.removeEventListener("keydown", handleKey);
  }, [started, gameOver, moveDown, lockPiece]);

  const startGame = () => {
    const b = emptyBoard();
    setBoard(b);
    setScore(0);
    setLines(0);
    setGameOver(false);
    setPaused(false);
    setStarted(true);
    const first = randomPiece();
    const second = randomPiece();
    setCurrent(first);
    setNext(second);
  };

  const displayBoard = current ? placePiece(board, current) : board;
  const ghost = current ? ghostY(board, current) : null;

  const renderBoard = displayBoard.map((row, r) =>
    row.map((cell, c) => {
      if (cell) return cell;
      if (current && ghost !== null) {
        for (let sr = 0; sr < current.shape.length; sr++) {
          for (let sc = 0; sc < current.shape[sr].length; sc++) {
            if (current.shape[sr][sc] && ghost + sr === r && current.x + sc === c) {
              return "ghost";
            }
          }
        }
      }
      return null;
    })
  );

  const CELL = 28;

  return (
    <div style={{
      minHeight: "100vh",
      background: "#0a0a0f",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      fontFamily: "'Courier New', monospace",
      color: "#fff",
    }}>
      <div style={{ display: "flex", gap: 24, alignItems: "flex-start" }}>
        <div style={{ position: "relative" }}>
          <div style={{
            border: "2px solid #333",
            display: "grid",
            gridTemplateColumns: `repeat(${BOARD_WIDTH}, ${CELL}px)`,
            gridTemplateRows: `repeat(${BOARD_HEIGHT}, ${CELL}px)`,
            gap: 1,
            background: "#111",
            boxShadow: "0 0 40px rgba(0,245,255,0.08)",
          }}>
            {renderBoard.map((row, r) =>
              row.map((cell, c) => (
                <div key={`${r}-${c}`} style={{
                  width: CELL, height: CELL,
                  background: cell === "ghost"
                    ? "rgba(255,255,255,0.07)"
                    : cell ? cell : "#0d0d14",
                  border: cell && cell !== "ghost"
                    ? "1px solid rgba(255,255,255,0.15)"
                    : "1px solid #1a1a24",
                  boxSizing: "border-box",
                  boxShadow: cell && cell !== "ghost"
                    ? "inset 0 0 8px rgba(255,255,255,0.1)"
                    : "none",
                  transition: "background 0.05s",
                }} />
              ))
            )}
          </div>

          {(!started || gameOver || paused) && (
            <div style={{
              position: "absolute", inset: 0,
              background: "rgba(0,0,0,0.85)",
              display: "flex", flexDirection: "column",
              alignItems: "center", justifyContent: "center",
              gap: 16,
            }}>
              {gameOver && <div style={{ fontSize: 22, color: "#ff2d55", letterSpacing: 4, fontWeight: "bold" }}>GAME OVER</div>}
              {paused && !gameOver && <div style={{ fontSize: 20, color: "#ffe600", letterSpacing: 4 }}>PAUSED</div>}
              {!started && !gameOver && <div style={{ fontSize: 22, letterSpacing: 4, color: "#00f5ff" }}>TETRIS</div>}
              <button onClick={startGame} style={{
                marginTop: 8,
                padding: "10px 28px",
                background: "transparent",
                border: "2px solid #00f5ff",
                color: "#00f5ff",
                fontSize: 14,
                letterSpacing: 3,
                cursor: "pointer",
                fontFamily: "inherit",
              }}>
                {gameOver ? "RETRY" : paused ? "RESUME" : "START"}
              </button>
            </div>
          )}
        </div>

        <div style={{ display: "flex", flexDirection: "column", gap: 20, minWidth: 100 }}>
          <div>
            <div style={{ fontSize: 10, color: "#555", letterSpacing: 3, marginBottom: 6 }}>SCORE</div>
            <div style={{ fontSize: 22, color: "#00f5ff", fontWeight: "bold" }}>{score}</div>
          </div>
          <div>
            <div style={{ fontSize: 10, color: "#555", letterSpacing: 3, marginBottom: 6 }}>LINES</div>
            <div style={{ fontSize: 22, color: "#ffe600" }}>{lines}</div>
          </div>

          <div>
            <div style={{ fontSize: 10, color: "#555", letterSpacing: 3, marginBottom: 8 }}>NEXT</div>
            {next && (
              <div style={{
                background: "#111", border: "1px solid #222",
                padding: 8, display: "inline-block",
              }}>
                {next.shape.map((row, r) => (
                  <div key={r} style={{ display: "flex" }}>
                    {row.map((cell, c) => (
                      <div key={c} style={{
                        width: 16, height: 16,
                        background: cell ? next.color : "transparent",
                        border: cell ? "1px solid rgba(255,255,255,0.15)" : "none",
                        margin: 1,
                      }} />
                    ))}
                  </div>
                ))}
              </div>
            )}
          </div>

          <div style={{ marginTop: 8 }}>
            <div style={{ fontSize: 10, color: "#333", letterSpacing: 2, marginBottom: 8 }}>CONTROLS</div>
            {[["←→", "移動"], ["", "回転"], ["", "落下"], ["SPC", "即落下"], ["P", "一時停止"]].map(([k, v]) => (
              <div key={k} style={{ display: "flex", justifyContent: "space-between", gap: 12, marginBottom: 4 }}>
                <span style={{ fontSize: 10, color: "#00f5ff", fontWeight: "bold" }}>{k}</span>
                <span style={{ fontSize: 10, color: "#444" }}>{v}</span>
              </div>
            ))}
          </div>

          <div style={{ marginTop: 8 }}>
            <div style={{ fontSize: 10, color: "#333", letterSpacing: 2, marginBottom: 8 }}>タッチ操作</div>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }}>
              {[["", () => {
                const piece = currentRef.current, b = boardRef.current;
                if (piece && isValid(b, piece.shape, piece.x - 1, piece.y)) setCurrent(p => ({...p, x: p.x - 1}));
              }], ["回転", () => {
                const piece = currentRef.current, b = boardRef.current;
                if (!piece) return;
                const rotated = rotate(piece.shape);
                if (isValid(b, rotated, piece.x, piece.y)) setCurrent(p => ({...p, shape: rotated}));
              }], ["", () => {
                const piece = currentRef.current, b = boardRef.current;
                if (piece && isValid(b, piece.shape, piece.x + 1, piece.y)) setCurrent(p => ({...p, x: p.x + 1}));
              }], ["", moveDown]].map(([label, fn]) => (
                <button key={label} onClick={fn} style={{
                  background: "#111", border: "1px solid #333",
                  color: "#aaa", fontSize: 18, padding: "10px 0",
                  cursor: "pointer", fontFamily: "inherit",
                  userSelect: "none",
                }}>{label}</button>
              ))}
            </div>
            <button onClick={() => {
              const piece = currentRef.current, b = boardRef.current;
              if (!piece) return;
              const gy = ghostY(b, piece);
              lockPiece({ ...piece, y: gy }, b);
            }} style={{
              marginTop: 6, width: "100%",
              background: "#111", border: "1px solid #333",
              color: "#00f5ff", fontSize: 11, padding: "8px 0",
              cursor: "pointer", fontFamily: "inherit", letterSpacing: 2,
            }}>DROP</button>
          </div>
        </div>
      </div>
    </div>
  );
}

この記事のコードはすべてClaudeが生成したものです。

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?