miya commited on
Commit
ca3ea91
·
1 Parent(s): e35d41d

modify Readme.md index.html

Browse files
Files changed (3) hide show
  1. README.md +121 -9
  2. index.html +468 -19
  3. style.css +0 -28
README.md CHANGED
@@ -1,12 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: ColorLinkPuzzle
3
- emoji: 💻
4
- colorFrom: blue
5
- colorTo: indigo
6
- sdk: static
7
- pinned: false
8
- license: mit
9
- short_description: An automatically playable puzzle game developed using vibrat
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # カラーリンクパズル (AI 自動プレイ版)
2
+
3
+ **Live Demo**: (HTML ファイルをブラウザで開くだけでプレイできます)
4
+
5
+ ---
6
+
7
+ ## 📖 ゲーム概要
8
+
9
+ ### ゲーム紹介
10
+ 「カラーリンクパズル」は、同じ色のタイルを **4 つ以上** 直線(横・縦・斜め)で連結させて消すマッチングパズルです。
11
+ - **6×6** のグリッドに 6 色のタイルがランダム配置されます。
12
+ - タイル同士は隣接(上下左右)でスワップでき、マッチが成立すれば自動で消えてスコアが加算、上から新しいタイルが降ってきます。
13
+ - **AI 自動プレイ機能** で、AI が自動で最適なスワップを探し、連鎖させながらプレイできます。
14
+
15
+ ### 操作方法
16
+ | 操作 | 説明 |
17
+ |------|------|
18
+ | **タイルのドラッグ** | タイルをクリック(またはタッチ)してドラッグし、隣接タイルと入れ替えてスワップ |
19
+ | **スワップ** | 隣接した 2 タイルの位置が入れ替われば、即座にマッチ判定が実行されます |
20
+ | **新規ゲーム** | 「新規ゲーム」ボタンでランダムに新しい盤面を生成 |
21
+ | **自動プレイ** | 「自動プレイ」ボタンで AI が自動でスワップを行います(再クリックで停止) |
22
+ | **速度スライダー** | AI のプレイ速度(ミリ秒)を調整(デフォルト 600ms) |
23
+ | **テーマ切替** | 「暗いモード」 / 「明るいモード」切替(ローカルストレージに保存) |
24
+
25
+ ### プレイの流れ
26
+ 1. **新規ゲーム** ボタンで開始。
27
+ 2. タイルをドラッグして隣接タイルとスワップ。
28
+ 3. 同色のタイル列が **4 個以上** できると自動で消失し、得点(5pt/タイル)が加算。
29
+ 4. 連鎖が起きた場合は自動で再判定・消去が続く。
30
+ 5. すべてのタイルが消えるか、ゲームオーバー(マッチが作れない)になるまで続ける。
31
+ 6. **自動プレイ** を有効にすれば、AI が連続で最適なスワップを実行し、スコアを稼ぎます。
32
+
33
+ ---
34
+
35
+ ## 🚀 今後の改良点(アイデア)
36
+
37
+ | # | 改良項目 | 説明 |
38
+ |---|----------|------|
39
+ | 1 | **レベル・ステージ** | 難易度別にタイル数・色数を変える、目標スコアや手数制限のステージを追加 |
40
+ | 2 | **スコアボード** | ハイスコア保存(ローカル)やリーダーボードを実装 |
41
+ | 3 | **エフェクト** | 消えるときのパーティクル、連鎖時のエフェクト強化 |
42
+ | 4 | **モバイル最適化** | タップ&スワイプでのスワップ操作、画面サイズに合わせた responsive デザイン |
43
+ | 5 | **AI アルゴリズム** | ミニマックス・評価関数を導入した本格的な AI、手数やスコア最適化 |
44
+ | 6 | **サウンド** | UI 操作音や消失エフェクト音を追加 |
45
+ | 7 | **マルチプレイヤー** | 同時対戦または協力モード |
46
+ | 8 | **設定画面** | 色やテーマ、難易度のカスタマイズ設定 |
47
+
48
+ ---
49
+
50
+ ## 💻 使用技術
51
+
52
+ | カテゴリ | 技術・ライブラリ |
53
+ |----------|----------------|
54
+ | **HTML** | `<canvas>` ではなく **HTML/CSS** で構築したシンプルな UI |
55
+ | **CSS** | カスタムプロパティ(変数)でテーマ・サイズ調整。`@keyframes` による消去アニメーション |
56
+ | **JavaScript (ES6)** | ゲームロジック、AI 自動プレイ、DOM 操作、ローカルストレージ |
57
+ | **CSS Grid** | 6×6 の棋盤( `grid-template-columns` で動的にサイズ調整) |
58
+ | **ローカルストレージ** | テーマ設定の保存 |
59
+ | **GitHub / HuggingFace** | ソースコード管理、README の公開先として HuggingFace Spaces にアップロード想定 |
60
+
61
+ **外部ライブラリは使用していません**(全て純粋な HTML/CSS/JS だけです)。
62
+
63
+ ---
64
+
65
+ ## 📄 ライセンス
66
+
67
+ **MIT License**
68
+
69
+ ```
70
+ Copyright (c) 2025 <開発者>
71
+
72
+ Permission is hereby granted, free of charge, to any person obtaining a
73
+ copy of this software and associated documentation files (the
74
+ "Software"), to deal in the Software without restriction,
75
+ including without limitation the rights to use, copy, modify,
76
+ merge, publish, distribute, sublicense, and/or sell copies
77
+ of the Software, and to permit persons to whom the
78
+ Software is furnished to do so, subject to the
79
+ following conditions:
80
+
81
+ …(省略)…
82
+ ```
83
+
84
  ---
