import { Api } from '../modules/api.js'; import { Game } from '../modules/game.js'; import { CommonCanvas } from '../modules/canvas_utils.js'; import { UI } from '../modules/ui.js'; document.addEventListener('DOMContentLoaded', () => { const canvas = document.getElementById('spiderCanvas'); if (!canvas) return; // 1000x1000 논리 크기 const V = 1000, CARD_W = 80, CARD_H = 112, GAP_X = 90, OVER_Y = 30; const common = CommonCanvas.init(canvas, V, V); const { ctx, resize, getCoords, isInside, showSuccessOverlay, fillRoundRect } = common; let currentGame = null, draggedCards = [], dragOff = {x:0, y:0}; let isProcessing = false; const cardBack = new Image(); cardBack.src = '/css/images/card-back.png'; const ui = { start: { x: 400, y: 480, w: 200, h: 60, label: "새 게임 시작" }, stock: { x: 880, y: 850, w: CARD_W, h: CARD_H }, undo: { x: 50, y: 900, w: 120, h: 50, label: "실행 취소" } }; function draw() { ctx.clearRect(0, 0, V, V); ctx.fillStyle = "#006633"; ctx.fillRect(0, 0, V, V); // 펠트색 if (!currentGame) drawMenu(); else drawGame(); } function drawMenu() { ctx.fillStyle = "#fff"; ctx.font = "bold 60px Arial"; ctx.textAlign = "center"; ctx.fillText("SPIDER SOLITAIRE", V/2, 250); fillRoundRect(ui.start.x, ui.start.y, ui.start.w, ui.start.h, 10, "#4CAF50"); ctx.fillStyle = "#fff"; ctx.font = "bold 24px Arial"; ctx.fillText(ui.start.label, ui.start.x + 100, ui.start.y + 38); } function drawGame() { // Tableau (테이블 카드) currentGame.tableau.forEach((stack, sIdx) => { stack.forEach((card, cIdx) => { if (draggedCards.includes(card)) return; // 드래그 중인 카드는 나중에 const x = 50 + sIdx*GAP_X, y = 120 + cIdx*OVER_Y; card.currentX = x; card.currentY = y; // 좌표 저장 drawCard(card, x, y); }); }); // Stock (덱) if (currentGame.stock.length > 0) ctx.drawImage(cardBack, ui.stock.x, ui.stock.y, CARD_W, CARD_H); // Foundation (완성된 세트) currentGame.foundation.forEach((set, i) => { drawCard(set[set.length-1], 20 + i*35, 850); }); // UI 버튼 fillRoundRect(ui.undo.x, ui.undo.y, ui.undo.w, ui.undo.h, 5, "#ff9800"); ctx.fillStyle = "#fff"; ctx.font = "16px Arial"; ctx.textAlign = "center"; ctx.fillText(ui.undo.label, ui.undo.x + 60, ui.undo.y + 30); // 드래그 중인 카드 (최상단) if (draggedCards.length > 0) { draggedCards.forEach((c, i) => drawCard(c, c.drawX, c.drawY + i*OVER_Y)); } } function drawCard(card, x, y) { if (!card.isFaceUp) { ctx.drawImage(cardBack, x, y, CARD_W, CARD_H); return; } fillRoundRect(x, y, CARD_W, CARD_H, 5, "#fff"); ctx.strokeStyle = "#000"; ctx.lineWidth = 1; ctx.strokeRect(x, y, CARD_W, CARD_H); const isRed = (card.suit === 'heart' || card.suit === 'diamond'); ctx.fillStyle = isRed ? "#d32f2f" : "#000"; ctx.font = "bold 18px Arial"; ctx.textAlign = "left"; ctx.fillText(getRankStr(card.rank), x+6, y+22); ctx.font = "24px Arial"; ctx.textAlign = "center"; ctx.fillText(getSuitStr(card.suit), x+CARD_W/2, y+CARD_H/2+5); } function getRankStr(r) { return r==1?'A':r==11?'J':r==12?'Q':r==13?'K':r; } function getSuitStr(s) { return {spade:'♠',heart:'♥',club:'♣',diamond:'♦'}[s]||''; } // --- 게임 로직 --- async function start() { isProcessing = true; try { // 필수 파라미터 포함 요청 currentGame = await Api.request('/puzzle/spider/new?numSuits=1&numCards=4,3'); if(!currentGame.foundation) currentGame.foundation = []; if(!currentGame.moves) currentGame.moves = 0; draw(); } catch(e) { console.error(e); } isProcessing = false; } // 카드 딜링 function deal() { if (currentGame.stock.length === 0) return; saveUndoState(); const dealCards = currentGame.stock.splice(0, 10); dealCards.forEach((c, i) => { c.isFaceUp = true; currentGame.tableau[i].push(c); }); currentGame.moves++; checkFoundation(); draw(); } // 드래그 시작 판정 function findDrag(p) { // 역순 탐색 (위쪽 카드부터) for (let sIdx=9; sIdx>=0; sIdx--) { const stack = currentGame.tableau[sIdx]; for (let cIdx=stack.length-1; cIdx>=0; cIdx--) { const card = stack[cIdx]; if (!card.isFaceUp) continue; // 카드 영역 확인 if (p.x >= card.currentX && p.x <= card.currentX+CARD_W && p.y >= card.currentY && p.y <= card.currentY+CARD_H) { // 하단 겹침 고려 단순화 // 이동 가능 여부 체크 const moving = getMovableStack(stack, cIdx); if (moving) { draggedCards = moving; draggedCards.sIdx = sIdx; dragOff.x = p.x - card.currentX; dragOff.y = p.y - card.currentY; moving.forEach(c => { c.drawX = c.currentX; c.drawY = c.currentY; }); } return; } } } } // 규칙: 같은 무늬, 연속된 숫자만 묶음 이동 가능 function getMovableStack(stack, cIdx) { const sub = stack.slice(cIdx); for(let i=0; i 0) srcStack[srcStack.length-1].isFaceUp = true; destStack.push(...draggedCards); currentGame.moves++; checkFoundation(); } } draggedCards = []; } // 세트 완성 체크 (K...A) function checkFoundation() { currentGame.tableau.forEach(stack => { if (stack.length < 13) return; // 끝에서 13장 검사 const suffix = stack.slice(stack.length-13); let isSeq = true; for(let i=0; i<12; i++) { if (!suffix[i].isFaceUp || suffix[i].suit !== suffix[i+1].suit || suffix[i].rank !== suffix[i+1].rank+1) { isSeq = false; break; } } if (isSeq) { // 완성! stack.splice(stack.length-13, 13); if (stack.length>0) stack[stack.length-1].isFaceUp = true; currentGame.foundation.push(suffix); // 게임 클리어 체크 if (currentGame.foundation.length === 8) { showSuccessOverlay({ title: "SPIDER CLEAR!", scoreLabel: "MOVES", scoreValue: currentGame.moves }); setTimeout(() => Game.showSuccessModal({ gameType: 'SPIDER', primaryScore: currentGame.moves }), 2500); } } }); } // Undo 관련 function saveUndoState() { if (!currentGame.history) currentGame.history = []; const state = JSON.parse(JSON.stringify({ t: currentGame.tableau, s: currentGame.stock, f: currentGame.foundation, m: currentGame.moves })); currentGame.history.push(state); if (currentGame.history.length > 10) currentGame.history.shift(); } function handleUndo() { if (!currentGame || !currentGame.history || currentGame.history.length===0) return; const prev = currentGame.history.pop(); currentGame.tableau = prev.t; currentGame.stock = prev.s; currentGame.foundation = prev.f; currentGame.moves = prev.m; } // 입력 이벤트 canvas.addEventListener('mousedown', e => { const p = getCoords(e); if (!currentGame) { if(isInside(p, ui.start)) start(); } else { if (isInside(p, ui.stock)) deal(); else if (isInside(p, ui.undo)) handleUndo(); else findDrag(p); } draw(); }); canvas.addEventListener('mousemove', e => { if (draggedCards.length > 0) { const p = getCoords(e); draggedCards.forEach(c => { c.drawX = p.x - dragOff.x; c.drawY = p.y - dragOff.y; }); draw(); } }); window.addEventListener('mouseup', e => { if (draggedCards.length > 0) { handleDrop(getCoords(e)); draw(); } }); cardBack.onload = () => { resize(); draw(); }; });