257 lines
9.7 KiB
JavaScript
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(); };
|
|
}); |