85
+
86
+ ## 👤 開発者
87
+
88
+ - **MiYa** – [Hugging Face: miya3333](https://huggingface.co/miya3333)
89
+ - **MiYa** – [X (Twitter) : @miya00907380](https://x.com/miya00907380)
90
+
91
+ ※本リポジトリは **Hugging Face Spaces** にて Web アプリとして公開可能です。`index.html` をそのままアップ���ードすれば、ブラウザ上で動作します。
92
+
93
  ---
94
 
95
+ ## 🤖 使用LLM
96
+
97
+ | 項目 | 内容 |
98
+ |------|------|
99
+ | **LLM** | [gpt-oss-120b](https://console.groq.com/playground?model=openai/gpt-oss-120b) |
100
+ | **Reasoning** | medium |
101
+ | **Max Completion Tokens** | 8192 |
102
+
103
+ > 本 README は **gpt-oss-120b**(Groq のオープンソース LLM)を使用して生成されています。
104
+
105
+ ---
106
+
107
+ ## 🛠️ ローカルでの実行手順
108
+
109
+ 1. **リポジトリをクローン** もしくはコードをコピー
110
+ ```bash
111
+ git clone https://huggingface.co/spaces/<your-username>/color-link-puzzle
112
+ ```
113
+ 2. 任意のディレクトリで **`index.html`** をブラウザ(Chrome/Firefox/Safari など)で開く
114
+ 3. **ボタン** でゲーム開始、操作は上記「操作方法」参照
115
+
116
+ **※追加のビルドや依存関係は不要です。**
117
+
118
+ ---
119
+
120
+ ### 🎉 Happy Puzzle! 🚀
121
+
122
+ ---
123
+
124
+ *この README は **Hugging Face Spaces** に公開する前提で作成しました。ぜひリポジトリに公開し、他のユーザーとシェアしてください!*
index.html CHANGED
@@ -1,19 +1,468 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>カラーリンクパズル (AI 自動プレイ版)</title>
6
+ <style>
7
+ :root {
8
+ --grid-size:6; /* 6×6 のグリッド */
9
+ --tile-size:80px; /* 1 タイルの横幅・高さ */
10
+ --transition-duration:0.15s; /* 移動アニメーション */
11
+ --remove-duration:0.35s; /* 消去アニメーションの長さ */
12
+
13
+ /* ---------- Light theme ---------- */
14
+ --bg-color:#f0f0f0; /* 背景 */
15
+ --text-color:#333; /* 基本テキスト */
16
+ --tile-bg:#ddd; /* タイル(未選択)背景 */
17
+ --tile-text:#fff; /* タイル文字色 (タイル上の文字は使われていませんが残しておく) */
18
+ }
19
+
20
+ /* Dark theme override – body に .dark がついた時に適用される */
21
+ body.dark {
22
+ --bg-color:#181a1b; /* 背景(暗め) */
23
+ --text-color:#e0e0e0; /* テキスト */
24
+ --tile-bg:#444; /* タイル背景 */
25
+ }
26
+
27
+ body{
28
+ font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;
29
+ background: var(--bg-color);
30
+ color: var(--text-color);
31
+ margin:0;padding:0;
32
+ display:flex;
33
+ flex-direction:column;
34
+ align-items:center;
35
+ justify-content:center; /* 縦方向センタリング */
36
+ height:100vh;
37
+ }
38
+ h1{margin:20px 0 5px; font-size:2rem; text-align:center;}
39
+ #controls{
40
+ margin:10px 0;
41
+ display:flex;
42
+ flex-wrap:wrap;
43
+ justify-content:center;
44
+ gap:8px;
45
+ }
46
+ #score,#moves{margin:0 6px; font-weight:bold;}
47
+ #board{
48
+ display:grid;
49
+ grid-template-columns:repeat(var(--grid-size),var(--tile-size));
50
+ grid-auto-rows:var(--tile-size);
51
+ gap:4px;
52
+ margin:20px;
53
+ user-select:none;
54
+ }
55
+ .tile{
56
+ width:var(--tile-size);
57
+ height:var(--tile-size);
58
+ border-radius:8px;
59
+ background:var(--tile-bg);
60
+ display:flex;
61
+ justify-content:center;
62
+ align-items:center;
63
+ font-size:2rem;
64
+ color: var(--tile-text);
65
+ cursor:pointer;
66
+ transition:
67
+ transform var(--transition-duration) ease;
68
+ transform:scale(1);
69
+ }
70
+ .tile:hover{ transform:scale(1.07); }
71
+ .empty{background:transparent; cursor:default; }
72
+ .disabled{pointer-events:none;}
73
+
74
+ /* 消去エフェクト */
75
+ @keyframes tile-fade-out{
76
+ 0% { opacity:1; transform:scale(1); }
77
+ 100% { opacity:0; transform:scale(0.2); }
78
+ }
79
+
80
+ /* アニメーション中に付けるクラス */
81
+ .tile.removing{
82
+ animation: tile-fade-out var(--remove-duration) ease-out forwards;
83
+ }
84
+
85
+ #message{margin-top:1rem; font-size:1.2rem; text-align:center;}
86
+
87
+ /* Media query for better mobile support */
88
+ @media (max-width:600px){
89
+ #board{
90
+ grid-template-columns:repeat(var(--grid-size),1fr); /* Make tiles responsive */
91
+ }
92
+ .tile{
93
+ --tile-size:60px; /* Adjust tile size for smaller screens */
94
+ }
95
+ }
96
+ </style>
97
+ </head>
98
+ <body>
99
+ <h1>カラーリンクパズル</h1>
100
+
101
+ <div id="controls">
102
+ <button id="newGameBtn">新規ゲーム</button>
103
+ <button id="autoPlayBtn">自動プレイ</button>
104
+
105
+ <!-- 速度スライダー -->
106
+ <label style="margin-left:20px;">
107
+ 速度:
108
+ <input type="range" id="speedSlider" min="20" max="2000" step="10"
109
+ value="600" style="vertical-align: middle; width:120px;">
110
+ <span id="speedValue">600</span> ms
111
+ </label>
112
+
113
+ <!-- ダーク / ライト 切替ボタン -->
114
+ <button id="themeBtn">暗いモード</button>
115
+
116
+ <span id="score">スコア: 0</span>
117
+ <span id="moves">手数: 0</span>
118
+ </div>
119
+
120
+ <div id="board"></div>
121
+ <div id="message"></div>
122
+
123
+ <script>
124
+ /* -------------------------------------------------
125
+ Ⅰ. グローバル変数 & 初期設定
126
+ ------------------------------------------------- */
127
+ const boardEl = document.getElementById('board');
128
+ const boardSize = parseInt(getComputedStyle(document.documentElement)
129
+ .getPropertyValue('--grid-size'));
130
+ const colors = ['#e74c3c','#3498db','#f1c40f','#2ecc71',
131
+ '#9b59b6','#ff9800']; // 6 色
132
+
133
+ let board = []; // 2‑D 格子(色コード)
134
+ let score = 0;
135
+ let moves = 0;
136
+ let isAnimating = false;
137
+ let autoPlayTimer = null;
138
+
139
+ // スライダー関連の変数を追加
140
+ const speedSlider = document.getElementById('speedSlider');
141
+ const speedValue = document.getElementById('speedValue');
142
+ let playDelay = parseInt(speedSlider.value); // 速さ(ms)
143
+
144
+ const scoreEl = document.getElementById('score');
145
+ const movesEl = document.getElementById('moves');
146
+ const messageEl = document.getElementById('message');
147
+ const newGameBtn = document.getElementById('newGameBtn');
148
+ const autoPlayBtn = document.getElementById('autoPlayBtn');
149
+ const themeBtn = document.getElementById('themeBtn');
150
+
151
+ /* -------------------------------------------------
152
+ テーマ切替
153
+ ------------------------------------------------- */
154
+ function setTheme(dark) {
155
+ const body = document.body;
156
+ if (dark) {
157
+ body.classList.add('dark');
158
+ themeBtn.textContent = '明るいモード';
159
+ localStorage.setItem('theme','dark');
160
+ } else {
161
+ body.classList.remove('dark');
162
+ themeBtn.textContent = '暗いモード';
163
+ localStorage.setItem('theme','light');
164
+ }
165
+ }
166
+ function toggleTheme() {
167
+ const isDark = document.body.classList.contains('dark');
168
+ setTheme(!isDark);
169
+ }
170
+ themeBtn.addEventListener('click', toggleTheme);
171
+
172
+ /* 起動時に前回の設定を復元 */
173
+ (() => {
174
+ const saved = localStorage.getItem('theme');
175
+ if (saved === 'dark') setTheme(true);
176
+ })();
177
+
178
+ /* -------------------------------------------------
179
+ スライダーの値変更時処理
180
+ ------------------------------------------------- */
181
+ speedSlider.addEventListener('input', () => {
182
+ playDelay = parseInt(speedSlider.value);
183
+ speedValue.textContent = playDelay;
184
+ if (autoPlayBtn.dataset.state === 'running') {
185
+ stopAutoPlay();
186
+ startAutoPlay();
187
+ }
188
+ });
189
+
190
+ /* -------------------------------------------------
191
+ Ⅱ. ボード生成・描画
192
+ ------------------------------------------------- */
193
+ function initBoard() {
194
+ board = [];
195
+ for (let r = 0; r < boardSize; r++) {
196
+ board[r] = [];
197
+ for (let c = 0; c < boardSize; c++) {
198
+ board[r][c] = colors[Math.floor(Math.random()*colors.length)];
199
+ }
200
+ }
201
+ renderBoard();
202
+ score = 0; moves = 0; updateScore();
203
+ messageEl.textContent = '';
204
+ isAnimating = false;
205
+ stopAutoPlay();
206
+ }
207
+ function renderBoard() {
208
+ boardEl.textContent = '';
209
+ for (let r = 0; r < boardSize; r++) {
210
+ for (let c = 0; c < boardSize; c++) {
211
+ const idx = r * boardSize + c;
212
+ const div = document.createElement('div');
213
+ div.className = 'tile';
214
+ div.dataset.idx = idx;
215
+ const col = board[r][c];
216
+ if (col === null) {
217
+ div.classList.add('empty');
218
+ } else {
219
+ div.style.background = col;
220
+ }
221
+ boardEl.appendChild(div);
222
+ }
223
+ }
224
+ }
225
+
226
+ /* -------------------------------------------------
227
+ Ⅲ. ユーザー操作(ドラッグ + スワップ)
228
+ ------------------------------------------------- */
229
+ let dragStart = null;
230
+ boardEl.addEventListener('mousedown', e => {
231
+ if (isAnimating) return;
232
+ const tile = e.target.closest('.tile');
233
+ if (!tile) return;
234
+ const idx = Number(tile.dataset.idx);
235
+ const [r,c] = indexToRC(idx);
236
+ if (board[r][c] === null) return;
237
+ dragStart = {r,c, elem: tile};
238
+ tile.style.opacity = 0.6;
239
+ });
240
+ boardEl.addEventListener('mouseup', e => {
241
+ if (!dragStart) return;
242
+ dragStart.elem.style.opacity = 1;
243
+ const target = e.target.closest('.tile');
244
+ if (!target) { dragStart = null; return; }
245
+ const idx = Number(target.dataset.idx);
246
+ const [r2,c2] = indexToRC(idx);
247
+ const {r:r1,c:c1} = dragStart;
248
+ if (Math.abs(r1-r2)+Math.abs(c1-c2) === 1) {
249
+ swapTiles(r1,c1,r2,c2);
250
+ checkAndClearMatches();
251
+ }
252
+ dragStart = null;
253
+ });
254
+
255
+ /* -------------------------------------------------
256
+ Ⅳ. スワップ & マッチ判定
257
+ ------------------------------------------------- */
258
+ function swapTiles(r1,c1,r2,c2){
259
+ [board[r1][c1], board[r2][c2]] = [board[r2][c2], board[r1][c1]];
260
+ renderBoard();
261
+ }
262
+
263
+ /* 同色ライン(4 以上)検出 */
264
+ function findMatches() {
265
+ const toClear = new Set();
266
+ const dirs = [[0,1],[1,0],[1,1],[1,-1]]; // 右・下・右下・左下
267
+ for (let r = 0; r < boardSize; r++) {
268
+ for (let c = 0; c < boardSize; c++) {
269
+ const col = board[r][c];
270
+ if (!col) continue;
271
+ for (const [dr,dc] of dirs) {
272
+ const line = [];
273
+ let rr = r, cc = c;
274
+ while (inside(rr,cc) && board[rr][cc] === col) {
275
+ line.push([rr,cc]);
276
+ rr += dr; cc += dc;
277
+ }
278
+ if (line.length >= 4) {
279
+ line.forEach(([sr,sc])=>toClear.add(`${sr}_${sc}`));
280
+ }
281
+ }
282
+ }
283
+ }
284
+ return Array.from(toClear);
285
+ }
286
+ function inside(r,c){ return r>=0 && c>=0 && r<boardSize && c<boardSize; }
287
+ function rcToIndex(r,c){ return r*boardSize + c; }
288
+ function indexToRC(i){ return [Math.floor(i/boardSize), i%boardSize]; }
289
+
290
+ function removeTiles(cells) {
291
+ cells.forEach(key => {
292
+ const [r,c] = key.split('_').map(Number);
293
+ board[r][c] = null;
294
+ });
295
+ }
296
+
297
+ /* 落下と補充 */
298
+ function applyGravityAndFill() {
299
+ for (let c = 0; c < boardSize; c++) {
300
+ const column = [];
301
+ for (let r = boardSize-1; r >= 0; r--) {
302
+ if (board[r][c] !== null) column.push(board[r][c]);
303
+ }
304
+ let r = boardSize-1;
305
+ for (const col of column) {
306
+ board[r][c] = col;
307
+ r--;
308
+ }
309
+ while (r >= 0) {
310
+ board[r][c] = colors[Math.floor(Math.random()*colors.length)];
311
+ r--;
312
+ }
313
+ }
314
+ }
315
+
316
+ /* 消去 & チェーン判定 */
317
+ function checkAndClearMatches() {
318
+ if (isAnimating) return;
319
+ const matches = findMatches();
320
+
321
+ // 消えるものが無い → 手数だけ増やす
322
+ if (matches.length === 0) {
323
+ moves++; updateScore();
324
+ if (isFinished()) {
325
+ messageEl.textContent = `クリア! 手数 ${moves}、スコア ${score}`;
326
+ }
327
+ return;
328
+ }
329
+
330
+ /* ① エフェクト付与 */
331
+ matches.forEach(key => {
332
+ const [r,c] = key.split('_').map(Number);
333
+ const idx = rcToIndex(r,c);
334
+ const tile = boardEl.querySelector(`.tile[data-idx='${idx}']`);
335
+ if (tile) tile.classList.add('removing');
336
+ });
337
+
338
+ /* ② アニメーション後に実際の削除・落下 */
339
+ isAnimating = true;
340
+ const ANIM_MS = parseFloat(getComputedStyle(document.documentElement)
341
+ .getPropertyValue('--remove-duration')) * 1000;
342
+
343
+ setTimeout(() => {
344
+ removeTiles(matches);
345
+ applyGravityAndFill();
346
+
347
+ const added = matches.length * 5; // 1 個=5 pt
348
+ score += added;
349
+ moves++; // 1 手としてカウント
350
+ renderBoard();
351
+ isAnimating = false;
352
+ checkAndClearMatches(); // 連鎖
353
+ updateScore();
354
+ }, ANIM_MS);
355
+ }
356
+
357
+ /* フィールドがすべて空か判定 */
358
+ function isFinished() {
359
+ for (let r = 0; r < boardSize; r++) {
360
+ for (let c = 0; c < boardSize; c++) {
361
+ if (board[r][c] !== null) return false;
362
+ }
363
+ }
364
+ return true;
365
+ }
366
+
367
+ /* -------------------------------------------------
368
+ V. AI 自動プレイロジック
369
+ ------------------------------------------------- */
370
+ function findSwapThatCreatesMatch() {
371
+ const clone = () => board.map(row=>[...row]);
372
+
373
+ for (let r = 0; r < boardSize; r++) {
374
+ for (let c = 0; c < boardSize; c++) {
375
+ if (!board[r][c]) continue;
376
+ const dirs = [[0,1],[1,0],[0,-1],[-1,0]];
377
+ for (const [dr,dc] of dirs) {
378
+ const nr=r+dr, nc=c+dc;
379
+ if (!inside(nr,nc) || !board[nr][nc]) continue;
380
+ const tmp = clone();
381
+ [tmp[r][c], tmp[nr][nc]] = [tmp[nr][nc], tmp[r][c]];
382
+ if (hasMatch(tmp)) return {r1:r,c1:c,r2:nr,c2:nc};
383
+ }
384
+ }
385
+ }
386
+ return null;
387
+ }
388
+
389
+ function hasMatch(arr) {
390
+ const dirs = [[0,1],[1,0],[1,1],[1,-1]];
391
+ for (let r = 0; r < boardSize; r++) {
392
+ for (let c = 0; c < boardSize; c++) {
393
+ const col = arr[r][c];
394
+ if (!col) continue;
395
+ for (const [dr,dc] of dirs) {
396
+ let len = 0, rr=r, cc=c;
397
+ while (inside(rr,cc) && arr[rr][cc] === col) {
398
+ len++; rr+=dr; cc+=dc;
399
+ }
400
+ if (len >= 4) return true;
401
+ }
402
+ }
403
+ }
404
+ return false;
405
+ }
406
+
407
+ /* AI の 1 手 */
408
+ function aiPlayOneMove() {
409
+ if (isAnimating || isFinished()) return;
410
+ const move = findSwapThatCreatesMatch();
411
+
412
+ if (move) {
413
+ swapTiles(move.r1,move.c1,move.r2,move.c2);
414
+ checkAndClearMatches();
415
+ } else {
416
+ // ランダムに隣接ペアをスワップ(マッチが作れないなら)
417
+ const r = Math.floor(Math.random()*boardSize);
418
+ const c = Math.floor(Math.random()*boardSize);
419
+ const dirs = [[0,1],[1,0],[0,-1],[-1,0]];
420
+ const cand = dirs.map(([dr,dc])=>[r+dr,c+dc])
421
+ .filter(([nr,nc])=>inside(nr,nc) && board[nr][nc]!=null);
422
+ if (cand.length) {
423
+ const [nr,nc] = cand[Math.floor(Math.random()*cand.length)];
424
+ swapTiles(r,c,nr,nc);
425
+ checkAndClearMatches();
426
+ } else {
427
+ moves++; updateScore();
428
+ }
429
+ }
430
+ }
431
+
432
+ /* 自動プレイの開始 / 停止 */
433
+ function startAutoPlay() {
434
+ autoPlayBtn.textContent = '停止';
435
+ autoPlayBtn.dataset.state = 'running';
436
+ if (autoPlayTimer) clearInterval(autoPlayTimer);
437
+ autoPlayTimer = setInterval(() => {
438
+ if (!isFinished()) aiPlayOneMove();
439
+ else stopAutoPlay();
440
+ }, playDelay);
441
+ }
442
+ function stopAutoPlay() {
443
+ if (autoPlayTimer) clearInterval(autoPlayTimer);
444
+ autoPlayTimer = null;
445
+ autoPlayBtn.textContent = '自動プレイ';
446
+ autoPlayBtn.dataset.state = 'stopped';
447
+ }
448
+
449
+ /* -------------------------------------------------
450
+ VI. UI 更新・ボタン処理
451
+ ------------------------------------------------- */
452
+ function updateScore() {
453
+ scoreEl.textContent = `スコア: ${score}`;
454
+ movesEl.textContent = `手数: ${moves}`;
455
+ }
456
+
457
+ /* ボタン処理 */
458
+ newGameBtn.addEventListener('click', initBoard);
459
+ autoPlayBtn.addEventListener('click', () => {
460
+ if (autoPlayBtn.dataset.state === 'running') stopAutoPlay();
461
+ else startAutoPlay();
462
+ });
463
+
464
+ /* 初回起動 */
465
+ initBoard();
466
+ </script>
467
+ </body>
468
+ </html>
style.css DELETED
@@ -1,28 +0,0 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }