diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/model/Spider.kt b/src/main/kotlin/kr/lunaticbum/back/lun/model/Spider.kt index 08c24a4..28f56b7 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/model/Spider.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/model/Spider.kt @@ -12,17 +12,18 @@ import kotlin.random.Random data class SpiderGame( @Id val id: String? = null, - val cards: List, + val tableau: List>, + val stock: List, + val foundation: List>, val moves: Int, val isCompleted: Boolean, val timestamp: Long = System.currentTimeMillis() ) data class SpiderCard( - val suit: String, // 스페이드, 하트, 클로버, 다이아 - val rank: Int, // 1(A), 2, ..., 13(K) - var isFaceUp: Boolean, // 앞면인지 뒷면인지 - var stack: String // 어느 더미에 속하는지 (ex: 'tableau-1', 'stock', 'foundation') + val suit: String, + val rank: Int, + var isFaceUp: Boolean, ) interface SpiderGameRepository : ReactiveMongoRepository { @@ -33,150 +34,100 @@ interface SpiderGameRepository : ReactiveMongoRepository { class SpiderService(private val spiderGameRepository: SpiderGameRepository, private val spiderRankRepository: SpiderRankRepository ) { - // 🔴 수정: 무늬 수(numSuits)와 카드 수 문자열(numCards)을 인자로 받습니다. - fun newGame(numSuits: Int, numCards: String): Mono { - // numCards 문자열 "4,5"를 List [4, 5]로 파싱합니다. + private fun createDeck(numSuits: Int): List { + val allSuits = listOf("spade", "heart", "club", "diamond") + val suits = allSuits.take(numSuits) + val deck = mutableListOf() + val setsPerSuit = 8 / numSuits + repeat(setsPerSuit) { + for (suit in suits) { + for (rank in 1..13) { + deck.add(SpiderCard(suit, rank, isFaceUp = false)) + } + } + } + return deck + } + + private fun dealCards(shuffledCards: List, numCards: String): Pair>, List> { val initialCards = numCards.split(",").map { it.trim().toInt() } val cardsPerStack = listOf( initialCards[0], initialCards[0], initialCards[0], initialCards[0], initialCards[1], initialCards[1], initialCards[1], initialCards[1], initialCards[1], initialCards[1] ) + val cards = shuffledCards.toMutableList() + val tableau = mutableListOf>() + + cardsPerStack.forEach { num -> + val stack = mutableListOf() + repeat(num) { + val card = cards.removeFirst() + card.isFaceUp = (it == num - 1) + stack.add(card) + } + tableau.add(stack) + } + return Pair(tableau, cards) + } + + fun newGame(numSuits: Int, numCards: String): Mono { val allCards = createDeck(numSuits) val shuffledCards = allCards.shuffled(Random) + val (tableau, stock) = dealCards(shuffledCards, numCards) + val initialGame = SpiderGame( id = null, - cards = dealCards(shuffledCards, cardsPerStack), + tableau = tableau, + stock = stock, + foundation = emptyList(), moves = 0, isCompleted = false ) return spiderGameRepository.save(initialGame) } - // 🔴 수정: 무늬 수를 인자로 받는 createDeck 함수 - private fun createDeck(numSuits: Int): List { - val allSuits = listOf("spade", "heart", "club", "diamond") - val suits = allSuits.take(numSuits) - - val deck = mutableListOf() - val setsPerSuit = 8 / numSuits // 각 무늬별로 생성해야 할 세트 수 계산 - - repeat(setsPerSuit) { // 각 무늬별로 필요한 세트 수만큼 반복 - for (suit in suits) { - for (rank in 1..13) { - deck.add(SpiderCard(suit, rank, isFaceUp = false, stack = "stock")) - } - } - } - return deck + fun getGame(id: String): Mono { + return spiderGameRepository.findById(id) } - // 🔴 수정: 스택별 카드 수를 인자로 받는 dealCards 함수 - private fun dealCards(shuffledCards: List, cardsPerStack: List): List { - val cards = shuffledCards.toMutableList() - val tableauCards = mutableListOf() - - cardsPerStack.forEachIndexed { i, numCards -> - repeat(numCards) { - val card = cards.removeFirst() - card.stack = "tableau-${i + 1}" - card.isFaceUp = (it == numCards - 1) - tableauCards.add(card) - } - } - - cards.forEach { it.stack = "stock" } - - return tableauCards + cards - } - - private fun createDeck(): List { - val suits = listOf("spade", "heart", "club", "diamond") - val deck = mutableListOf() - repeat(2) { - for (suit in suits) { - for (rank in 1..13) { - deck.add(SpiderCard(suit, rank, isFaceUp = false, stack = "stock")) - } - } - } - return deck - } - - private fun dealCards(shuffledCards: List): List { - val cards = shuffledCards.toMutableList() - val tableauCards = mutableListOf() - - for (i in 0 until 10) { - val numCards = if (i < 4) 5 else 4 - repeat(numCards) { - val card = cards.removeFirst() - card.stack = "tableau-${i + 1}" - card.isFaceUp = (it == numCards - 1) - tableauCards.add(card) - } - } - - cards.forEach { it.stack = "stock" } - - return tableauCards + cards + fun updateGame(game: SpiderGame): Mono { + return spiderGameRepository.save(game) } fun dealCardsFromStock(gameId: String): Mono { return spiderGameRepository.findById(gameId) .flatMap { game -> - val stockCards = game.cards.filter { it.stack == "stock" } + val stockCards = game.stock.toMutableList() if (stockCards.size >= 10) { - // A map to hold the updated cards - val updatedCards = game.cards.toMutableList() - - // Take 10 cards from the stock val cardsToDeal = stockCards.take(10) + val updatedTableau = game.tableau.toMutableList() + val remainingStock = stockCards.drop(10) - // Update their stack and face-up status - cardsToDeal.forEachIndexed { index, card -> - val targetStack = "tableau-${index + 1}" - val originalCard = updatedCards.find { it == card } - if (originalCard != null) { - originalCard.stack = targetStack - originalCard.isFaceUp = true - } + updatedTableau.forEachIndexed { index, stack -> + val cardToDeal = cardsToDeal[index] + cardToDeal.isFaceUp = true + (stack as MutableList).add(cardToDeal) } - // Update the game and save it - val updatedGame = game.copy(cards = updatedCards, moves = game.moves + 1) + val updatedGame = game.copy( + tableau = updatedTableau, + stock = remainingStock, + moves = game.moves + 1 + ) spiderGameRepository.save(updatedGame) } else { - // No more cards to deal Mono.error(IllegalArgumentException("No more cards in stock.")) } } } - fun updateGame(game: SpiderGame): Mono { - // ... 카드 이동, 묶음 제거 등 실제 게임 로직 구현 ... - return spiderGameRepository.save(game) - } - - fun getGame(id: String): Mono { - return spiderGameRepository.findById(id) - } - // 게임 완료 후 랭킹 등록 + // 🔴 추가: 랭킹 등록 함수 fun registerRank(rank: SpiderRank): Mono { - // 게임의 유효성 검증 로직 (선택적): - // 해당 gameId가 실제로 존재하는지, 게임이 완료 상태인지 확인 가능 - return spiderGameRepository.findById(rank.gameId) - .flatMap { game -> - if (game.isCompleted) { - spiderRankRepository.save(rank) - } else { - Mono.error(IllegalArgumentException("게임이 완료되지 않았습니다.")) - } - } - .switchIfEmpty(Mono.error(IllegalArgumentException("존재하지 않는 게임 ID입니다."))) + return spiderRankRepository.save(rank) } - // 게임 ID별 랭킹 조회 + // 🔴 추가: 랭킹 조회 함수 fun getRanksByGameId(gameId: String): Flux { return spiderRankRepository.findByGameIdOrderByMovesAscCompletionTimeAsc(gameId) } @@ -186,17 +137,13 @@ class SpiderService(private val spiderGameRepository: SpiderGameRepository, data class SpiderRank( @Id val id: String? = null, - val gameId: String, // 어떤 게임의 기록인지 - val playerName: String, // 플레이어 이름 - val moves: Int, // 이동 횟수 (낮을수록 좋은 기록) - val completionTime: Long, // 완료 시간 (낮을수록 좋은 기록) + val gameId: String, + val playerName: String, + val moves: Int, + val completionTime: Long, val timestamp: Long = System.currentTimeMillis() ) interface SpiderRankRepository : ReactiveMongoRepository { - // gameId를 기준으로 랭킹을 가져오는 메서드 - // moves와 completionTime이 낮을수록 높은 순위이므로, 두 필드로 정렬하여 반환합니다. fun findByGameIdOrderByMovesAscCompletionTimeAsc(gameId: String): Flux -} - - +} \ No newline at end of file diff --git a/src/main/resources/static/js/spider.js b/src/main/resources/static/js/spider.js index 8489d19..df1ef63 100644 --- a/src/main/resources/static/js/spider.js +++ b/src/main/resources/static/js/spider.js @@ -15,7 +15,7 @@ document.addEventListener('DOMContentLoaded', () => { let isAnimating = false; let isDragging = false; - let draggedCards = []; // 🔴 드래그 중인 카드 묶음 (배열) + let draggedCards = []; let dragOffsetX = 0; let dragOffsetY = 0; let lastTapTime = 0; @@ -26,16 +26,57 @@ document.addEventListener('DOMContentLoaded', () => { 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(); + updateCardCountOptions(); }; + const suitCountSelect = document.getElementById('suitCountSelect'); + const cardCountSelect = document.getElementById('cardCountSelect'); + const startButton = document.getElementById('startButton'); + + // 🔴 수정: 난이도별 카드 분배 옵션을 세분화 + 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); + }); + } + // Canvas 크기 조절 function resizeCanvas() { canvas.width = window.innerWidth * 0.9; @@ -48,76 +89,13 @@ document.addEventListener('DOMContentLoaded', () => { 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); + drawGame(currentGame); if (animatedCard) drawAnimatedCard(); if (shakeOffset !== 0) drawShakingCard(); @@ -128,33 +106,34 @@ document.addEventListener('DOMContentLoaded', () => { } /** - * 테이블로 스택의 카드들을 그리는 로직 (기존 drawCards 함수명 변경) + * 🔴 수정: 새로운 데이터 구조에 맞게 게임 전체를 그리는 함수 */ - function drawTableauCards(cards) { + function drawGame(game) { 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)) { + // 테이블로 스택 그리기 + game.tableau.forEach((stack, stackIndex) => { + stack.forEach((card, cardIndex) => { + // 드래그 중인 카드는 숨김 + if (isDragging && draggedCards.includes(card)) { return; } - - const x = startX + i * (cardWidth + cardWidth * 0.5); - const y = startY + index * cardOverlapY; - + const x = startX + stackIndex * (cardWidth + cardWidth * 0.5); + const y = startY + cardIndex * cardOverlapY; drawSingleCard(card, x, y); }); - } + }); + // 드래그 중인 카드 묶음 그리기 if (isDragging && draggedCards.length > 0) { drawDraggedCards(draggedCards); } + + // 스톡과 파운데이션 그리기 + drawStockAndFoundation(game.stock, game.foundation); } - - /** * 드래그 중인 카드 묶음을 그리는 함수 */ @@ -176,6 +155,28 @@ document.addEventListener('DOMContentLoaded', () => { }); } + /** + * 단일 카드를 그리는 헬퍼 함수 + */ + 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`; + + // 🔴 수정: 텍스트의 Y 좌표를 위로 올립니다. + ctx.fillText(getRankText(card.rank), x + cardWidth * 0.1, y + cardHeight * 0.15); + ctx.fillText(getSuitSymbol(card.suit), x + cardWidth * 0.1, y + cardHeight * 0.4); + } else { + ctx.drawImage(cardBackImage, x, y, cardWidth, cardHeight); + } + } + /** * 이동 애니메이션 중인 카드를 그리는 로직 */ @@ -232,6 +233,27 @@ document.addEventListener('DOMContentLoaded', () => { ctx.fillRect(0, 0, canvas.width, canvas.height); } + /** + * 🔴 추가: 스톡 더미와 파운데이션 영역을 그리는 로직 + */ + function drawStockAndFoundation(stock, foundation) { + const stockX = canvas.width - cardWidth * 1.5; + const stockY = canvas.height - cardHeight * 1.5; + + if (stock.length > 0) { + ctx.drawImage(cardBackImage, stockX, stockY, cardWidth, cardHeight); + } + + const foundationY = canvas.height - cardHeight * 1.5; + foundation.forEach((stack, index) => { + const foundationX = cardWidth * 0.5 + index * (cardWidth + 10); + if (stack.length > 0) { + const topCard = stack[stack.length - 1]; + drawSingleCard(topCard, foundationX, foundationY); + } + }); + } + // === 3. 이벤트 리스너 연결 === canvas.addEventListener('mousedown', handlePointerDown); canvas.addEventListener('mousemove', handlePointerMove); @@ -241,6 +263,9 @@ document.addEventListener('DOMContentLoaded', () => { canvas.addEventListener('touchend', handlePointerUp); canvas.addEventListener('dblclick', handleDoubleClick); + suitCountSelect.addEventListener('change', updateCardCountOptions); + startButton.addEventListener('click', startNewGame); + function getCanvasCoordinates(event) { const rect = canvas.getBoundingClientRect(); const clientX = event.touches ? event.touches[0].clientX : event.clientX; @@ -251,69 +276,43 @@ document.addEventListener('DOMContentLoaded', () => { }; } - function findCardAt(x, y, cards) { + /** + * 🔴 수정: 새로운 데이터 구조를 사용하도록 findCardAt 함수 변경 + */ + function findCardAt(x, y) { 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 (let stackIndex = 9; stackIndex >= 0; stackIndex--) { + const stackCards = currentGame.tableau[stackIndex]; + for (let cardIndex = stackCards.length - 1; cardIndex >= 0; cardIndex--) { + const card = stackCards[cardIndex]; + if (!card.isFaceUp) continue; - 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 cardX = startX + stackIndex * (cardWidth + cardWidth * 0.5); + const cardY = startY + cardIndex * cardOverlapY; + const touchHeight = (cardIndex === stackCards.length - 1) ? cardHeight : 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}`; + if (x >= cardX && x <= cardX + cardWidth && y >= cardY && y <= cardY + touchHeight) { + return { card, stackIndex, cardIndex }; + } } } 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; + function getCardStackForMove(cardData, stackIndex) { + const stack = currentGame.tableau[stackIndex]; + const startIndex = stack.findIndex(c => c.suit === cardData.suit && c.rank === cardData.rank); + if (startIndex === -1 || !cardData.isFaceUp) 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) { + // 🔴 수정: 랭크와 무늬를 모두 확인하여 유효한 묶음인지 체크 + if (movableStack[i].rank !== movableStack[i+1].rank + 1 || movableStack[i].suit !== movableStack[i+1].suit) { return null; } } @@ -321,8 +320,16 @@ document.addEventListener('DOMContentLoaded', () => { } function handlePointerDown(event) { - if (isAnimating) return; + // 🔴 로그 추가: 이 로그가 찍히는지 확인하세요. + console.log('--- handlePointerDown 실행 ---'); + + if (isAnimating) { + console.log('애니메이션 중이므로 드래그 시작 불가.'); + return; + } const coords = getCanvasCoordinates(event); + const clickedCardData = findCardAt(coords.x, coords.y); + // 🔴 추가: 스톡 더미 클릭 감지 const stockX = canvas.width - cardWidth * 1.5; @@ -333,13 +340,14 @@ document.addEventListener('DOMContentLoaded', () => { return; } - const clickedCard = findCardAt(coords.x, coords.y, currentGame.cards); - if (clickedCard) { - const movableStack = getCardStackForMove(clickedCard); + if (clickedCardData) { + const { card, stackIndex } = clickedCardData; + const movableStack = getCardStackForMove(card, stackIndex); if (movableStack) { isDragging = true; draggedCards = movableStack; - const cardPos = getCardPosition(clickedCard); + draggedCards.sourceStackIndex = stackIndex; // 드래그 시작 스택 인덱스 저장 + const cardPos = getCardPosition(card, stackIndex); dragOffsetX = coords.x - cardPos.x; dragOffsetY = coords.y - cardPos.y; } @@ -360,11 +368,27 @@ document.addEventListener('DOMContentLoaded', () => { isDragging = false; const coords = getCanvasCoordinates(event); const dropTargetStackId = findStackAt(coords.x, coords.y); + const sourceStackIndex = draggedCards.sourceStackIndex; + + // 🔴 로그 추가: 드래그가 끝났을 때의 상태를 확인 + console.log('--- 드래그 종료 ---'); + console.log('드래그된 카드 묶음:', draggedCards.map(c => `${getRankText(c.rank)} of ${c.suit}`)); + console.log('드롭 대상 스택 ID:', dropTargetStackId); + console.log('------------------'); if (dropTargetStackId) { - const isValid = isValidMove(draggedCards, dropTargetStackId); + const destinationStackIndex = (parseInt(dropTargetStackId.split('-')[1]) || 1) - 1; + const isValid = isValidMove(draggedCards, destinationStackIndex); + + // 🔴 로그 추가: isValidMove가 실행될 때의 정보를 확인 + console.log('isValidMove 체크 시작'); + console.log('대상 스택의 맨 위 카드:', currentGame.tableau[destinationStackIndex].length > 0 ? currentGame.tableau[destinationStackIndex][currentGame.tableau[destinationStackIndex].length - 1] : '없음'); + console.log('이동할 묶음의 첫 카드:', draggedCards[0]); + console.log('결과:', isValid); + console.log('------------------'); + if (isValid) { - moveCardLocally(draggedCards, dropTargetStackId); + moveCardLocally(draggedCards, sourceStackIndex, destinationStackIndex); updateGameOnServer(currentGame); } else { draggedCards.forEach(card => { @@ -383,140 +407,64 @@ document.addEventListener('DOMContentLoaded', () => { } function handleStockClick() { - if (isAnimating || isDragging) return; + if (!currentGame || isAnimating) 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; + const coords = getCanvasCoordinates(event); + const clickedCardData = findCardAt(coords.x, coords.y); - if (timeSinceLastTap < 300 && timeSinceLastTap > 0) { - const coords = getCanvasCoordinates(event); - const clickedCard = findCardAt(coords.x, coords.y, currentGame.cards); + if (clickedCardData) { + const { card, stackIndex } = clickedCardData; + const movableStack = getCardStackForMove(card, stackIndex); - if (clickedCard) { - // 클릭된 카드가 이동 가능한 묶음의 시작점인지 확인 - const movableStack = getCardStackForMove(clickedCard); - if (movableStack) { - // 이동 가능한 묶음에 대해 최적의 목적지 스택을 찾음 - const destinationStackId = getBestMoveForStack(movableStack); + if (movableStack) { + const destinationStackId = getBestMoveForStack(movableStack); - if (destinationStackId) { - animateCardMove(movableStack, destinationStackId); - // TODO: 애니메이션 완료 후 moveCardLocally 호출 - } else { - animateInvalidMove(clickedCard); - } + if (destinationStackId) { + const destinationStackIndex = (parseInt(destinationStackId.split('-')[1]) || 1) - 1; + moveCardLocally(movableStack, stackIndex, destinationStackIndex); + updateGameOnServer(currentGame); } else { - animateInvalidMove(clickedCard); + animateInvalidMove(card); } + } else { + animateInvalidMove(card); } } - lastTapTime = now; } // === 4. 게임 로직 및 애니메이션 === - function getBestMove(cardData) { - let bestDestination = null; - let bestScore = -1; + 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 === cardData.stack) continue; + for (let i = 0; i < 10; i++) { + const destStackId = `tableau-${i + 1}`; + const destStackCards = currentGame.tableau[i]; - 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; + if (destStackCards.length === 0) { + if (firstCardToMove.rank === 13) { + return destStackId; } } + + const destTopCard = destStackCards[destStackCards.length - 1]; + + if (firstCardToMove.rank === destTopCard.rank - 1) { + return destStackId; + } } - return bestDestination; + return null; } - /** - * 🔴 수정: 카드 묶음을 이동시키는 로직 (완전한 구현) - */ - 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) { + function isValidMove(cardsToMove, destinationStackIndex) { if (cardsToMove.length === 0) return false; const firstCardToMove = cardsToMove[0]; - const destStackCards = currentGame.cards.filter(c => c.stack === destinationStackId); + const destStackCards = currentGame.tableau[destinationStackIndex]; if (destStackCards.length === 0) { return true; @@ -529,70 +477,61 @@ document.addEventListener('DOMContentLoaded', () => { return false; } + /** + * 🔴 수정: moveCardLocally 함수를 새로운 데이터 구조에 맞게 변경 + */ + function moveCardLocally(cardsToMove, sourceStackIndex, destinationStackIndex) { + // 이동할 카드들을 소스 스택에서 제거 + const sourceStack = currentGame.tableau[sourceStackIndex]; + const newSourceStack = sourceStack.slice(0, sourceStack.length - cardsToMove.length); + + // 대상 스택에 카드들을 추가 + const destinationStack = currentGame.tableau[destinationStackIndex]; + const newDestinationStack = [...destinationStack, ...cardsToMove]; + + // 게임 상태 업데이트 + const newTableau = [...currentGame.tableau]; + newTableau[sourceStackIndex] = newSourceStack; + newTableau[destinationStackIndex] = newDestinationStack; + + // 이전 스택의 마지막 카드 뒤집기 + if (newSourceStack.length > 0) { + newSourceStack[newSourceStack.length - 1].isFaceUp = true; + } + + // 서버에 보낼 데이터 구조 업데이트 + currentGame.tableau = newTableau; + currentGame.moves++; + } + 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 - }; - }); + const startPos = getCardPosition(cardsToMove[0], draggedCards.sourceStackIndex); + const endPos = getCardDestinationPosition(cardsToMove, 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(); + animatedCard = { + cards: cardsToMove, + startX: startPos.x, + startY: startPos.y, + endX: endPos.x, + endY: endPos.y, + destinationStack: destinationStackId + }; } function animateInvalidMove(card) { - let shakeStartTime = Date.Now(); + let shakeStartTime = Date.now(); const shakeDuration = 500; shakenCard = { card: card, - originalX: getCardPosition(card).x, - originalY: getCardPosition(card).y + originalX: getCardPosition(card, findStackIndexForCard(card)).x, + originalY: getCardPosition(card, findStackIndexForCard(card)).y }; function updateShake() { - const elapsedTime = Date.Now() - shakeStartTime; + const elapsedTime = Date.now() - shakeStartTime; if (elapsedTime > shakeDuration) { shakeOffset = 0; dimOpacity = 0; @@ -643,22 +582,49 @@ document.addEventListener('DOMContentLoaded', () => { // === 5. 헬퍼 함수 === - function getCardPosition(card) { + function findStackAt(x, y) { 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); + + for (let i = 0; i < 10; i++) { + const stackX = startX + i * (cardWidth + cardWidth * 0.5); + const stackCards = currentGame.tableau[i]; + 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 findStackIndexForCard(card) { + for(let i = 0; i < currentGame.tableau.length; i++) { + if (currentGame.tableau[i].includes(card)) { + return i; + } + } + return -1; + } + + function getCardPosition(card, stackIndex) { + const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2; + const startY = cardHeight * 0.5; + const stackCards = currentGame.tableau[stackIndex]; + const cardIndexInStack = stackCards.findIndex(c => c.suit === card.suit && c.rank === card.rank); const x = startX + stackIndex * (cardWidth + cardWidth * 0.5); const y = startY + cardIndexInStack * cardOverlapY; return { x, y }; } - function getCardDestinationPosition(card, destinationStackId) { + function getCardDestinationPosition(cardsToMove, 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 destStackIndex = (parseInt(destinationStackId.split('-')[1]) || 1) - 1; + const destStackCards = currentGame.tableau[destStackIndex]; + const x = startX + destStackIndex * (cardWidth + cardWidth * 0.5); const y = startY + destStackCards.length * cardOverlapY; return { x, y }; } @@ -678,70 +644,11 @@ document.addEventListener('DOMContentLoaded', () => { 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" 등 문자열로 가져옵니다. - + const numCards = cardCountSelect.value; try { - // API 요청 URL에 두 가지 파라미터를 모두 포함시킵니다. const response = await fetch(`${API_BASE_URL}/new?numSuits=${numSuits}&numCards=${numCards}`); currentGame = await response.json(); draw(); @@ -750,5 +657,5 @@ document.addEventListener('DOMContentLoaded', () => { } } - startNewGame(); + updateCardCountOptions(); }); \ No newline at end of file