|
|
| let soundEnabled = false;
|
| let titleBgm, bgm, sfxPut, sfxMatch, sfxGameover, sfxRotate, sfxWin, sfxDamage;
|
| let isPlayingGame = false;
|
|
|
| function createAudio(src) { const a = new Audio(src); a.preload = 'auto'; return a; }
|
|
|
| function ensureAudioCreated() {
|
| if (!bgm) {
|
| titleBgm = createAudio('static/title.mp3'); titleBgm.loop = true;
|
| bgm = createAudio('static/bgm.mp3'); bgm.loop = true;
|
| sfxPut = createAudio('static/put.mp3');
|
| sfxMatch = createAudio('static/match.mp3');
|
| sfxGameover = createAudio('static/gameover.mp3');
|
| sfxRotate = createAudio('static/rotate.mp3');
|
| sfxWin = createAudio('static/win.mp3');
|
| sfxDamage = createAudio('static/damage.mp3');
|
| }
|
| }
|
|
|
| function playSfx(audio, vol = 1.0) {
|
| if (!soundEnabled || !audio) return;
|
| const c = audio.cloneNode(); c.volume = vol; c.play().catch(() => { });
|
| }
|
|
|
| function bgmControl(state) {
|
| if (!soundEnabled || !titleBgm || !bgm) return;
|
| titleBgm.pause(); bgm.pause();
|
| if (state === 'title') { titleBgm.currentTime = 0; titleBgm.volume = 0.4; titleBgm.play().catch(() => { }); }
|
| else if (state === 'game') { bgm.currentTime = 0; bgm.volume = 0.4; bgm.play().catch(() => { }); }
|
| }
|
|
|
| document.getElementById('sound-toggle').addEventListener('click', () => {
|
| ensureAudioCreated();
|
| soundEnabled = !soundEnabled;
|
| document.getElementById('sound-toggle').textContent = soundEnabled ? '🔊' : '🔇';
|
| if (soundEnabled) {
|
| if (isPlayingGame && (!p1 || !p1.isGameOver)) bgmControl('game');
|
| else if (!isPlayingGame) bgmControl('title');
|
| } else {
|
| bgmControl('stop');
|
| }
|
| });
|
|
|
|
|
| const COLS = 8, ROWS = 8, COLORS = 4, MIN_MATCH = 3;
|
| const tileImages = ['static/tile1.png', 'static/tile2.png', 'static/tile3.png', 'static/tile4.png', 'static/tile5.png'];
|
| const tileImg = (i) => { const img = document.createElement('img'); img.src = tileImages[i]; img.draggable = false; return img; };
|
| const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
|
| let p1, p2;
|
| let gameMode = '1p';
|
| let currentTurn = 1;
|
| let isDemoActive = true;
|
|
|
| class QuadGame {
|
| constructor(rootEl, isAI, isDemo = false) {
|
| this.root = rootEl; this.isAI = isAI; this.isDemo = isDemo;
|
| this.boardEl = rootEl.querySelector('.board');
|
| this.handEl = rootEl.querySelector('.hand-area');
|
| this.scoreEl = rootEl.querySelector('.score-display');
|
| this.msgEl = rootEl.querySelector('.message-pop');
|
| this.garbageUi = rootEl.querySelector('.garbage-ui');
|
| this.garbageBar = rootEl.querySelector('.garbage-bar');
|
| this.garbageText = rootEl.querySelector('.garbage-text');
|
|
|
| this.board = []; this.hand = [];
|
| this.score = 0; this.combo = 0;
|
| this.isProcessing = false; this.isGameOver = false; this.aiFetching = false;
|
| this.opponent = null;
|
|
|
| this.pendingGarbage = 0;
|
| this.garbageTimerMax = 6000;
|
| this.garbageTimer = 0;
|
|
|
| this.dragging = null; this.dragEl = null; this.ghostCells = [];
|
| }
|
|
|
| init() {
|
| this.board = Array.from({ length: ROWS }, () => Array(COLS).fill(-1));
|
| this.hand = [this.genPiece(), this.genPiece(), this.genPiece()];
|
| this.score = 0; this.combo = 0; this.pendingGarbage = 0;
|
| this.isGameOver = false; this.isProcessing = false;
|
|
|
| this.boardEl.innerHTML = '';
|
| for (let i = 0; i < 64; i++) {
|
| const c = document.createElement('div'); c.className = 'cell';
|
| this.boardEl.appendChild(c);
|
| }
|
| this.updateHandUI();
|
| if (this.scoreEl) this.scoreEl.textContent = '0';
|
| if (this.garbageUi) this.garbageUi.style.display = 'none';
|
| }
|
|
|
| genPiece() {
|
| let p;
|
| while (true) {
|
| p = [[Math.floor(Math.random() * COLORS), Math.floor(Math.random() * COLORS)],
|
| [Math.floor(Math.random() * COLORS), Math.floor(Math.random() * COLORS)]];
|
| if (!this.hasMatchSync(p)) return p;
|
| }
|
| }
|
|
|
| hasMatchSync(p) {
|
| for (let col = 0; col < COLORS; col++) {
|
| let count = 0;
|
| for (let r = 0; r < 2; r++) for (let c = 0; c < 2; c++) if (p[r][c] === col) count++;
|
| if (count >= 3) return true;
|
| }
|
| return false;
|
| }
|
|
|
| getCellEl(r, c) { return this.boardEl.children[r * COLS + c]; }
|
|
|
| renderBoard() {
|
| for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
|
| const cell = this.getCellEl(r, c);
|
| const ex = cell.querySelector('.block'); if (ex) ex.remove();
|
| if (this.board[r][c] >= 0) {
|
| const blk = document.createElement('div'); blk.className = 'block';
|
| blk.appendChild(tileImg(this.board[r][c]));
|
| cell.appendChild(blk);
|
| }
|
| }
|
| }
|
|
|
| updateHandUI() {
|
| this.handEl.innerHTML = '';
|
| this.hand.forEach((p, idx) => {
|
| const el = document.createElement('div');
|
| el.className = 'hand-piece' + (!this.isAI ? ' interactive' : '');
|
| if (!p) { el.style.visibility = 'hidden'; this.handEl.appendChild(el); return; }
|
|
|
| [[0, 0], [0, 1], [1, 0], [1, 1]].forEach(([r, c]) => {
|
| const w = document.createElement('div'); w.className = 'hand-cell-wrapper';
|
| const cl = document.createElement('div'); cl.className = 'hand-cell';
|
| cl.style.left = '0'; cl.style.top = '0';
|
| cl.appendChild(tileImg(p[r][c])); w.appendChild(cl); el.appendChild(w);
|
| });
|
|
|
| if (!this.isAI) {
|
| el.addEventListener('mousedown', e => this.onHandDown(e, idx));
|
| el.addEventListener('touchstart', e => this.onHandDown(e, idx), { passive: false });
|
| }
|
| this.handEl.appendChild(el);
|
| });
|
| this.checkValidMoves();
|
| }
|
|
|
| rotateCW(idx) {
|
| const p = this.hand[idx];
|
| this.hand[idx] = [[p[1][0], p[0][0]], [p[1][1], p[0][1]]];
|
| }
|
|
|
| async animateRotation(idx) {
|
| if (!this.isDemo) playSfx(sfxRotate, 0.4);
|
| this.rotateCW(idx);
|
| this.updateHandUI();
|
| }
|
|
|
| canPlace(piece, sr, sc) {
|
| if (!piece) return false;
|
| for (let r = 0; r < 2; r++) for (let c = 0; c < 2; c++) {
|
| const br = sr + r, bc = sc + c;
|
| if (br < 0 || br >= ROWS || bc < 0 || bc >= COLS || this.board[br][bc] >= 0) return false;
|
| }
|
| return true;
|
| }
|
|
|
| checkValidMoves() {
|
| if (this.isGameOver) return;
|
| let any = false;
|
| const els = this.handEl.querySelectorAll('.hand-piece');
|
|
|
|
|
| const isMyTurn = (gameMode !== 'vs-turn') || (this.isAI ? currentTurn === 2 : currentTurn === 1);
|
|
|
| this.hand.forEach((p, i) => {
|
| if (!p) return;
|
| let ok = false;
|
| if (isMyTurn) {
|
| for (let r = 0; r < 7; r++) for (let c = 0; c < 7; c++) if (this.canPlace(p, r, c)) ok = true;
|
| }
|
| if (ok) any = true;
|
| if (els[i]) els[i].classList.toggle('disabled', !ok);
|
| });
|
|
|
|
|
| let trueAny = false;
|
| this.hand.forEach(p => {
|
| if (!p) return;
|
| for (let r = 0; r < 7; r++) for (let c = 0; c < 7; c++) if (this.canPlace(p, r, c)) trueAny = true;
|
| });
|
|
|
| if (!trueAny && this.hand.some(p => p !== null) && !this.isProcessing) {
|
| this.triggerGameOver();
|
| }
|
| }
|
|
|
| onHandDown(e, idx) {
|
| if (this.isProcessing || this.isGameOver || this.isDemo) return;
|
| if (gameMode === 'vs-turn' && currentTurn !== 1) return;
|
|
|
| const t = e.touches ? e.touches[0] : e;
|
| let startX = t.clientX, startY = t.clientY, moved = false;
|
|
|
| const onMove = ev => {
|
| const tt = ev.touches ? ev.touches[0] : ev;
|
| if (!moved && Math.hypot(tt.clientX - startX, tt.clientY - startY) > 8) {
|
| moved = true;
|
| this.boardRect = this.boardEl.getBoundingClientRect();
|
| this.cellSize = this.boardRect.width / COLS;
|
| this.beginDrag(idx, tt.clientX, tt.clientY);
|
| }
|
| if (moved) this.moveDrag(tt.clientX, tt.clientY);
|
| };
|
|
|
| const onUp = ev => {
|
| document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp);
|
| document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onUp);
|
| if (!moved) { this.animateRotation(idx); }
|
| else { const tt = ev.changedTouches ? ev.changedTouches[0] : ev; this.finishDrag(tt.clientX, tt.clientY); }
|
| };
|
|
|
| document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
|
| document.addEventListener('touchmove', onMove, { passive: false }); document.addEventListener('touchend', onUp);
|
| }
|
|
|
| beginDrag(idx, x, y) {
|
| this.dragging = { idx, piece: this.hand[idx] };
|
| this.dragEl = document.createElement('div'); this.dragEl.className = 'dragging-piece';
|
| for (let r = 0; r < 2; r++) for (let c = 0; c < 2; c++) {
|
| const cl = document.createElement('div'); cl.className = 'drag-cell'; cl.appendChild(tileImg(this.hand[idx][r][c])); this.dragEl.appendChild(cl);
|
| }
|
| document.body.appendChild(this.dragEl);
|
| this.handEl.children[idx].style.opacity = '0.3';
|
| this.moveDrag(x, y);
|
| }
|
|
|
| moveDrag(x, y) {
|
| if (!this.dragEl) return;
|
| this.dragEl.style.left = (x - 45) + 'px'; this.dragEl.style.top = (y - 45) + 'px';
|
| const relX = x - this.boardRect.left, relY = y - this.boardRect.top;
|
| const c = Math.round(relX / this.cellSize - 1), r = Math.round(relY / this.cellSize - 1);
|
|
|
| this.ghostCells.forEach(el => { el.classList.remove('highlight'); const g = el.querySelector('.ghost'); if (g) g.remove(); });
|
| this.ghostCells = [];
|
| if (this.canPlace(this.dragging.piece, r, c)) {
|
| for (let ir = 0; ir < 2; ir++) for (let ic = 0; ic < 2; ic++) {
|
| const el = this.getCellEl(r + ir, c + ic); el.classList.add('highlight');
|
| const g = document.createElement('div'); g.className = 'ghost'; g.appendChild(tileImg(this.dragging.piece[ir][ic]));
|
| el.appendChild(g); this.ghostCells.push(el);
|
| }
|
| }
|
| }
|
|
|
| finishDrag(x, y) {
|
| const relX = x - this.boardRect.left, relY = y - this.boardRect.top;
|
| const c = Math.round(relX / this.cellSize - 1), r = Math.round(relY / this.cellSize - 1);
|
| if (this.canPlace(this.dragging.piece, r, c)) {
|
| this.placePiece(this.dragging.idx, r, c);
|
| } else {
|
| this.handEl.children[this.dragging.idx].style.opacity = '1';
|
| }
|
| this.ghostCells.forEach(el => { el.classList.remove('highlight'); const g = el.querySelector('.ghost'); if (g) g.remove(); });
|
| this.ghostCells = [];
|
| if (this.dragEl) { this.dragEl.remove(); this.dragEl = null; }
|
| this.dragging = null;
|
| }
|
|
|
| placePiece(idx, r, c) {
|
| if (!this.isDemo) playSfx(sfxPut, 0.6);
|
| const p = this.hand[idx];
|
| for (let ir = 0; ir < 2; ir++) for (let ic = 0; ic < 2; ic++) this.board[r + ir][c + ic] = p[ir][ic];
|
| this.hand[idx] = this.genPiece();
|
| this.combo = 0; this.isProcessing = true;
|
| this.renderBoard(); this.updateHandUI();
|
| setTimeout(() => this.processMatches(), 150);
|
| }
|
|
|
| processMatches() {
|
| const vis = Array.from({ length: ROWS }, () => Array(COLS).fill(false));
|
| const toRemove = new Set();
|
|
|
| for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
|
| if (this.board[r][c] >= 0 && this.board[r][c] <= 3 && !vis[r][c]) {
|
| const color = this.board[r][c], grp = [], q = [[r, c]]; vis[r][c] = true;
|
| while (q.length > 0) {
|
| const [cr, cc] = q.shift(); grp.push([cr, cc]);
|
| [[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
|
| const nr = cr + dr, nc = cc + dc;
|
| if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS && !vis[nr][nc] && this.board[nr][nc] === color) {
|
| vis[nr][nc] = true; q.push([nr, nc]);
|
| }
|
| });
|
| }
|
| if (grp.length >= MIN_MATCH) grp.forEach(([gr, gc]) => toRemove.add(`${gr},${gc}`));
|
| }
|
| }
|
|
|
| if (toRemove.size > 0) {
|
| if (!this.isDemo) playSfx(sfxMatch, 0.7);
|
| this.combo++;
|
| const garbageSet = new Set();
|
| toRemove.forEach(k => {
|
| const [r, c] = k.split(',').map(Number);
|
| [[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
|
| const nr = r + dr, nc = c + dc;
|
| if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS && this.board[nr][nc] === 4) garbageSet.add(`${nr},${nc}`);
|
| });
|
| });
|
| garbageSet.forEach(k => toRemove.add(k));
|
|
|
| const count = toRemove.size;
|
| this.score += count * 10 * this.combo;
|
| if (this.scoreEl) this.scoreEl.textContent = this.score;
|
| this.showPop(`${count} BLOCKS!` + (this.combo > 1 ? `<br>${this.combo} COMBO!` : ''));
|
|
|
| if (this.opponent && (this.combo >= 2 || count >= 4)) {
|
| const atk = Math.max(1, count - 3) + (this.combo - 1) * 2;
|
| this.sendGarbage(atk);
|
| }
|
|
|
| toRemove.forEach(k => {
|
| const [r, c] = k.split(',').map(Number);
|
| const el = this.getCellEl(r, c).querySelector('.block');
|
| if (el) el.classList.add('clearing');
|
| });
|
|
|
| setTimeout(() => {
|
| toRemove.forEach(k => { const [r, c] = k.split(',').map(Number); this.board[r][c] = -1; });
|
| this.renderBoard();
|
| setTimeout(() => this.processMatches(), 200);
|
| }, 400);
|
|
|
| } else {
|
| this.isProcessing = false;
|
| this.checkValidMoves();
|
|
|
|
|
| if (gameMode === 'vs-turn' && !this.isGameOver) {
|
|
|
| switchTurn();
|
| } else if (this.isAI && !this.isGameOver && gameMode !== 'vs-turn') {
|
|
|
| setTimeout(() => requestAI(this), 300);
|
| }
|
| }
|
| }
|
|
|
| showPop(txt) {
|
| if (!this.msgEl) return;
|
| this.msgEl.innerHTML = txt;
|
| this.msgEl.style.transition = 'none'; this.msgEl.style.opacity = '1'; this.msgEl.style.transform = 'translate(-50%,-50%) scale(0.5)';
|
| requestAnimationFrame(() => {
|
| this.msgEl.style.transition = 'transform 0.3s cubic-bezier(0.175,0.885,0.32,1.275), opacity 0.5s';
|
| this.msgEl.style.transform = 'translate(-50%,-50%) scale(1)';
|
| setTimeout(() => this.msgEl.style.opacity = '0', 800);
|
| });
|
| }
|
|
|
| sendGarbage(amount) {
|
| if (this.pendingGarbage > 0) {
|
| if (this.pendingGarbage >= amount) { this.pendingGarbage -= amount; amount = 0; }
|
| else { amount -= this.pendingGarbage; this.pendingGarbage = 0; }
|
| this.garbageTimer = this.garbageTimerMax;
|
| this.updateGarbageUI();
|
| }
|
|
|
| if (amount > 0 && this.opponent) {
|
| if (gameMode === 'vs-turn') {
|
|
|
| this.opponent.receiveGarbageInstant(amount);
|
| } else {
|
| this.opponent.receiveGarbage(amount);
|
| }
|
| }
|
| }
|
|
|
| receiveGarbage(amount) {
|
| this.pendingGarbage += amount;
|
| this.garbageTimer = this.garbageTimerMax;
|
| this.updateGarbageUI();
|
| }
|
|
|
| receiveGarbageInstant(amount) {
|
| this.pendingGarbage += amount;
|
| this.dropGarbage();
|
| }
|
|
|
| updateGarbageUI() {
|
| if (!this.garbageUi) return;
|
| if (gameMode === 'vs-turn') { this.garbageUi.style.display = 'none'; return; }
|
|
|
| if (this.pendingGarbage > 0) {
|
| this.garbageUi.style.display = 'block';
|
| this.garbageText.textContent = `+${this.pendingGarbage}`;
|
| this.garbageBar.style.transform = `scaleX(${Math.max(0, this.garbageTimer / this.garbageTimerMax)})`;
|
| } else {
|
| this.garbageUi.style.display = 'none';
|
| }
|
| }
|
|
|
| tick(dt) {
|
| if (this.isGameOver || this.isProcessing || gameMode === 'vs-turn') return;
|
| if (this.pendingGarbage > 0) {
|
| this.garbageTimer -= dt;
|
| this.updateGarbageUI();
|
| if (this.garbageTimer <= 0) {
|
| this.dropGarbage();
|
| }
|
| }
|
| }
|
|
|
| dropGarbage() {
|
| if (!this.isDemo) playSfx(sfxDamage, 0.8);
|
| let empty = [];
|
| for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) if (this.board[r][c] === -1) empty.push({ r, c });
|
| for (let i = empty.length - 1; i > 0; i--) {
|
| const j = Math.floor(Math.random() * (i + 1));
|
| [empty[i], empty[j]] = [empty[j], empty[i]];
|
| }
|
| const drop = Math.min(this.pendingGarbage, empty.length);
|
| for (let i = 0; i < drop; i++) this.board[empty[i].r][empty[i].c] = 4;
|
|
|
| this.pendingGarbage -= drop;
|
| if (this.pendingGarbage < 0) this.pendingGarbage = 0;
|
| this.updateGarbageUI();
|
| this.renderBoard();
|
| this.checkValidMoves();
|
| }
|
|
|
| triggerGameOver() {
|
| this.isGameOver = true;
|
| if (this.isDemo) {
|
| setTimeout(() => {
|
| if (!isPlayingGame && isDemoActive) {
|
| this.init();
|
| requestAI(this);
|
| }
|
| }, 2000);
|
| return;
|
| }
|
| checkGlobalGameOver();
|
| }
|
| }
|
|
|
|
|
| let lastTime = 0;
|
| let demoGame = null;
|
|
|
| window.onload = () => {
|
| demoGame = new QuadGame(document.getElementById('demo-area'), true, true);
|
| demoGame.init();
|
| requestAI(demoGame);
|
| };
|
|
|
| document.getElementById('btn-1p').onclick = () => startGame('1p');
|
| document.getElementById('btn-vs-real').onclick = () => startGame('vs-real');
|
| document.getElementById('btn-vs-turn').onclick = () => startGame('vs-turn');
|
|
|
| function startGame(mode) {
|
| isDemoActive = false;
|
| isPlayingGame = true;
|
| gameMode = mode;
|
| currentTurn = 1;
|
|
|
| ensureAudioCreated();
|
| bgmControl('game');
|
|
|
| document.getElementById('title-screen').classList.add('hidden');
|
| document.getElementById('main-wrapper').classList.add('show');
|
|
|
| p1 = new QuadGame(document.getElementById('p1-area'), false);
|
| p1.init();
|
|
|
| const indicator = document.getElementById('turn-indicator');
|
|
|
| if (mode === 'vs-real' || mode === 'vs-turn') {
|
| document.getElementById('p2-area').style.display = 'flex';
|
| p2 = new QuadGame(document.getElementById('p2-area'), true);
|
| p2.init();
|
| p1.opponent = p2; p2.opponent = p1;
|
|
|
| if (mode === 'vs-turn') {
|
| indicator.style.display = 'block';
|
| updateTurnUI();
|
| } else {
|
| indicator.style.display = 'none';
|
| setTimeout(() => requestAI(p2), 1000);
|
| }
|
| } else {
|
| document.getElementById('p2-area').style.display = 'none';
|
| indicator.style.display = 'none';
|
| }
|
|
|
| lastTime = performance.now();
|
| requestAnimationFrame(gameLoop);
|
| }
|
|
|
| function gameLoop(time) {
|
| const dt = time - lastTime; lastTime = time;
|
| if (p1 && !p1.isGameOver) p1.tick(dt);
|
| if (p2 && !p2.isGameOver) p2.tick(dt);
|
| requestAnimationFrame(gameLoop);
|
| }
|
|
|
| function switchTurn() {
|
| if (p1.isGameOver || p2.isGameOver) return;
|
| currentTurn = currentTurn === 1 ? 2 : 1;
|
| updateTurnUI();
|
|
|
|
|
| p1.checkValidMoves();
|
| p2.checkValidMoves();
|
|
|
| if (currentTurn === 2) {
|
| setTimeout(() => requestAI(p2), 800);
|
| }
|
| }
|
|
|
| function updateTurnUI() {
|
| const ind = document.getElementById('turn-indicator');
|
| const p1a = document.getElementById('p1-area');
|
| const p2a = document.getElementById('p2-area');
|
|
|
| if (currentTurn === 1) {
|
| ind.textContent = "PLAYER'S TURN";
|
| ind.style.color = "#4d96ff";
|
| p1a.classList.remove('waiting');
|
| p2a.classList.add('waiting');
|
| } else {
|
| ind.textContent = "AI'S TURN";
|
| ind.style.color = "#ff6b6b";
|
| p1a.classList.add('waiting');
|
| p2a.classList.remove('waiting');
|
| }
|
| }
|
|
|
| function checkGlobalGameOver() {
|
| if (gameMode !== '1p') {
|
| if (p1.isGameOver || p2.isGameOver) {
|
| bgmControl('stop');
|
| if (p1.isGameOver) {
|
| document.getElementById('result-title').textContent = "YOU LOSE...";
|
| document.getElementById('result-title').style.color = "#ff6b6b";
|
| playSfx(sfxGameover, 0.8);
|
| } else {
|
| document.getElementById('result-title').textContent = "YOU WIN!!";
|
| document.getElementById('result-title').style.color = "#6bcb77";
|
| playSfx(sfxWin, 0.8);
|
| }
|
| document.getElementById('game-over-overlay').classList.add('show');
|
| }
|
| } else {
|
| if (p1.isGameOver) {
|
| bgmControl('stop');
|
| playSfx(sfxGameover, 0.8);
|
| document.getElementById('game-over-overlay').classList.add('show');
|
| }
|
| }
|
| }
|
|
|
| async function requestAI(gameObj) {
|
| if (gameObj.isGameOver || gameObj.isProcessing || gameObj.aiFetching) return;
|
| if (!gameObj.hand.some(p => p !== null)) return;
|
| if (gameObj.isDemo && !isDemoActive) return;
|
|
|
|
|
|
|
| let temp = 0.0;
|
| if (!gameObj.isDemo) {
|
| temp = gameMode === 'vs-turn' ? 20.0 : 35.0;
|
| }
|
|
|
| gameObj.aiFetching = true;
|
| try {
|
| const res = await fetch('/api/ai_move', {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({
|
| board: gameObj.board,
|
| hand: gameObj.hand,
|
| temperature: temp
|
| })
|
| });
|
| const move = await res.json();
|
| gameObj.aiFetching = false;
|
|
|
| if (gameObj.isDemo && !isDemoActive) return;
|
|
|
| if (move && move.piece_idx !== undefined && !gameObj.isGameOver && !gameObj.isProcessing) {
|
|
|
|
|
|
|
| let thinkTime = gameObj.isDemo ? 300 : (gameMode === 'vs-real' ? 1200 + Math.random() * 800 : 600);
|
| await sleep(thinkTime);
|
|
|
| if (gameObj.isGameOver || gameObj.isProcessing || (gameObj.isDemo && !isDemoActive)) return;
|
|
|
| for (let i = 0; i < move.rotations; i++) {
|
| gameObj.animateRotation(move.piece_idx);
|
| await sleep(gameObj.isDemo ? 100 : 250);
|
| }
|
|
|
| if (gameObj.canPlace(gameObj.hand[move.piece_idx], move.r, move.c)) {
|
| gameObj.placePiece(move.piece_idx, move.r, move.c);
|
| }
|
| }
|
| } catch (e) {
|
| gameObj.aiFetching = false;
|
| setTimeout(() => { if (!gameObj.isGameOver && !gameObj.isProcessing && (!gameObj.isDemo || isDemoActive)) requestAI(gameObj); }, 2000);
|
| }
|
| } |