【ローカルLLMコーディング性能比較】テトリスを生成できるか試してみた

# 1. はじめに
ここ数か月でgemma3, qwen3, mistral-samll3.2, ph4-readsoningなど、コンシューマ用のGPUで十分動作可能で、かつ、それなりに性能の高いLLMが複数発表されました。このため、個人的にコーディングで普段使いするためのLLMを決めるため、各LLMにテトリスを生成するための同一のプロンプトを入力し、結果を比較してみました。

# 2. 比較対象LLM
今回、試行したローカルLLMのモデルは以下の通りです。

## ローカルLLM(すべて4bit量子化したバージョン)
| モデル (Model) | パラメータ数 (Number of Parameters) |
|---|---|
| gemma3 | 12B |
| deepcoder | 14B |
| Deepseek-r1 | 14B |
| qwen3 | 14B |
| phi4 | 14B |
| phi4-reasoning | 14B |
| magistral | 24b |
| Mistral-small3.2 | 24b |
| gemma3 | 27B |
| qwen3 | 30B-A3B |
| qwen3 | 32B |
| Deepseek-r1 | 32B |

また、比較のために以下の商用LLMにについても試行しています。

■商用LLM
- o4-mini
- o3-mini
- Gemini 2.5 Flash
- Deepseek-r1

# 3.測定環境

## ハードウェア
ハードウェアは以下のものを使用しています。GPUは2枚刺しです。

| 項目 (Item) | 内容 (Content) |
|---|---|
| OS | Kubuntu 24.02.02 |
| CPU | Ryzen 5 7600 |
| メモリ (Memory) | DDR5-5600 64GB |
| マザーボード (Motherboard) | MSI MAG B650 TOMAHAWK WIFI |
| GPU1 | ZOTAC GAMING GeForce RTX 5060 Ti 16GB Twin Edge |
| GPU2 | MSI GeForce RTX 3060 VENTUS 2X 12G OC |
| GPUドライババージョン (GPU Driver Version) | 570.169 |

## ソフトウェア
使用したソフトウェア環境は以下の通り。特に設定を変更せず、デフォルトのまま使用しています。

| ソフトウェア (Software) | バージョン (Version) |
|---|---|
| ollama | v0.9.2 |
| Open-webui | v0.6.15 |

# 4. プロンプト
テトリスを生成させるためのプロンプトは以下の通りです。
プロンプトはDeepSeek-R1に生成してもらいました。

```md
下の要件でブラウザ上で動作するテトリスゲームの完全なソースコードを生成してください。HTML/CSS/JavaScriptを単一ファイルに記述し、コピペで実行可能な形式（<!DOCTYPE html>から</html>まで全てを含む）で出力してください。JavaScriptは<script>タグ内に、CSSは<style>タグ内に記述してください。

### 必須要件
1.  **キー操作**
    * 左矢印キー: ブロックを左に1マス移動
    * 右矢印キー: ブロックを右に1マス移動
    * 下矢印キー: ブロックを1マス加速落下（ソフトドロップ）
    * エンターキー: ブロックを時計回りに90度回転。テトリス公式ルールに準拠したスーパーローテーションシステム（SRS）を実装し、壁や他のブロックとの衝突を回避しながら回転を試みる。

2.  **ゲーム機能**
    * 7種類のテトロミノ（I, O, T, S, Z, J, L）を、テトリス公式ルールに準拠したランダム生成アルゴリズム（7種類すべてが出現するまで重複なし、その後シャッフル）で生成。
    * ブロックが最下部または他のブロックに到達すると固定され、新しいブロックが出現。
    * 横一列が揃ったらライン消去。ライン消去時、効果音（コードコメントで「効果音再生」と記述するのみで良い）を鳴らすことを想定し、その後、消したライン数に応じてスコアを加算。
        * 1ライン: 100点
        * 2ライン: 300点
        * 3ライン: 500点
        * 4ライン（テトリス）: 800点
    * 新しいブロックが出現する場所に既存のブロックがある場合、ゲームオーバーと判定。
    * スコア表示（消したライン数に応じて加算）
    * ゲーム開始時、ブロックの初期落下速度は500msとし、ラインを5つ消すごとに落下速度を10%ずつ加速する。
    * 画面を表示する
    
3.  **表示要素**
    * メインゲームボード（10x20グリッド）。各グリッドは正方形で、黒い背景に薄いグレーのグリッド線を表示。
    * 現在のスコア表示領域をゲームボードの右側に配置。フォントはゴシック系、サイズは24px、色は白。
    * 次のブロックをプレビュー表示する領域をスコア表示の下に配置。プレビューも各テトロミノの色で表示。
    * ゲームオーバー時は、ゲームボード中央に「GAME OVER」という明確なメッセージを赤色、大きなフォント（48px）で表示。
    * 各テトロミノの色は以下の通りとする: I:シアン, O:イエロー, T:マゼンタ, S:グリーン, Z:レッド, J:ブルー, L:オレンジ。

4.  **技術要件**
    * 純粋なJavaScriptのみ使用（ライブラリ、フレームワーク、ビルドツール、外部ファイルは不可）。
    * CSSは`<style>`タグ内に、JavaScriptは`<script>`タグ内に記述し、HTMLファイル内にすべてを完結させる。
    * レスポンシブデザインに対応し、画面サイズが変化してもゲームボードのアスペクト比が維持されるように調整。キャンバスの最大幅は400pxとする。
    * ES2015（ES6）以降のJavaScript構文（let, const, アロー関数など）を使用しても良い。
    * コードは可読性を重視し、適切な変数名、関数名、コメントを使用すること。特に複雑なロジックにはコメントを付加すること。
    * プログラムは予期せぬキー入力に対して堅牢であり、パフォーマンスが良好でスムーズなアニメーションが維持されること。

```

