Spaces:
Running
Running
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>カラーリンクパズル (AI 自動プレイ版)</title> | |
<style> | |
:root { | |
--grid-size:6; /* 6×6 のグリッド */ | |
--tile-size:80px; /* 1 タイルの横幅・高さ */ | |
--transition-duration:0.15s; /* 移動アニメーション */ | |
--remove-duration:0.35s; /* 消去アニメーションの長さ */ | |
/* ---------- Light theme ---------- */ | |
--bg-color:#f0f0f0; /* 背景 */ | |
--text-color:#333; /* 基本テキスト */ | |
--tile-bg:#ddd; /* タイル(未選択)背景 */ | |
--tile-text:#fff; /* タイル文字色 (タイル上の文字は使われていませんが残しておく) */ | |
} | |
/* Dark theme override – body に .dark がついた時に適用される */ | |
body.dark { | |
--bg-color:#181a1b; /* 背景(暗め) */ | |
--text-color:#e0e0e0; /* テキスト */ | |
--tile-bg:#444; /* タイル背景 */ | |
} | |
body{ | |
font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; | |
background: var(--bg-color); | |
color: var(--text-color); | |
margin:0;padding:0; | |
display:flex; | |
flex-direction:column; | |
align-items:center; | |
justify-content:center; /* 縦方向センタリング */ | |
height:100vh; | |
} | |
h1{margin:20px 0 5px; font-size:2rem; text-align:center;} | |
#controls{ | |
margin:10px 0; | |
display:flex; | |
flex-wrap:wrap; | |
justify-content:center; | |
gap:8px; | |
} | |
#score,#moves{margin:0 6px; font-weight:bold;} | |
#board{ | |
display:grid; | |
grid-template-columns:repeat(var(--grid-size),var(--tile-size)); | |
grid-auto-rows:var(--tile-size); | |
gap:4px; | |
margin:20px; | |
user-select:none; | |
} | |
.tile{ | |
width:var(--tile-size); | |
height:var(--tile-size); | |
border-radius:8px; | |
background:var(--tile-bg); | |
display:flex; | |
justify-content:center; | |
align-items:center; | |
font-size:2rem; | |
color: var(--tile-text); | |
cursor:pointer; | |
transition: | |
transform var(--transition-duration) ease; | |
transform:scale(1); | |
} | |
.tile:hover{ transform:scale(1.07); } | |
.empty{background:transparent; cursor:default; } | |
.disabled{pointer-events:none;} | |
/* 消去エフェクト */ | |
@keyframes tile-fade-out{ | |
0% { opacity:1; transform:scale(1); } | |
100% { opacity:0; transform:scale(0.2); } | |
} | |
/* アニメーション中に付けるクラス */ | |
.tile.removing{ | |
animation: tile-fade-out var(--remove-duration) ease-out forwards; | |
} | |
#message{margin-top:1rem; font-size:1.2rem; text-align:center;} | |
/* Media query for better mobile support */ | |
@media (max-width:600px){ | |
#board{ | |
grid-template-columns:repeat(var(--grid-size),1fr); /* Make tiles responsive */ | |
} | |
.tile{ | |
--tile-size:60px; /* Adjust tile size for smaller screens */ | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<h1>カラーリンクパズル</h1> | |
<div id="controls"> | |
<button id="newGameBtn">新規ゲーム</button> | |
<button id="autoPlayBtn">自動プレイ</button> | |
<!-- 速度スライダー --> | |
<label style="margin-left:20px;"> | |
速度: | |
<input type="range" id="speedSlider" min="20" max="2000" step="10" | |
value="600" style="vertical-align: middle; width:120px;"> | |
<span id="speedValue">600</span> ms | |
</label> | |
<!-- ダーク / ライト 切替ボタン --> | |
<button id="themeBtn">暗いモード</button> | |
<span id="score">スコア: 0</span> | |
<span id="moves">手数: 0</span> | |
</div> | |
<div id="board"></div> | |
<div id="message"></div> | |
<script> | |
/* ------------------------------------------------- | |
Ⅰ. グローバル変数 & 初期設定 | |
------------------------------------------------- */ | |
const boardEl = document.getElementById('board'); | |
const boardSize = parseInt(getComputedStyle(document.documentElement) | |
.getPropertyValue('--grid-size')); | |
const colors = ['#e74c3c','#3498db','#f1c40f','#2ecc71', | |
'#9b59b6','#ff9800']; // 6 色 | |
let board = []; // 2‑D 格子(色コード) | |
let score = 0; | |
let moves = 0; | |
let isAnimating = false; | |
let autoPlayTimer = null; | |
// スライダー関連の変数を追加 | |
const speedSlider = document.getElementById('speedSlider'); | |
const speedValue = document.getElementById('speedValue'); | |
let playDelay = parseInt(speedSlider.value); // 速さ(ms) | |
const scoreEl = document.getElementById('score'); | |
const movesEl = document.getElementById('moves'); | |
const messageEl = document.getElementById('message'); | |
const newGameBtn = document.getElementById('newGameBtn'); | |
const autoPlayBtn = document.getElementById('autoPlayBtn'); | |
const themeBtn = document.getElementById('themeBtn'); | |
/* ------------------------------------------------- | |
テーマ切替 | |
------------------------------------------------- */ | |
function setTheme(dark) { | |
const body = document.body; | |
if (dark) { | |
body.classList.add('dark'); | |
themeBtn.textContent = '明るいモード'; | |
localStorage.setItem('theme','dark'); | |
} else { | |
body.classList.remove('dark'); | |
themeBtn.textContent = '暗いモード'; | |
localStorage.setItem('theme','light'); | |
} | |
} | |
function toggleTheme() { | |
const isDark = document.body.classList.contains('dark'); | |
setTheme(!isDark); | |
} | |
themeBtn.addEventListener('click', toggleTheme); | |
/* 起動時に前回の設定を復元 */ | |
(() => { | |
const saved = localStorage.getItem('theme'); | |
if (saved === 'dark') setTheme(true); | |
})(); | |
/* ------------------------------------------------- | |
スライダーの値変更時処理 | |
------------------------------------------------- */ | |
speedSlider.addEventListener('input', () => { | |
playDelay = parseInt(speedSlider.value); | |
speedValue.textContent = playDelay; | |
if (autoPlayBtn.dataset.state === 'running') { | |
stopAutoPlay(); | |
startAutoPlay(); | |
} | |
}); | |
/* ------------------------------------------------- | |
Ⅱ. ボード生成・描画 | |
------------------------------------------------- */ | |
function initBoard() { | |
board = []; | |
for (let r = 0; r < boardSize; r++) { | |
board[r] = []; | |
for (let c = 0; c < boardSize; c++) { | |
board[r][c] = colors[Math.floor(Math.random()*colors.length)]; | |
} | |
} | |
renderBoard(); | |
score = 0; moves = 0; updateScore(); | |
messageEl.textContent = ''; | |
isAnimating = false; | |
stopAutoPlay(); | |
} | |
function renderBoard() { | |
boardEl.textContent = ''; | |
for (let r = 0; r < boardSize; r++) { | |
for (let c = 0; c < boardSize; c++) { | |
const idx = r * boardSize + c; | |
const div = document.createElement('div'); | |
div.className = 'tile'; | |
div.dataset.idx = idx; | |
const col = board[r][c]; | |
if (col === null) { | |
div.classList.add('empty'); | |
} else { | |
div.style.background = col; | |
} | |
boardEl.appendChild(div); | |
} | |
} | |
} | |
/* ------------------------------------------------- | |
Ⅲ. ユーザー操作(ドラッグ + スワップ) | |
------------------------------------------------- */ | |
let dragStart = null; | |
boardEl.addEventListener('mousedown', e => { | |
if (isAnimating) return; | |
const tile = e.target.closest('.tile'); | |
if (!tile) return; | |
const idx = Number(tile.dataset.idx); | |
const [r,c] = indexToRC(idx); | |
if (board[r][c] === null) return; | |
dragStart = {r,c, elem: tile}; | |
tile.style.opacity = 0.6; | |
}); | |
boardEl.addEventListener('mouseup', e => { | |
if (!dragStart) return; | |
dragStart.elem.style.opacity = 1; | |
const target = e.target.closest('.tile'); | |
if (!target) { dragStart = null; return; } | |
const idx = Number(target.dataset.idx); | |
const [r2,c2] = indexToRC(idx); | |
const {r:r1,c:c1} = dragStart; | |
if (Math.abs(r1-r2)+Math.abs(c1-c2) === 1) { | |
swapTiles(r1,c1,r2,c2); | |
checkAndClearMatches(); | |
} | |
dragStart = null; | |
}); | |
/* ------------------------------------------------- | |
Ⅳ. スワップ & マッチ判定 | |
------------------------------------------------- */ | |
function swapTiles(r1,c1,r2,c2){ | |
[board[r1][c1], board[r2][c2]] = [board[r2][c2], board[r1][c1]]; | |
renderBoard(); | |
} | |
/* 同色ライン(4 以上)検出 */ | |
function findMatches() { | |
const toClear = new Set(); | |
const dirs = [[0,1],[1,0],[1,1],[1,-1]]; // 右・下・右下・左下 | |
for (let r = 0; r < boardSize; r++) { | |
for (let c = 0; c < boardSize; c++) { | |
const col = board[r][c]; | |
if (!col) continue; | |
for (const [dr,dc] of dirs) { | |
const line = []; | |
let rr = r, cc = c; | |
while (inside(rr,cc) && board[rr][cc] === col) { | |
line.push([rr,cc]); | |
rr += dr; cc += dc; | |
} | |
if (line.length >= 4) { | |
line.forEach(([sr,sc])=>toClear.add(`${sr}_${sc}`)); | |
} | |
} | |
} | |
} | |
return Array.from(toClear); | |
} | |
function inside(r,c){ return r>=0 && c>=0 && r<boardSize && c<boardSize; } | |
function rcToIndex(r,c){ return r*boardSize + c; } | |
function indexToRC(i){ return [Math.floor(i/boardSize), i%boardSize]; } | |
function removeTiles(cells) { | |
cells.forEach(key => { | |
const [r,c] = key.split('_').map(Number); | |
board[r][c] = null; | |
}); | |
} | |
/* 落下と補充 */ | |
function applyGravityAndFill() { | |
for (let c = 0; c < boardSize; c++) { | |
const column = []; | |
for (let r = boardSize-1; r >= 0; r--) { | |
if (board[r][c] !== null) column.push(board[r][c]); | |
} | |
let r = boardSize-1; | |
for (const col of column) { | |
board[r][c] = col; | |
r--; | |
} | |
while (r >= 0) { | |
board[r][c] = colors[Math.floor(Math.random()*colors.length)]; | |
r--; | |
} | |
} | |
} | |
/* 消去 & チェーン判定 */ | |
function checkAndClearMatches() { | |
if (isAnimating) return; | |
const matches = findMatches(); | |
// 消えるものが無い → 手数だけ増やす | |
if (matches.length === 0) { | |
moves++; updateScore(); | |
if (isFinished()) { | |
messageEl.textContent = `クリア! 手数 ${moves}、スコア ${score}`; | |
} | |
return; | |
} | |
/* ① エフェクト付与 */ | |
matches.forEach(key => { | |
const [r,c] = key.split('_').map(Number); | |
const idx = rcToIndex(r,c); | |
const tile = boardEl.querySelector(`.tile[data-idx='${idx}']`); | |
if (tile) tile.classList.add('removing'); | |
}); | |
/* ② アニメーション後に実際の削除・落下 */ | |
isAnimating = true; | |
const ANIM_MS = parseFloat(getComputedStyle(document.documentElement) | |
.getPropertyValue('--remove-duration')) * 1000; | |
setTimeout(() => { | |
removeTiles(matches); | |
applyGravityAndFill(); | |
const added = matches.length * 5; // 1 個=5 pt | |
score += added; | |
moves++; // 1 手としてカウント | |
renderBoard(); | |
isAnimating = false; | |
checkAndClearMatches(); // 連鎖 | |
updateScore(); | |
}, ANIM_MS); | |
} | |
/* フィールドがすべて空か判定 */ | |
function isFinished() { | |
for (let r = 0; r < boardSize; r++) { | |
for (let c = 0; c < boardSize; c++) { | |
if (board[r][c] !== null) return false; | |
} | |
} | |
return true; | |
} | |
/* ------------------------------------------------- | |
V. AI 自動プレイロジック | |
------------------------------------------------- */ | |
function findSwapThatCreatesMatch() { | |
const clone = () => board.map(row=>[...row]); | |
for (let r = 0; r < boardSize; r++) { | |
for (let c = 0; c < boardSize; c++) { | |
if (!board[r][c]) continue; | |
const dirs = [[0,1],[1,0],[0,-1],[-1,0]]; | |
for (const [dr,dc] of dirs) { | |
const nr=r+dr, nc=c+dc; | |
if (!inside(nr,nc) || !board[nr][nc]) continue; | |
const tmp = clone(); | |
[tmp[r][c], tmp[nr][nc]] = [tmp[nr][nc], tmp[r][c]]; | |
if (hasMatch(tmp)) return {r1:r,c1:c,r2:nr,c2:nc}; | |
} | |
} | |
} | |
return null; | |
} | |
function hasMatch(arr) { | |
const dirs = [[0,1],[1,0],[1,1],[1,-1]]; | |
for (let r = 0; r < boardSize; r++) { | |
for (let c = 0; c < boardSize; c++) { | |
const col = arr[r][c]; | |
if (!col) continue; | |
for (const [dr,dc] of dirs) { | |
let len = 0, rr=r, cc=c; | |
while (inside(rr,cc) && arr[rr][cc] === col) { | |
len++; rr+=dr; cc+=dc; | |
} | |
if (len >= 4) return true; | |
} | |
} | |
} | |
return false; | |
} | |
/* AI の 1 手 */ | |
function aiPlayOneMove() { | |
if (isAnimating || isFinished()) return; | |
const move = findSwapThatCreatesMatch(); | |
if (move) { | |
swapTiles(move.r1,move.c1,move.r2,move.c2); | |
checkAndClearMatches(); | |
} else { | |
// ランダムに隣接ペアをスワップ(マッチが作れないなら) | |
const r = Math.floor(Math.random()*boardSize); | |
const c = Math.floor(Math.random()*boardSize); | |
const dirs = [[0,1],[1,0],[0,-1],[-1,0]]; | |
const cand = dirs.map(([dr,dc])=>[r+dr,c+dc]) | |
.filter(([nr,nc])=>inside(nr,nc) && board[nr][nc]!=null); | |
if (cand.length) { | |
const [nr,nc] = cand[Math.floor(Math.random()*cand.length)]; | |
swapTiles(r,c,nr,nc); | |
checkAndClearMatches(); | |
} else { | |
moves++; updateScore(); | |
} | |
} | |
} | |
/* 自動プレイの開始 / 停止 */ | |
function startAutoPlay() { | |
autoPlayBtn.textContent = '停止'; | |
autoPlayBtn.dataset.state = 'running'; | |
if (autoPlayTimer) clearInterval(autoPlayTimer); | |
autoPlayTimer = setInterval(() => { | |
if (!isFinished()) aiPlayOneMove(); | |
else stopAutoPlay(); | |
}, playDelay); | |
} | |
function stopAutoPlay() { | |
if (autoPlayTimer) clearInterval(autoPlayTimer); | |
autoPlayTimer = null; | |
autoPlayBtn.textContent = '自動プレイ'; | |
autoPlayBtn.dataset.state = 'stopped'; | |
} | |
/* ------------------------------------------------- | |
VI. UI 更新・ボタン処理 | |
------------------------------------------------- */ | |
function updateScore() { | |
scoreEl.textContent = `スコア: ${score}`; | |
movesEl.textContent = `手数: ${moves}`; | |
} | |
/* ボタン処理 */ | |
newGameBtn.addEventListener('click', initBoard); | |
autoPlayBtn.addEventListener('click', () => { | |
if (autoPlayBtn.dataset.state === 'running') stopAutoPlay(); | |
else startAutoPlay(); | |
}); | |
/* 初回起動 */ | |
initBoard(); | |
</script> | |
</body> | |
</html> |