...
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 470 KiB |
@@ -0,0 +1,40 @@
|
||||
/* src/main/resources/static/css/spider.css */
|
||||
|
||||
/*
|
||||
* 전체 게임 컨테이너 (Canvas를 담는 역할)
|
||||
* - 기존 블로그 레이아웃을 해치지 않도록 최소한의 스타일만 적용
|
||||
*/
|
||||
#game-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #008000;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/*
|
||||
* 캔버스 스타일
|
||||
* - 배경을 초록색으로 설정하고, 테두리를 추가
|
||||
* - 화면 크기에 맞춰 비율을 유지하며 최대 크기 제한
|
||||
*/
|
||||
#gameCanvas {
|
||||
background-color: #008000;
|
||||
border: 2px solid #fff;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/*
|
||||
* 카드 이동 애니메이션 (Canvas 로직에서는 사용하지 않지만, 향후 재사용을 위해 남겨둠)
|
||||
*/
|
||||
@keyframes moveCard {
|
||||
from {
|
||||
transform: translate(var(--fromX), var(--fromY));
|
||||
}
|
||||
to {
|
||||
transform: translate(var(--toX), var(--toY));
|
||||
}
|
||||
}
|
||||
@@ -267,7 +267,7 @@ function gotoPuzzleUpload() {
|
||||
}
|
||||
|
||||
|
||||
function gotoPuzzleUpload() {
|
||||
function gotoSudoKuGen() {
|
||||
document.location.replace(getMainPath()+"/puzzle/sudoku_gen.bs")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,754 @@
|
||||
// src/main/resources/static/js/spider.js
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// === 1. 게임 상태 및 Canvas 설정 ===
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let gameId = null;
|
||||
let currentGame = null;
|
||||
const API_BASE_URL = '/spider';
|
||||
|
||||
let cardWidth = 0;
|
||||
let cardHeight = 0;
|
||||
let cardOverlapY = 0;
|
||||
|
||||
let isAnimating = false;
|
||||
let isDragging = false;
|
||||
let draggedCards = []; // 🔴 드래그 중인 카드 묶음 (배열)
|
||||
let dragOffsetX = 0;
|
||||
let dragOffsetY = 0;
|
||||
let lastTapTime = 0;
|
||||
|
||||
let animatedCard = null;
|
||||
let animationProgress = 0;
|
||||
let shakeOffset = 0;
|
||||
let dimOpacity = 0;
|
||||
let shakenCard = null;
|
||||
|
||||
// 카드 뒷면 이미지 로드
|
||||
const cardBackImage = new Image();
|
||||
cardBackImage.src = '../assets/css/images/card-back.png';
|
||||
|
||||
let assetsLoaded = false;
|
||||
cardBackImage.onload = () => {
|
||||
assetsLoaded = true;
|
||||
startNewGame();
|
||||
};
|
||||
|
||||
// Canvas 크기 조절
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth * 0.9;
|
||||
canvas.height = window.innerHeight * 0.9;
|
||||
cardWidth = canvas.width / 12;
|
||||
cardHeight = cardWidth * 1.4;
|
||||
cardOverlapY = cardHeight * 0.2;
|
||||
}
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
resizeCanvas();
|
||||
|
||||
// === 2. 렌더링 및 게임 루프 ===
|
||||
|
||||
|
||||
/**
|
||||
* 🔴 추가: 스톡 더미와 파운데이션 영역을 그리는 로직
|
||||
*/
|
||||
function drawStockAndFoundation(cards) {
|
||||
const stockCards = cards.filter(card => card.stack === 'stock');
|
||||
const foundationCards = cards.filter(card => card.stack.startsWith('foundation'));
|
||||
|
||||
// 스톡 더미 그리기 (오른쪽 아래)
|
||||
const stockX = canvas.width - cardWidth * 1.5;
|
||||
const stockY = canvas.height - cardHeight * 1.5;
|
||||
|
||||
// 카드가 10장 이상 있을 때만 스톡 더미를 표시
|
||||
if (stockCards.length > 0) {
|
||||
ctx.drawImage(cardBackImage, stockX, stockY, cardWidth, cardHeight);
|
||||
}
|
||||
|
||||
// 파운데이션 영역 그리기 (왼쪽 아래)
|
||||
const foundationY = canvas.height - cardHeight * 1.5;
|
||||
const foundationStacks = foundationCards.reduce((acc, card) => {
|
||||
const stackId = card.stack;
|
||||
if (!acc[stackId]) acc[stackId] = [];
|
||||
acc[stackId].push(card);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
Object.keys(foundationStacks).forEach((stackId, index) => {
|
||||
const stackCards = foundationStacks[stackId];
|
||||
const foundationX = cardWidth * 0.5 + index * (cardWidth + 10);
|
||||
|
||||
if (stackCards.length > 0) {
|
||||
// 완성된 스택의 맨 위 카드만 그리기
|
||||
const topCard = stackCards[stackCards.length - 1];
|
||||
drawSingleCard(topCard, foundationX, foundationY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 추가: 단일 카드를 그리는 헬퍼 함수
|
||||
*/
|
||||
function drawSingleCard(card, x, y) {
|
||||
if (card.isFaceUp) {
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(x, y, cardWidth, cardHeight);
|
||||
ctx.strokeStyle = '#333333';
|
||||
ctx.strokeRect(x, y, cardWidth, cardHeight);
|
||||
|
||||
const isRed = (card.suit === 'heart' || card.suit === 'diamond');
|
||||
ctx.fillStyle = isRed ? '#ff0000' : '#000000';
|
||||
ctx.font = `${cardWidth * 0.25}px Arial`;
|
||||
ctx.fillText(getRankText(card.rank), x + cardWidth * 0.1, y + cardHeight * 0.25);
|
||||
ctx.fillText(getSuitSymbol(card.suit), x + cardWidth * 0.1, y + cardHeight * 0.5);
|
||||
} else {
|
||||
ctx.drawImage(cardBackImage, x, y, cardWidth, cardHeight);
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#008000';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (currentGame && assetsLoaded) {
|
||||
// 테이블로 스택을 그립니다.
|
||||
drawTableauCards(currentGame.cards);
|
||||
|
||||
// 추가: 스톡과 파운데이션을 그립니다.
|
||||
drawStockAndFoundation(currentGame.cards);
|
||||
|
||||
if (animatedCard) drawAnimatedCard();
|
||||
if (shakeOffset !== 0) drawShakingCard();
|
||||
if (dimOpacity > 0) drawDimOverlay();
|
||||
}
|
||||
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블로 스택의 카드들을 그리는 로직 (기존 drawCards 함수명 변경)
|
||||
*/
|
||||
function drawTableauCards(cards) {
|
||||
const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2;
|
||||
const startY = cardHeight * 0.5;
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const stackCards = cards.filter(card => card.stack === `tableau-${i + 1}`);
|
||||
stackCards.forEach((card, index) => {
|
||||
if (isDragging && draggedCards.some(dc => dc.suit === card.suit && dc.rank === card.rank && dc.stack === card.stack)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x = startX + i * (cardWidth + cardWidth * 0.5);
|
||||
const y = startY + index * cardOverlapY;
|
||||
|
||||
drawSingleCard(card, x, y);
|
||||
});
|
||||
}
|
||||
|
||||
if (isDragging && draggedCards.length > 0) {
|
||||
drawDraggedCards(draggedCards);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 드래그 중인 카드 묶음을 그리는 함수
|
||||
*/
|
||||
function drawDraggedCards(cards) {
|
||||
cards.forEach((card, index) => {
|
||||
const x = cards[0].x;
|
||||
const y = cards[0].y + index * cardOverlapY;
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(x, y, cardWidth, cardHeight);
|
||||
ctx.strokeStyle = '#333333';
|
||||
ctx.strokeRect(x, y, cardWidth, cardHeight);
|
||||
|
||||
const isRed = (card.suit === 'heart' || card.suit === 'diamond');
|
||||
ctx.fillStyle = isRed ? '#ff0000' : '#000000';
|
||||
ctx.font = `${cardWidth * 0.25}px Arial`;
|
||||
ctx.fillText(getRankText(card.rank), x + cardWidth * 0.1, y + cardHeight * 0.25);
|
||||
ctx.fillText(getSuitSymbol(card.suit), x + cardWidth * 0.1, y + cardHeight * 0.5);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 이동 애니메이션 중인 카드를 그리는 로직
|
||||
*/
|
||||
function drawAnimatedCard() {
|
||||
if (!animatedCard) return;
|
||||
|
||||
const startX = animatedCard.startX + (animatedCard.endX - animatedCard.startX) * animationProgress;
|
||||
const startY = animatedCard.startY + (animatedCard.endY - animatedCard.startY) * animationProgress;
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(startX, startY, cardWidth, cardHeight);
|
||||
ctx.strokeStyle = '#333333';
|
||||
ctx.strokeRect(startX, startY, cardWidth, cardHeight);
|
||||
|
||||
const isRed = (animatedCard.card.suit === 'heart' || animatedCard.card.suit === 'diamond');
|
||||
ctx.fillStyle = isRed ? '#ff0000' : '#000000';
|
||||
ctx.font = `${cardWidth * 0.25}px Arial`;
|
||||
ctx.fillText(getRankText(animatedCard.card.rank), startX + cardWidth * 0.1, startY + cardHeight * 0.25);
|
||||
ctx.fillText(getSuitSymbol(animatedCard.card.suit), startX + cardWidth * 0.1, startY + cardHeight * 0.5);
|
||||
|
||||
animationProgress += 0.05;
|
||||
if (animationProgress >= 1) {
|
||||
isAnimating = false;
|
||||
animatedCard = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 무효화 시 흔들리는 카드를 그리는 로직
|
||||
*/
|
||||
function drawShakingCard() {
|
||||
if (!shakenCard) return;
|
||||
|
||||
const x = shakenCard.originalX + shakeOffset;
|
||||
const y = shakenCard.originalY;
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(x, y, cardWidth, cardHeight);
|
||||
ctx.strokeStyle = '#333333';
|
||||
ctx.strokeRect(x, y, cardWidth, cardHeight);
|
||||
|
||||
const isRed = (shakenCard.card.suit === 'heart' || shakenCard.card.suit === 'diamond');
|
||||
ctx.fillStyle = isRed ? '#ff0000' : '#000000';
|
||||
ctx.font = `${cardWidth * 0.25}px Arial`;
|
||||
ctx.fillText(getRankText(shakenCard.card.rank), x + cardWidth * 0.1, y + cardHeight * 0.25);
|
||||
ctx.fillText(getSuitSymbol(shakenCard.card.suit), x + cardWidth * 0.1, y + cardHeight * 0.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 붉은색 딤 오버레이를 그리는 로직
|
||||
*/
|
||||
function drawDimOverlay() {
|
||||
ctx.fillStyle = `rgba(255, 0, 0, ${dimOpacity})`;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
// === 3. 이벤트 리스너 연결 ===
|
||||
canvas.addEventListener('mousedown', handlePointerDown);
|
||||
canvas.addEventListener('mousemove', handlePointerMove);
|
||||
canvas.addEventListener('mouseup', handlePointerUp);
|
||||
canvas.addEventListener('touchstart', handlePointerDown);
|
||||
canvas.addEventListener('touchmove', handlePointerMove);
|
||||
canvas.addEventListener('touchend', handlePointerUp);
|
||||
canvas.addEventListener('dblclick', handleDoubleClick);
|
||||
|
||||
function getCanvasCoordinates(event) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const clientX = event.touches ? event.touches[0].clientX : event.clientX;
|
||||
const clientY = event.touches ? event.touches[0].clientY : event.clientY;
|
||||
return {
|
||||
x: clientX - rect.left,
|
||||
y: clientY - rect.top
|
||||
};
|
||||
}
|
||||
|
||||
function findCardAt(x, y, cards) {
|
||||
const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2;
|
||||
const startY = cardHeight * 0.5;
|
||||
|
||||
// 원본 배열의 참조를 유지하면서 복사본을 만들어 정렬
|
||||
const sortedCards = cards.slice().sort((a, b) => {
|
||||
const stackA = parseInt(a.stack.split('-')[1]) || 0;
|
||||
const stackB = parseInt(b.stack.split('-')[1]) || 0;
|
||||
if (stackA !== stackB) return stackB - stackA;
|
||||
const indexA = cards.filter(c => c.stack === a.stack).indexOf(a);
|
||||
const indexB = cards.filter(c => c.stack === b.stack).indexOf(b);
|
||||
return indexB - indexA;
|
||||
});
|
||||
|
||||
for (const card of sortedCards) {
|
||||
if (!card.isFaceUp) continue;
|
||||
const stackIndex = (parseInt(card.stack.split('-')[1]) || 1) - 1;
|
||||
const cardIndexInStack = cards.filter(c => c.stack === card.stack).indexOf(card);
|
||||
const cardX = startX + stackIndex * (cardWidth + cardWidth * 0.5);
|
||||
const cardY = startY + cardIndexInStack * cardOverlapY;
|
||||
|
||||
// 드래그 가능한 카드 묶음의 첫 번째 카드만 선택 가능하도록 높이 범위를 수정
|
||||
const stackCards = cards.filter(c => c.stack === card.stack);
|
||||
const lastCardInStack = stackCards[stackCards.length - 1];
|
||||
const isFirstCardOfMovableStack = (card.suit === lastCardInStack.suit && card.rank === lastCardInStack.rank);
|
||||
const touchHeight = isFirstCardOfMovableStack ? cardHeight : cardOverlapY;
|
||||
|
||||
if (x >= cardX && x <= cardX + cardWidth && y >= cardY && y <= cardY + touchHeight) {
|
||||
return card;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findStackAt(x, y) {
|
||||
const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2;
|
||||
const startY = cardHeight * 0.5;
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const stackX = startX + i * (cardWidth + cardWidth * 0.5);
|
||||
const stackCards = currentGame.cards.filter(card => card.stack === `tableau-${i + 1}`);
|
||||
const lastCardIndex = stackCards.length > 0 ? stackCards.length - 1 : -1;
|
||||
const stackY = lastCardIndex >= 0 ? startY + lastCardIndex * cardOverlapY : startY;
|
||||
const stackHeight = lastCardIndex >= 0 ? cardHeight : cardHeight + (15 * cardOverlapY);
|
||||
|
||||
if (x >= stackX && x <= stackX + cardWidth && y >= stackY && y <= stackY + stackHeight) {
|
||||
return `tableau-${i + 1}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 수정: 클릭된 카드 아래의 묶음을 반환
|
||||
*/
|
||||
function getCardStackForMove(card) {
|
||||
const stack = currentGame.cards.filter(c => c.stack === card.stack);
|
||||
const startIndex = stack.findIndex(c => c.suit === card.suit && c.rank === card.rank);
|
||||
if (startIndex === -1) return null;
|
||||
|
||||
const movableStack = stack.slice(startIndex);
|
||||
for (let i = 0; i < movableStack.length - 1; i++) {
|
||||
if (movableStack[i].suit !== movableStack[i+1].suit || movableStack[i].rank !== movableStack[i+1].rank + 1) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return movableStack;
|
||||
}
|
||||
|
||||
function handlePointerDown(event) {
|
||||
if (isAnimating) return;
|
||||
const coords = getCanvasCoordinates(event);
|
||||
|
||||
// 🔴 추가: 스톡 더미 클릭 감지
|
||||
const stockX = canvas.width - cardWidth * 1.5;
|
||||
const stockY = canvas.height - cardHeight * 1.5;
|
||||
|
||||
if (coords.x >= stockX && coords.x <= stockX + cardWidth && coords.y >= stockY && coords.y <= stockY + cardHeight) {
|
||||
handleStockClick();
|
||||
return;
|
||||
}
|
||||
|
||||
const clickedCard = findCardAt(coords.x, coords.y, currentGame.cards);
|
||||
if (clickedCard) {
|
||||
const movableStack = getCardStackForMove(clickedCard);
|
||||
if (movableStack) {
|
||||
isDragging = true;
|
||||
draggedCards = movableStack;
|
||||
const cardPos = getCardPosition(clickedCard);
|
||||
dragOffsetX = coords.x - cardPos.x;
|
||||
dragOffsetY = coords.y - cardPos.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerMove(event) {
|
||||
if (!isDragging || draggedCards.length === 0) return;
|
||||
event.preventDefault();
|
||||
const coords = getCanvasCoordinates(event);
|
||||
draggedCards[0].x = coords.x - dragOffsetX;
|
||||
draggedCards[0].y = coords.y - dragOffsetY;
|
||||
draw();
|
||||
}
|
||||
|
||||
function handlePointerUp(event) {
|
||||
if (!isDragging || draggedCards.length === 0) return;
|
||||
isDragging = false;
|
||||
const coords = getCanvasCoordinates(event);
|
||||
const dropTargetStackId = findStackAt(coords.x, coords.y);
|
||||
|
||||
if (dropTargetStackId) {
|
||||
const isValid = isValidMove(draggedCards, dropTargetStackId);
|
||||
if (isValid) {
|
||||
moveCardLocally(draggedCards, dropTargetStackId);
|
||||
updateGameOnServer(currentGame);
|
||||
} else {
|
||||
draggedCards.forEach(card => {
|
||||
delete card.x;
|
||||
delete card.y;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
draggedCards.forEach(card => {
|
||||
delete card.x;
|
||||
delete card.y;
|
||||
});
|
||||
}
|
||||
draggedCards = [];
|
||||
draw();
|
||||
}
|
||||
|
||||
function handleStockClick() {
|
||||
if (isAnimating || isDragging) return;
|
||||
dealFromStock();
|
||||
}
|
||||
|
||||
// src/main/resources/static/js/spider.js
|
||||
function getBestMoveForStack(cardsToMove) {
|
||||
if (cardsToMove.length === 0) return null;
|
||||
const firstCardToMove = cardsToMove[0];
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const destStackId = `tableau-${i}`;
|
||||
if (destStackId === firstCardToMove.stack) continue;
|
||||
|
||||
const destStackCards = currentGame.cards.filter(c => c.stack === destStackId);
|
||||
|
||||
// 목적지 스택이 비어있는 경우
|
||||
if (destStackCards.length === 0) {
|
||||
// 킹(K)은 빈 공간으로 이동 가능하지만, 여기서는 모든 카드에 대해 허용
|
||||
return destStackId;
|
||||
}
|
||||
|
||||
const destTopCard = destStackCards[destStackCards.length - 1];
|
||||
|
||||
// 이동하려는 카드 묶음의 첫 번째 카드 랭크가 목적지 스택의 맨 위 카드 랭크보다 정확히 1 작으면 유효
|
||||
if (firstCardToMove.rank === destTopCard.rank - 1) {
|
||||
// 첫 번째 유효한 이동 장소를 반환
|
||||
return destStackId;
|
||||
}
|
||||
}
|
||||
return null; // 유효한 이동 장소를 찾지 못함
|
||||
}
|
||||
function handleDoubleClick(event) {
|
||||
// 오타 수정: Date.Now() -> Date.now()
|
||||
const now = Date.now();
|
||||
const timeSinceLastTap = now - lastTapTime;
|
||||
|
||||
if (timeSinceLastTap < 300 && timeSinceLastTap > 0) {
|
||||
const coords = getCanvasCoordinates(event);
|
||||
const clickedCard = findCardAt(coords.x, coords.y, currentGame.cards);
|
||||
|
||||
if (clickedCard) {
|
||||
// 클릭된 카드가 이동 가능한 묶음의 시작점인지 확인
|
||||
const movableStack = getCardStackForMove(clickedCard);
|
||||
if (movableStack) {
|
||||
// 이동 가능한 묶음에 대해 최적의 목적지 스택을 찾음
|
||||
const destinationStackId = getBestMoveForStack(movableStack);
|
||||
|
||||
if (destinationStackId) {
|
||||
animateCardMove(movableStack, destinationStackId);
|
||||
// TODO: 애니메이션 완료 후 moveCardLocally 호출
|
||||
} else {
|
||||
animateInvalidMove(clickedCard);
|
||||
}
|
||||
} else {
|
||||
animateInvalidMove(clickedCard);
|
||||
}
|
||||
}
|
||||
}
|
||||
lastTapTime = now;
|
||||
}
|
||||
|
||||
// === 4. 게임 로직 및 애니메이션 ===
|
||||
|
||||
function getBestMove(cardData) {
|
||||
let bestDestination = null;
|
||||
let bestScore = -1;
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const destStackId = `tableau-${i}`;
|
||||
if (destStackId === cardData.stack) continue;
|
||||
|
||||
const destStackCards = currentGame.cards.filter(c => c.stack === destStackId);
|
||||
const destTopCard = destStackCards.length > 0 ? destStackCards[destStackCards.length - 1] : null;
|
||||
|
||||
if (destTopCard) {
|
||||
const isRankConsecutive = (destTopCard.rank === cardData.rank + 1);
|
||||
if (isRankConsecutive) {
|
||||
const isSuitMatch = (destTopCard.suit === cardData.suit);
|
||||
if (isSuitMatch) {
|
||||
bestDestination = destStackId;
|
||||
bestScore = 2;
|
||||
}
|
||||
if (bestScore < 2) {
|
||||
bestDestination = destStackId;
|
||||
bestScore = 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (bestScore < 0) {
|
||||
bestDestination = destStackId;
|
||||
bestScore = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestDestination;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 수정: 카드 묶음을 이동시키는 로직 (완전한 구현)
|
||||
*/
|
||||
function moveCardLocally(cardsToMove, destinationStackId) {
|
||||
const oldStackId = cardsToMove[0].stack;
|
||||
|
||||
// 1. 드래그된 카드 묶음의 stack 속성을 업데이트
|
||||
cardsToMove.forEach(card => {
|
||||
card.stack = destinationStackId;
|
||||
delete card.x; // 드래그 시 추가된 임시 속성 제거
|
||||
delete card.y;
|
||||
});
|
||||
|
||||
// 2. 기존 게임 상태에서 드래그된 카드를 제거 (참조를 유지하면서)
|
||||
currentGame.cards = currentGame.cards.filter(card => !cardsToMove.includes(card));
|
||||
|
||||
// 3. 업데이트된 카드 묶음을 다시 게임 상태에 추가
|
||||
currentGame.cards = currentGame.cards.concat(cardsToMove);
|
||||
|
||||
// 4. 이전 스택의 마지막 카드 뒤집기
|
||||
const oldStackCards = currentGame.cards.filter(c => c.stack === oldStackId);
|
||||
if (oldStackCards.length > 0) {
|
||||
const lastCardInOldStack = oldStackCards[oldStackCards.length - 1];
|
||||
lastCardInOldStack.isFaceUp = true;
|
||||
}
|
||||
|
||||
currentGame.moves++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 수정: 이동 가능 여부를 확인하는 함수
|
||||
*/
|
||||
function isValidMove(cardsToMove, destinationStackId) {
|
||||
if (cardsToMove.length === 0) return false;
|
||||
|
||||
const firstCardToMove = cardsToMove[0];
|
||||
const destStackCards = currentGame.cards.filter(c => c.stack === destinationStackId);
|
||||
|
||||
if (destStackCards.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const destTopCard = destStackCards[destStackCards.length - 1];
|
||||
if (firstCardToMove.rank === destTopCard.rank - 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function animateCardMove(cardsToMove, destinationStackId) {
|
||||
isAnimating = true;
|
||||
animationProgress = 0;
|
||||
|
||||
// 묶음의 각 카드에 대해 애니메이션 시작/끝 위치 계산
|
||||
const animatedCards = cardsToMove.map(card => {
|
||||
const startPos = getCardPosition(card);
|
||||
const endPos = getCardDestinationPosition(card, destinationStackId);
|
||||
return {
|
||||
card: card,
|
||||
startX: startPos.x,
|
||||
startY: startPos.y,
|
||||
endX: endPos.x,
|
||||
endY: endPos.y,
|
||||
destinationStack: destinationStackId
|
||||
};
|
||||
});
|
||||
|
||||
// 애니메이션 루프
|
||||
function updateAnimation() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
draw(); // 현재 게임 상태 그리기
|
||||
|
||||
animatedCards.forEach((animCard, index) => {
|
||||
const startX = animCard.startX + (animCard.endX - animCard.startX) * animationProgress;
|
||||
const startY = animCard.startY + (animCard.endY - animCard.startY) * animationProgress + index * cardOverlapY;
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(startX, startY, cardWidth, cardHeight);
|
||||
ctx.strokeStyle = '#333333';
|
||||
ctx.strokeRect(startX, startY, cardWidth, cardHeight);
|
||||
|
||||
const isRed = (animCard.card.suit === 'heart' || animCard.card.suit === 'diamond');
|
||||
ctx.fillStyle = isRed ? '#ff0000' : '#000000';
|
||||
ctx.font = `${cardWidth * 0.25}px Arial`;
|
||||
ctx.fillText(getRankText(animCard.card.rank), startX + cardWidth * 0.1, startY + cardHeight * 0.25);
|
||||
ctx.fillText(getSuitSymbol(animCard.card.suit), startX + cardWidth * 0.1, startY + cardHeight * 0.5);
|
||||
});
|
||||
|
||||
animationProgress += 0.05;
|
||||
if (animationProgress >= 1) {
|
||||
// 애니메이션 완료 후 실제 게임 로직 처리
|
||||
moveCardLocally(cardsToMove, destinationStackId);
|
||||
updateGameOnServer(currentGame);
|
||||
isAnimating = false;
|
||||
} else {
|
||||
requestAnimationFrame(updateAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
updateAnimation();
|
||||
}
|
||||
|
||||
function animateInvalidMove(card) {
|
||||
let shakeStartTime = Date.Now();
|
||||
const shakeDuration = 500;
|
||||
shakenCard = {
|
||||
card: card,
|
||||
originalX: getCardPosition(card).x,
|
||||
originalY: getCardPosition(card).y
|
||||
};
|
||||
|
||||
function updateShake() {
|
||||
const elapsedTime = Date.Now() - shakeStartTime;
|
||||
if (elapsedTime > shakeDuration) {
|
||||
shakeOffset = 0;
|
||||
dimOpacity = 0;
|
||||
shakenCard = null;
|
||||
return;
|
||||
}
|
||||
|
||||
shakeOffset = Math.sin(elapsedTime * 0.1) * 5;
|
||||
dimOpacity = 0.5 * (1 - elapsedTime / shakeDuration);
|
||||
requestAnimationFrame(updateShake);
|
||||
}
|
||||
updateShake();
|
||||
}
|
||||
|
||||
async function dealFromStock() {
|
||||
if (!currentGame || isAnimating) return;
|
||||
isAnimating = true;
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/deal`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ gameId: currentGame.id })
|
||||
});
|
||||
const newGame = await response.json();
|
||||
currentGame = newGame;
|
||||
draw();
|
||||
} catch (error) {
|
||||
console.error('카드 분배 실패:', error);
|
||||
} finally {
|
||||
isAnimating = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateGameOnServer(updatedGame) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/update`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updatedGame)
|
||||
});
|
||||
const newGame = await response.json();
|
||||
currentGame = newGame;
|
||||
draw();
|
||||
} catch (error) {
|
||||
console.error('게임 업데이트 실패:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// === 5. 헬퍼 함수 ===
|
||||
|
||||
function getCardPosition(card) {
|
||||
const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2;
|
||||
const startY = cardHeight * 0.5;
|
||||
const stackIndex = (parseInt(card.stack.split('-')[1]) || 1) - 1;
|
||||
const cardIndexInStack = currentGame.cards.filter(c => c.stack === card.stack).indexOf(card);
|
||||
const x = startX + stackIndex * (cardWidth + cardWidth * 0.5);
|
||||
const y = startY + cardIndexInStack * cardOverlapY;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function getCardDestinationPosition(card, destinationStackId) {
|
||||
const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2;
|
||||
const startY = cardHeight * 0.5;
|
||||
const destStackCards = currentGame.cards.filter(c => c.stack === destinationStackId);
|
||||
const stackIndex = (parseInt(destinationStackId.split('-')[1]) || 1) - 1;
|
||||
const x = startX + stackIndex * (cardWidth + cardWidth * 0.5);
|
||||
const y = startY + destStackCards.length * cardOverlapY;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function getRankText(rank) {
|
||||
if (rank === 1) return 'A';
|
||||
if (rank === 11) return 'J';
|
||||
if (rank === 12) return 'Q';
|
||||
if (rank === 13) return 'K';
|
||||
return String(rank);
|
||||
}
|
||||
|
||||
function getSuitSymbol(suit) {
|
||||
if (suit === 'spade') return '♠️';
|
||||
if (suit === 'heart') return '♥️';
|
||||
if (suit === 'club') return '♣️';
|
||||
if (suit === 'diamond') return '♦️';
|
||||
}
|
||||
|
||||
// UI 요소에 대한 참조를 추가합니다.
|
||||
const suitCountSelect = document.getElementById('suitCountSelect');
|
||||
const cardCountSelect = document.getElementById('cardCountSelect');
|
||||
const startButton = document.getElementById('startButton');
|
||||
|
||||
// 시작 버튼에 이벤트 리스너 추가
|
||||
startButton.addEventListener('click', startNewGame);
|
||||
|
||||
// 🔴 수정: 무늬 수에 따른 카드 분배 옵션 정의
|
||||
const cardDistributionOptions = {
|
||||
'1': [ // 1 무늬: 맞출 확률이 매우 높으므로 쉬운 난이도만 제공
|
||||
{ value: '3,3', text: '쉬움 (총 30장)' },
|
||||
{ value: '4,3', text: '보통 (총 34장)' },
|
||||
{ value: '5,4', text: '어려움 (총 44장)' }
|
||||
],
|
||||
'2': [ // 2 무늬: 난이도가 높아져 4단계까지 제공
|
||||
{ value: '3,3', text: '쉬움 (총 30장)' },
|
||||
{ value: '4,3', text: '보통 (총 34장)' },
|
||||
{ value: '5,4', text: '어려움 (총 44장)' },
|
||||
{ value: '5,5', text: '매우 어려움 (총 50장)' }
|
||||
],
|
||||
'4': [ // 4 무늬: 가장 어려우므로 쉬움 난이도는 제외하고 제공
|
||||
{ value: '4,3', text: '보통 (총 34장)' },
|
||||
{ value: '5,4', text: '어려움 (총 44장)' },
|
||||
{ value: '5,5', text: '매우 어려움 (총 50장)' },
|
||||
{ value: '6,6', text: '극악 (총 60장)' }
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* 무늬 수 선택에 따라 카드 수 옵션을 업데이트하는 함수
|
||||
*/
|
||||
function updateCardCountOptions() {
|
||||
const selectedSuits = suitCountSelect.value;
|
||||
const options = cardDistributionOptions[selectedSuits] || [];
|
||||
|
||||
// 기존 옵션 제거
|
||||
cardCountSelect.innerHTML = '';
|
||||
|
||||
// 새 옵션 추가
|
||||
options.forEach(option => {
|
||||
const newOption = document.createElement('option');
|
||||
newOption.value = option.value;
|
||||
newOption.textContent = option.text;
|
||||
cardCountSelect.appendChild(newOption);
|
||||
});
|
||||
}
|
||||
|
||||
// 무늬 수 선택 변경 시 옵션 업데이트
|
||||
suitCountSelect.addEventListener('change', updateCardCountOptions);
|
||||
|
||||
// 페이지 로드 시 초기 옵션 설정
|
||||
updateCardCountOptions();
|
||||
|
||||
// 시작 버튼에 이벤트 리스너 추가
|
||||
startButton.addEventListener('click', startNewGame);
|
||||
|
||||
async function startNewGame() {
|
||||
if (!assetsLoaded) return;
|
||||
const numSuits = suitCountSelect.value;
|
||||
const numCards = cardCountSelect.value; // "3,4" 또는 "4,5" 등 문자열로 가져옵니다.
|
||||
|
||||
try {
|
||||
// API 요청 URL에 두 가지 파라미터를 모두 포함시킵니다.
|
||||
const response = await fetch(`${API_BASE_URL}/new?numSuits=${numSuits}&numCards=${numCards}`);
|
||||
currentGame = await response.json();
|
||||
draw();
|
||||
} catch (error) {
|
||||
console.error('새 게임 시작 실패:', error);
|
||||
}
|
||||
}
|
||||
|
||||
startNewGame();
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
layout:decorate="~{layout/default_layout}"
|
||||
>
|
||||
<th:block layout:fragment="head" id="head">
|
||||
<script type="text/javascript" th:src="@{/js/spider.js}"></script>
|
||||
<!-- <link th:href="@{/css/puzzle.css}" rel="stylesheet" />-->
|
||||
<link th:href="@{/css/spider.css}" rel="stylesheet" />
|
||||
|
||||
<script th:inline="javascript">
|
||||
|
||||
</script>
|
||||
<script type="text/javascript" th:src="@{/js/user.js}"></script>
|
||||
</th:block >
|
||||
<th:block layout:fragment="content">
|
||||
<div class="game-controls">
|
||||
<label for="suitCountSelect">무늬 수:</label>
|
||||
<select id="suitCountSelect">
|
||||
<option value="1" selected>1개</option>
|
||||
<option value="2" >2개</option>
|
||||
<option value="4">4개</option>
|
||||
</select>
|
||||
|
||||
<label for="cardCountSelect">초기 카드 수:</label>
|
||||
<select id="cardCountSelect">
|
||||
<option value="3,2">적음 (숨겨진 카드 3/2장)</option>
|
||||
<option value="4,3" selected>보통 (숨겨진 카드 4/3장)</option>
|
||||
<option value="5,6">많음 (숨겨진 카드 5/6장)</option>
|
||||
</select>
|
||||
<button id="startButton">새 게임 시작</button>
|
||||
</div>
|
||||
<div id="game-container">
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
</div>
|
||||
|
||||
</th:block>
|
||||
</html>
|
||||
@@ -18,6 +18,7 @@
|
||||
<li id="menu_nonogram"><a href="puzzle/play">Nonogram</a></li>
|
||||
<li id="menu_2048"><a href="puzzle/2048">2048</a></li>
|
||||
<li id="menu_sudoku"><a href="puzzle/sudoku">sudoku</a></li>
|
||||
<li id="menu_spider"><a href="puzzle/spider">spider</a></li>
|
||||
<!-- <li id="menu_sec"><a href="left-sidebar">Left Sidebar</a></li>-->
|
||||
<!-- <li id="menu_thr"><a href="right-sidebar">Right Sidebar</a></li>-->
|
||||
<!-- <li id="menu_four"><a href="two-sidebar">Two Sidebar</a></li>-->
|
||||
@@ -34,7 +35,7 @@
|
||||
<li><a href="javascript:gotoWrite()">글쓰기</a></li>
|
||||
<li><a href="javascript:gotoModify()">수정하기</a></li>
|
||||
<li><a href="javascript:gotoPuzzleUpload()">네모로직 문제 생성</a></li>
|
||||
<li><a href="javascript:gotoPuzzleUpload()">스도쿠 문제 생성</a></li>
|
||||
<li><a href="javascript:gotoSudoKuGen()">스도쿠 문제 생성</a></li>
|
||||
</th:block>
|
||||
<li><a href="#">Phasellus magna</a></li>
|
||||
<!-- <li><a href="#">Magna phasellus</a></li>-->
|
||||
|
||||
Reference in New Issue
Block a user