# 5. 試行結果サマリ
## ローカルLLM
ローカルLLMでの試行結果のサマリを以下に示します。パラメタ数が14Bまでのモデルは、どれも「全く動作せず」。
パラメタ数が24Bを超えてくると、「ちょっとは動く」レベルのもの、「普通に遊べる」レベルのものがチラホラ現れてくるといった結果でした。やはりモデルのパラメタ数は性能に直結するというのを再確認できました。
Mistral-small3.2は比較的少なめならパラメタ(24B)なのに、「普通に遊べる」ものが出力されて、かなり優秀な印象を受けました。


| モデル             | パラメタ数   | 評価  | 理由                                                                 |
|--------------------|-------------|-------|----------------------------------------------------------------------|
| gemma3            | 12B         | ×     | まったく動作せず。真っ黒な画面のまま。描画ロジック欠落。                   |
| deepcoder         | 14B         | ×     | まったく動作せず。真っ黒な画面のまま。変数定義欠落。                       |
| Deepseek-r1       | 14B         | ×     | まったく動作せず。真っ黒な画面のまま。変数定義欠落。                       |
| qwen3             | 14B         | ×     | まったく動作せず。真っ黒な画面のまま。引数の型誤り。                       |
| phi4              | 14B         | ×××   | まったく動作せず。真っ黒な画面のまま。HTML/Javascriptの構造が壊れてる。     |
| phi4-reasoning    | 14B         | ×     | まったく動作せず。真っ黒な画面のまま。原因は複合要因でよくわからない。       |
| magistral         | 24b         | ××    | まったく動作せず。真っ黒な画面のまま。コードが途中で途切れていた。           |
| Mistral-small3.2  | 24b         | ◎     | 普通に遊べるものが生成された。                                             |
| gemma3            | 27B         | △     | ゲームが始まると即ゲームオーバー。                                         |
| qwen3             | 30B-A3B     | ×     | まったく動作せず。原因は複合要因でよくわからない。                         |
| qwen3             | 32B         | △     | ブロックを3個置いた時点でフリーズ。                                        |
| Deepseek-r1       | 32B         | 〇    | 普通に遊べるものが生成された。ただしブロックの回転に若干のバグあり。         |

## 商用LLM
商用の試行結果のサマリを以下に示します。今回試したモデルはどれも「普通に遊べる」レベルのテトリスのコードが生成できました。やっぱり商用LLMはさすがといったところです。


| モデル            | 評価   | 理由                                                                 |
|-------------------|--------|----------------------------------------------------------------------|
| o4-mini          | ◎      | 普通に遊べるものが生成された。Mistral-small3.2(24B)と同程度。           |
| o3-mini               | ◎      | 普通に遊べるものが生成された。Mistral-small3.2(24B)と同程度。           |
| Gemini 2.5 Flash | ◎◎     | 普通に遊べるものが生成された。見た目も美しい。                           |
| Deepseek-r1      | ◎◎◎    | 普通に遊べるものが生成された。見た目がとても美しい。                      |

# 6. 試行結果詳細
## gemma3(12B)

真っ黒な画面のまま全く動作せず。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/3aed7487-ed64-443e-811e-6c270e2901a6.png)

主な原因はテトロミノ(ブロック)の描画ロジックが抜け落ちてコメントだけになっていたため。

```javascript
        // テトロミノの描画
        function drawBlock() {
            const x = Math.floor(boardWidth / 2) - Math.floor(currentBlock.data[0].length / 2);
            const y = 0;

            for (let row = 0; row < currentBlock.data.length; row++) {
                for (let col = 0; col < currentBlock.data[row].length; col++) {
                    if (currentBlock.data[row][col]) {
                        if (x + col >= 0 && x + col < boardWidth && y + row >= 0 && y + row < boardHeight && board[y + row][x + col] === 0) {
                            //  描画処理をここに記述(Canvas APIを利用するなど)
                        }
                    }
                }
            }
        }
```

## deepcoder(14B)
真っ黒な画面のまま全く動作せず。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/6b274492-8cfb-464c-bc7f-1eea953ab1f2.png)

原因は複数あるが、一番大きいミスはブロックの位置を保持する変数の定義が漏れている点。

```javascript
    ctx.fillRect(
        (currentPosX + x) * BLOCK_SIZE,  // currentPosXを定義せずに使用
        (currentPosY + y) * BLOCK_SIZE,  // currentPosYを定義せずに使用
        BLOCK_SIZE - 1,
        BLOCK_SIZE - 1
    );
```


## DeepSeek-R1(14B)
真っ黒な画面のまま全く動作せず。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/5bd74941-eeae-4f7c-b237-e1b15011fba9.png)

動作しない原因はdeepcoder同様に変数の定義漏れ。

```javascript
        function drawBlock(block, position) {
            block.forEach((row, y) => {
                row.forEach((value, x) => {
                    if (value) {
                        ctx.fillStyle = COLORS[blockType]; // blockTypeが未定義
                        ctx.fillRect(
                            (position.x + x) * BLOCK_SIZE,
                            (position.y + y) * BLOCK_SIZE,
                            BLOCK_SIZE - 1,
                            BLOCK_SIZE - 1
                        );
                    }
                });
            });
        }
```

## Qwen3(14B)

真っ黒な画面のまま全く動作せず。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/cb188330-007b-4d8d-85f0-863389908cdd.png)

こちらも原因は複数があるが、一番大きいのは引数をオブジェクトとして渡しているのに文字列として扱っている点。

```javascript
    function drawPiece(piece, x, y) { // pieceはオブジェクト
      const shape = PIECES[piece]; // piecce.shapeが正解。
      for (let i = 0; i < shape.length; i++) {
        for (let j = 0; j < shape[i].length; j++) {
          if (shape[i][j]) {
            context.fillStyle = COLORS[piece];
            context.fillRect((x + j) * BLOCK_SIZE, (y + i) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
          }
        }
      }
    }

```

## phi-4(14B)
真っ黒な画面のまま全く動作せず。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/10893a14-18d9-48cc-aba7-f93f94edbad8.png)

phi-4は、これまでの中でもっともひどい。
HTMLの構造が完全に壊れていた。これはひどい。

```javascript
(略)
    const width = boardWidth * blockSize;
    const height = (boardHeight + 3) * blockSize;

    canvas.width = width;
    canvas.height = height;

    drawNextPiece();

    startTimestamp = performance.now();
    startGame();

// 一続きのコードとして出力されたが、ここで完全に壊れている。

**HTML:**

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Tetris</title>
    <style>
(略)
```


