ColorLinkPuzzle / index.html
miya
modify Readme.md index.html
ca3ea91
<!DOCTYPE html>
<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>