2025-12-26 17:31:21 +09:00

257 lines
9.7 KiB
JavaScript

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<sub.length-1; i++) {
if (sub[i].suit !== sub[i+1].suit || sub[i].rank !== sub[i+1].rank + 1) return null;
}
return sub;
}
// 드롭 처리
function handleDrop(p) {
let bestDist = 9999, targetIdx = -1;
// 가장 가까운 컬럼 찾기 (X축 기준)
for (let i=0; i<10; i++) {
const cx = 50 + i*GAP_X + CARD_W/2;
const dist = Math.abs(p.x - cx);
if (dist < CARD_W && dist < bestDist) { bestDist = dist; targetIdx = i; }
}
if (targetIdx !== -1) {
const destStack = currentGame.tableau[targetIdx];
// 이동 규칙: 비어있거나, 타겟 카드가 이동할 카드보다 1 커야 함
let valid = false;
if (destStack.length === 0) valid = true;
else {
const top = destStack[destStack.length-1];
const moving = draggedCards[0];
if (top.rank === moving.rank + 1) valid = true;
}
if (valid) {
saveUndoState();
const srcStack = currentGame.tableau[draggedCards.sIdx];
srcStack.splice(srcStack.length - draggedCards.length, draggedCards.length);
if (srcStack.length > 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(); };
});