## phi-4-reasoning(14B)
真っ黒な画面のまま全く動作せず。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/8f5fde5b-7274-469d-a7a6-4a105ba9a998.png)

動作しない理由はいっぱいあってよくわかりませんでした。
が、ブロックの生成処理や描画処理の呼び出しが漏れているようでした。

なお、個人的な意見も入りますが、phi-4-reasoningはthinkingの時間が長すぎて普段使いには厳しいと感じました。


## magistral(24B)
真っ黒な画面のまま全く動作せず。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/cad572e0-b126-4c8b-851b-250f8dfea14f.png)

原因はコードの出力が途切れたこと。コンテキスト長の最大に達したわけではなく純粋にコードの出力が突然途切れていた。これはひどい。

```javascript
    gameLoop();
    
    // 画面リサイズ時の再描画
    window.addEventListener('resize', () => {
        drawBoard();
        drawNextBlockPreview();
    });
    }
// ここで途切れている
// 少なくとも以降に</script>などのHTML要素が必要だが出力されていない。

```

## Mistral-small3.2(24B)
普通に遊べるものが出力されていた。凄い。ブロックの回転も、ライン消去も普通にできた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/218d4419-78dc-4f06-9bbb-94e0be9884dd.png)

ゲームオーバーも正しく判定されていた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/45967b6d-a872-4797-bcec-b39289200970.png)




## gemma3(32B)
ゲームが始まってブロックが表示された瞬間にゲームオーバー。理不尽。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/ef859702-31ef-49c7-8d1e-78a1ecf759b4.png)

原因はよくわからず(複数要因が絡んでよくわからず)

## qwen3(30B-A3B)
真っ黒な画面のまま動作しない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/fd9711da-2091-4be4-83f7-5d29ad76311d.png)

主な原因は未定義の変数参照。

```javascript
        // ピース回転
        function rotatePiece() {
            const nextIndex = (currentPiece.shapeIndex + 1) %  currentPiece.shape.length;  // shapeIndexという変数が未定義
            const newShape = currentPiece.shape[nextIndex];
            const temp = currentPiece.shape;
            currentPiece.shape = newShape;
            if (isCollision(currentPiece)) {
                currentPiece.shape = temp;
            } else {
                currentPiece.shapeIndex = nextIndex;
            }
        }
```


## qwen3(32B)
途中まで動作するがブロック3つ置くとフリーズする。
また、底面の位置がおかしい(空中にある)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/436a622f-0e5c-4de2-a831-ac2bff72c99d.png)

原因はよくわからなったのですが、AIに聞くとゲームオーバー判定ロジックの誤りと、生成したブロックのキュー管理がおかしいとのことでした。

## Deepseek-R1(32B)
普通に遊べるものが生成された。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/158df738-4c0c-4b28-88f3-809139ec282e.png)

ゲームオーバー判定も問題なし。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/95b0a708-3215-4e42-9897-7d4729276125.png)

ただし、ブロックを回転すると、ブロックが左に1マスずつずれていくというバグあり。


## o4-mini
普通に遊べるものが生成された。Mistral-small3.2(24B)と同程度の印象。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/71ac16a1-1ca3-45be-b256-313f182ebc04.png)

## o3-mini
普通に遊べるものが生成された。Mistral-small3.2(24B)と同程度の印象。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/227990b4-c39a-4f94-8915-04efe0a952d4.png)

## Gemini 2.5 Flash
完璧。ただし、見た目のリッチ感は後述のDeepseek-R1に若干劣る。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/23b84c33-5931-4af5-b268-473f5a767871.png)

## Deepseek-R1
完璧すぎて言うことなし。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072158/e35ef007-7ff9-405b-8b64-382f61659eb3.png)

## 7. まとめ
今回はローカルLLMコーディング性能の比較をしてみました。ローカルLLMでも24Bから32Bのモデルになれば、今回のようにそれなりに複雑なゲームのコードも生成できる性能があるということが確認できました。
これまでは、qwwen3をメインに使っていましたが、今回の検証でMistral-small3.2(24B)が非常に優秀なことが分かったため、しばらくはMistral-small3.2(24B)を使って、いろいろ遊んでみようと思います。
あと、商用LLMはさすがですね(笑)