はじめに
「プログラミングできないけど、ゲーム作ってみたい」
そんな気持ちを持ったことはありませんか?
私もそのひとりでした。
学生の頃、図書室で「ゲームを作ろう」という本を見つけて、
必死に本に書いてあるプログラムを学校の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で今すぐ試す(推奨・インストール不要)
- StackBlitz を開く
-
src/App.jsの中身を全部消す - 下のコードをまるごと貼り付ける
- 自動で動きます!
ローカルで動かす場合
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が生成したものです。