...
This commit is contained in:
parent
903292b246
commit
2987825cb2
@ -12,17 +12,18 @@ import kotlin.random.Random
|
|||||||
data class SpiderGame(
|
data class SpiderGame(
|
||||||
@Id
|
@Id
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
val cards: List<SpiderCard>,
|
val tableau: List<List<SpiderCard>>,
|
||||||
|
val stock: List<SpiderCard>,
|
||||||
|
val foundation: List<List<SpiderCard>>,
|
||||||
val moves: Int,
|
val moves: Int,
|
||||||
val isCompleted: Boolean,
|
val isCompleted: Boolean,
|
||||||
val timestamp: Long = System.currentTimeMillis()
|
val timestamp: Long = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SpiderCard(
|
data class SpiderCard(
|
||||||
val suit: String, // 스페이드, 하트, 클로버, 다이아
|
val suit: String,
|
||||||
val rank: Int, // 1(A), 2, ..., 13(K)
|
val rank: Int,
|
||||||
var isFaceUp: Boolean, // 앞면인지 뒷면인지
|
var isFaceUp: Boolean,
|
||||||
var stack: String // 어느 더미에 속하는지 (ex: 'tableau-1', 'stock', 'foundation')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
interface SpiderGameRepository : ReactiveMongoRepository<SpiderGame, String> {
|
interface SpiderGameRepository : ReactiveMongoRepository<SpiderGame, String> {
|
||||||
@ -33,150 +34,100 @@ interface SpiderGameRepository : ReactiveMongoRepository<SpiderGame, String> {
|
|||||||
class SpiderService(private val spiderGameRepository: SpiderGameRepository,
|
class SpiderService(private val spiderGameRepository: SpiderGameRepository,
|
||||||
private val spiderRankRepository: SpiderRankRepository ) {
|
private val spiderRankRepository: SpiderRankRepository ) {
|
||||||
|
|
||||||
// 🔴 수정: 무늬 수(numSuits)와 카드 수 문자열(numCards)을 인자로 받습니다.
|
private fun createDeck(numSuits: Int): List<SpiderCard> {
|
||||||
fun newGame(numSuits: Int, numCards: String): Mono<SpiderGame> {
|
val allSuits = listOf("spade", "heart", "club", "diamond")
|
||||||
// numCards 문자열 "4,5"를 List<Int> [4, 5]로 파싱합니다.
|
val suits = allSuits.take(numSuits)
|
||||||
|
val deck = mutableListOf<SpiderCard>()
|
||||||
|
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<SpiderCard>, numCards: String): Pair<List<List<SpiderCard>>, List<SpiderCard>> {
|
||||||
val initialCards = numCards.split(",").map { it.trim().toInt() }
|
val initialCards = numCards.split(",").map { it.trim().toInt() }
|
||||||
val cardsPerStack = listOf(
|
val cardsPerStack = listOf(
|
||||||
initialCards[0], initialCards[0], initialCards[0], initialCards[0],
|
initialCards[0], initialCards[0], initialCards[0], initialCards[0],
|
||||||
initialCards[1], initialCards[1], initialCards[1], initialCards[1], initialCards[1], initialCards[1]
|
initialCards[1], initialCards[1], initialCards[1], initialCards[1], initialCards[1], initialCards[1]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val cards = shuffledCards.toMutableList()
|
||||||
|
val tableau = mutableListOf<MutableList<SpiderCard>>()
|
||||||
|
|
||||||
|
cardsPerStack.forEach { num ->
|
||||||
|
val stack = mutableListOf<SpiderCard>()
|
||||||
|
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<SpiderGame> {
|
||||||
val allCards = createDeck(numSuits)
|
val allCards = createDeck(numSuits)
|
||||||
val shuffledCards = allCards.shuffled(Random)
|
val shuffledCards = allCards.shuffled(Random)
|
||||||
|
val (tableau, stock) = dealCards(shuffledCards, numCards)
|
||||||
|
|
||||||
val initialGame = SpiderGame(
|
val initialGame = SpiderGame(
|
||||||
id = null,
|
id = null,
|
||||||
cards = dealCards(shuffledCards, cardsPerStack),
|
tableau = tableau,
|
||||||
|
stock = stock,
|
||||||
|
foundation = emptyList(),
|
||||||
moves = 0,
|
moves = 0,
|
||||||
isCompleted = false
|
isCompleted = false
|
||||||
)
|
)
|
||||||
return spiderGameRepository.save(initialGame)
|
return spiderGameRepository.save(initialGame)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔴 수정: 무늬 수를 인자로 받는 createDeck 함수
|
fun getGame(id: String): Mono<SpiderGame> {
|
||||||
private fun createDeck(numSuits: Int): List<SpiderCard> {
|
return spiderGameRepository.findById(id)
|
||||||
val allSuits = listOf("spade", "heart", "club", "diamond")
|
|
||||||
val suits = allSuits.take(numSuits)
|
|
||||||
|
|
||||||
val deck = mutableListOf<SpiderCard>()
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔴 수정: 스택별 카드 수를 인자로 받는 dealCards 함수
|
fun updateGame(game: SpiderGame): Mono<SpiderGame> {
|
||||||
private fun dealCards(shuffledCards: List<SpiderCard>, cardsPerStack: List<Int>): List<SpiderCard> {
|
return spiderGameRepository.save(game)
|
||||||
val cards = shuffledCards.toMutableList()
|
|
||||||
val tableauCards = mutableListOf<SpiderCard>()
|
|
||||||
|
|
||||||
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<SpiderCard> {
|
|
||||||
val suits = listOf("spade", "heart", "club", "diamond")
|
|
||||||
val deck = mutableListOf<SpiderCard>()
|
|
||||||
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<SpiderCard>): List<SpiderCard> {
|
|
||||||
val cards = shuffledCards.toMutableList()
|
|
||||||
val tableauCards = mutableListOf<SpiderCard>()
|
|
||||||
|
|
||||||
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 dealCardsFromStock(gameId: String): Mono<SpiderGame> {
|
fun dealCardsFromStock(gameId: String): Mono<SpiderGame> {
|
||||||
return spiderGameRepository.findById(gameId)
|
return spiderGameRepository.findById(gameId)
|
||||||
.flatMap { game ->
|
.flatMap { game ->
|
||||||
val stockCards = game.cards.filter { it.stack == "stock" }
|
val stockCards = game.stock.toMutableList()
|
||||||
if (stockCards.size >= 10) {
|
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 cardsToDeal = stockCards.take(10)
|
||||||
|
val updatedTableau = game.tableau.toMutableList()
|
||||||
|
val remainingStock = stockCards.drop(10)
|
||||||
|
|
||||||
// Update their stack and face-up status
|
updatedTableau.forEachIndexed { index, stack ->
|
||||||
cardsToDeal.forEachIndexed { index, card ->
|
val cardToDeal = cardsToDeal[index]
|
||||||
val targetStack = "tableau-${index + 1}"
|
cardToDeal.isFaceUp = true
|
||||||
val originalCard = updatedCards.find { it == card }
|
(stack as MutableList).add(cardToDeal)
|
||||||
if (originalCard != null) {
|
|
||||||
originalCard.stack = targetStack
|
|
||||||
originalCard.isFaceUp = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the game and save it
|
val updatedGame = game.copy(
|
||||||
val updatedGame = game.copy(cards = updatedCards, moves = game.moves + 1)
|
tableau = updatedTableau,
|
||||||
|
stock = remainingStock,
|
||||||
|
moves = game.moves + 1
|
||||||
|
)
|
||||||
spiderGameRepository.save(updatedGame)
|
spiderGameRepository.save(updatedGame)
|
||||||
} else {
|
} else {
|
||||||
// No more cards to deal
|
|
||||||
Mono.error(IllegalArgumentException("No more cards in stock."))
|
Mono.error(IllegalArgumentException("No more cards in stock."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateGame(game: SpiderGame): Mono<SpiderGame> {
|
// 🔴 추가: 랭킹 등록 함수
|
||||||
// ... 카드 이동, 묶음 제거 등 실제 게임 로직 구현 ...
|
|
||||||
return spiderGameRepository.save(game)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getGame(id: String): Mono<SpiderGame> {
|
|
||||||
return spiderGameRepository.findById(id)
|
|
||||||
}
|
|
||||||
// 게임 완료 후 랭킹 등록
|
|
||||||
fun registerRank(rank: SpiderRank): Mono<SpiderRank> {
|
fun registerRank(rank: SpiderRank): Mono<SpiderRank> {
|
||||||
// 게임의 유효성 검증 로직 (선택적):
|
return spiderRankRepository.save(rank)
|
||||||
// 해당 gameId가 실제로 존재하는지, 게임이 완료 상태인지 확인 가능
|
|
||||||
return spiderGameRepository.findById(rank.gameId)
|
|
||||||
.flatMap { game ->
|
|
||||||
if (game.isCompleted) {
|
|
||||||
spiderRankRepository.save(rank)
|
|
||||||
} else {
|
|
||||||
Mono.error(IllegalArgumentException("게임이 완료되지 않았습니다."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.switchIfEmpty(Mono.error(IllegalArgumentException("존재하지 않는 게임 ID입니다.")))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 게임 ID별 랭킹 조회
|
// 🔴 추가: 랭킹 조회 함수
|
||||||
fun getRanksByGameId(gameId: String): Flux<SpiderRank> {
|
fun getRanksByGameId(gameId: String): Flux<SpiderRank> {
|
||||||
return spiderRankRepository.findByGameIdOrderByMovesAscCompletionTimeAsc(gameId)
|
return spiderRankRepository.findByGameIdOrderByMovesAscCompletionTimeAsc(gameId)
|
||||||
}
|
}
|
||||||
@ -186,17 +137,13 @@ class SpiderService(private val spiderGameRepository: SpiderGameRepository,
|
|||||||
data class SpiderRank(
|
data class SpiderRank(
|
||||||
@Id
|
@Id
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
val gameId: String, // 어떤 게임의 기록인지
|
val gameId: String,
|
||||||
val playerName: String, // 플레이어 이름
|
val playerName: String,
|
||||||
val moves: Int, // 이동 횟수 (낮을수록 좋은 기록)
|
val moves: Int,
|
||||||
val completionTime: Long, // 완료 시간 (낮을수록 좋은 기록)
|
val completionTime: Long,
|
||||||
val timestamp: Long = System.currentTimeMillis()
|
val timestamp: Long = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
|
|
||||||
interface SpiderRankRepository : ReactiveMongoRepository<SpiderRank, String> {
|
interface SpiderRankRepository : ReactiveMongoRepository<SpiderRank, String> {
|
||||||
// gameId를 기준으로 랭킹을 가져오는 메서드
|
|
||||||
// moves와 completionTime이 낮을수록 높은 순위이므로, 두 필드로 정렬하여 반환합니다.
|
|
||||||
fun findByGameIdOrderByMovesAscCompletionTimeAsc(gameId: String): Flux<SpiderRank>
|
fun findByGameIdOrderByMovesAscCompletionTimeAsc(gameId: String): Flux<SpiderRank>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
let isAnimating = false;
|
let isAnimating = false;
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let draggedCards = []; // 🔴 드래그 중인 카드 묶음 (배열)
|
let draggedCards = [];
|
||||||
let dragOffsetX = 0;
|
let dragOffsetX = 0;
|
||||||
let dragOffsetY = 0;
|
let dragOffsetY = 0;
|
||||||
let lastTapTime = 0;
|
let lastTapTime = 0;
|
||||||
@ -26,16 +26,57 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let dimOpacity = 0;
|
let dimOpacity = 0;
|
||||||
let shakenCard = null;
|
let shakenCard = null;
|
||||||
|
|
||||||
// 카드 뒷면 이미지 로드
|
|
||||||
const cardBackImage = new Image();
|
const cardBackImage = new Image();
|
||||||
cardBackImage.src = '../assets/css/images/card-back.png';
|
cardBackImage.src = '../assets/css/images/card-back.png';
|
||||||
|
|
||||||
let assetsLoaded = false;
|
let assetsLoaded = false;
|
||||||
cardBackImage.onload = () => {
|
cardBackImage.onload = () => {
|
||||||
assetsLoaded = true;
|
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 크기 조절
|
// Canvas 크기 조절
|
||||||
function resizeCanvas() {
|
function resizeCanvas() {
|
||||||
canvas.width = window.innerWidth * 0.9;
|
canvas.width = window.innerWidth * 0.9;
|
||||||
@ -48,76 +89,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
|
|
||||||
// === 2. 렌더링 및 게임 루프 ===
|
// === 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() {
|
function draw() {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.fillStyle = '#008000';
|
ctx.fillStyle = '#008000';
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
if (currentGame && assetsLoaded) {
|
if (currentGame && assetsLoaded) {
|
||||||
// 테이블로 스택을 그립니다.
|
drawGame(currentGame);
|
||||||
drawTableauCards(currentGame.cards);
|
|
||||||
|
|
||||||
// 추가: 스톡과 파운데이션을 그립니다.
|
|
||||||
drawStockAndFoundation(currentGame.cards);
|
|
||||||
|
|
||||||
if (animatedCard) drawAnimatedCard();
|
if (animatedCard) drawAnimatedCard();
|
||||||
if (shakeOffset !== 0) drawShakingCard();
|
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 startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2;
|
||||||
const startY = cardHeight * 0.5;
|
const startY = cardHeight * 0.5;
|
||||||
|
|
||||||
for (let i = 0; i < 10; i++) {
|
// 테이블로 스택 그리기
|
||||||
const stackCards = cards.filter(card => card.stack === `tableau-${i + 1}`);
|
game.tableau.forEach((stack, stackIndex) => {
|
||||||
stackCards.forEach((card, index) => {
|
stack.forEach((card, cardIndex) => {
|
||||||
if (isDragging && draggedCards.some(dc => dc.suit === card.suit && dc.rank === card.rank && dc.stack === card.stack)) {
|
// 드래그 중인 카드는 숨김
|
||||||
|
if (isDragging && draggedCards.includes(card)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const x = startX + stackIndex * (cardWidth + cardWidth * 0.5);
|
||||||
const x = startX + i * (cardWidth + cardWidth * 0.5);
|
const y = startY + cardIndex * cardOverlapY;
|
||||||
const y = startY + index * cardOverlapY;
|
|
||||||
|
|
||||||
drawSingleCard(card, x, y);
|
drawSingleCard(card, x, y);
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
|
// 드래그 중인 카드 묶음 그리기
|
||||||
if (isDragging && draggedCards.length > 0) {
|
if (isDragging && draggedCards.length > 0) {
|
||||||
drawDraggedCards(draggedCards);
|
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);
|
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. 이벤트 리스너 연결 ===
|
// === 3. 이벤트 리스너 연결 ===
|
||||||
canvas.addEventListener('mousedown', handlePointerDown);
|
canvas.addEventListener('mousedown', handlePointerDown);
|
||||||
canvas.addEventListener('mousemove', handlePointerMove);
|
canvas.addEventListener('mousemove', handlePointerMove);
|
||||||
@ -241,6 +263,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
canvas.addEventListener('touchend', handlePointerUp);
|
canvas.addEventListener('touchend', handlePointerUp);
|
||||||
canvas.addEventListener('dblclick', handleDoubleClick);
|
canvas.addEventListener('dblclick', handleDoubleClick);
|
||||||
|
|
||||||
|
suitCountSelect.addEventListener('change', updateCardCountOptions);
|
||||||
|
startButton.addEventListener('click', startNewGame);
|
||||||
|
|
||||||
function getCanvasCoordinates(event) {
|
function getCanvasCoordinates(event) {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const clientX = event.touches ? event.touches[0].clientX : event.clientX;
|
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 startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2;
|
||||||
const startY = cardHeight * 0.5;
|
const startY = cardHeight * 0.5;
|
||||||
|
|
||||||
// 원본 배열의 참조를 유지하면서 복사본을 만들어 정렬
|
for (let stackIndex = 9; stackIndex >= 0; stackIndex--) {
|
||||||
const sortedCards = cards.slice().sort((a, b) => {
|
const stackCards = currentGame.tableau[stackIndex];
|
||||||
const stackA = parseInt(a.stack.split('-')[1]) || 0;
|
for (let cardIndex = stackCards.length - 1; cardIndex >= 0; cardIndex--) {
|
||||||
const stackB = parseInt(b.stack.split('-')[1]) || 0;
|
const card = stackCards[cardIndex];
|
||||||
if (stackA !== stackB) return stackB - stackA;
|
if (!card.isFaceUp) continue;
|
||||||
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) {
|
const cardX = startX + stackIndex * (cardWidth + cardWidth * 0.5);
|
||||||
if (!card.isFaceUp) continue;
|
const cardY = startY + cardIndex * cardOverlapY;
|
||||||
const stackIndex = (parseInt(card.stack.split('-')[1]) || 1) - 1;
|
const touchHeight = (cardIndex === stackCards.length - 1) ? cardHeight : cardOverlapY;
|
||||||
const cardIndexInStack = cards.filter(c => c.stack === card.stack).indexOf(card);
|
|
||||||
const cardX = startX + stackIndex * (cardWidth + cardWidth * 0.5);
|
|
||||||
const cardY = startY + cardIndexInStack * cardOverlapY;
|
|
||||||
|
|
||||||
// 드래그 가능한 카드 묶음의 첫 번째 카드만 선택 가능하도록 높이 범위를 수정
|
if (x >= cardX && x <= cardX + cardWidth && y >= cardY && y <= cardY + touchHeight) {
|
||||||
const stackCards = cards.filter(c => c.stack === card.stack);
|
return { card, stackIndex, cardIndex };
|
||||||
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔴 수정: 클릭된 카드 아래의 묶음을 반환
|
* 🔴 수정: 클릭된 카드 아래의 묶음을 반환 (새로운 데이터 구조용)
|
||||||
*/
|
*/
|
||||||
function getCardStackForMove(card) {
|
function getCardStackForMove(cardData, stackIndex) {
|
||||||
const stack = currentGame.cards.filter(c => c.stack === card.stack);
|
const stack = currentGame.tableau[stackIndex];
|
||||||
const startIndex = stack.findIndex(c => c.suit === card.suit && c.rank === card.rank);
|
const startIndex = stack.findIndex(c => c.suit === cardData.suit && c.rank === cardData.rank);
|
||||||
if (startIndex === -1) return null;
|
if (startIndex === -1 || !cardData.isFaceUp) return null;
|
||||||
|
|
||||||
const movableStack = stack.slice(startIndex);
|
const movableStack = stack.slice(startIndex);
|
||||||
for (let i = 0; i < movableStack.length - 1; i++) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -321,8 +320,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePointerDown(event) {
|
function handlePointerDown(event) {
|
||||||
if (isAnimating) return;
|
// 🔴 로그 추가: 이 로그가 찍히는지 확인하세요.
|
||||||
|
console.log('--- handlePointerDown 실행 ---');
|
||||||
|
|
||||||
|
if (isAnimating) {
|
||||||
|
console.log('애니메이션 중이므로 드래그 시작 불가.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const coords = getCanvasCoordinates(event);
|
const coords = getCanvasCoordinates(event);
|
||||||
|
const clickedCardData = findCardAt(coords.x, coords.y);
|
||||||
|
|
||||||
|
|
||||||
// 🔴 추가: 스톡 더미 클릭 감지
|
// 🔴 추가: 스톡 더미 클릭 감지
|
||||||
const stockX = canvas.width - cardWidth * 1.5;
|
const stockX = canvas.width - cardWidth * 1.5;
|
||||||
@ -333,13 +340,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clickedCard = findCardAt(coords.x, coords.y, currentGame.cards);
|
if (clickedCardData) {
|
||||||
if (clickedCard) {
|
const { card, stackIndex } = clickedCardData;
|
||||||
const movableStack = getCardStackForMove(clickedCard);
|
const movableStack = getCardStackForMove(card, stackIndex);
|
||||||
if (movableStack) {
|
if (movableStack) {
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
draggedCards = movableStack;
|
draggedCards = movableStack;
|
||||||
const cardPos = getCardPosition(clickedCard);
|
draggedCards.sourceStackIndex = stackIndex; // 드래그 시작 스택 인덱스 저장
|
||||||
|
const cardPos = getCardPosition(card, stackIndex);
|
||||||
dragOffsetX = coords.x - cardPos.x;
|
dragOffsetX = coords.x - cardPos.x;
|
||||||
dragOffsetY = coords.y - cardPos.y;
|
dragOffsetY = coords.y - cardPos.y;
|
||||||
}
|
}
|
||||||
@ -360,11 +368,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
isDragging = false;
|
isDragging = false;
|
||||||
const coords = getCanvasCoordinates(event);
|
const coords = getCanvasCoordinates(event);
|
||||||
const dropTargetStackId = findStackAt(coords.x, coords.y);
|
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) {
|
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) {
|
if (isValid) {
|
||||||
moveCardLocally(draggedCards, dropTargetStackId);
|
moveCardLocally(draggedCards, sourceStackIndex, destinationStackIndex);
|
||||||
updateGameOnServer(currentGame);
|
updateGameOnServer(currentGame);
|
||||||
} else {
|
} else {
|
||||||
draggedCards.forEach(card => {
|
draggedCards.forEach(card => {
|
||||||
@ -383,140 +407,64 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleStockClick() {
|
function handleStockClick() {
|
||||||
if (isAnimating || isDragging) return;
|
if (!currentGame || isAnimating) return;
|
||||||
dealFromStock();
|
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) {
|
function handleDoubleClick(event) {
|
||||||
// 오타 수정: Date.Now() -> Date.now()
|
const coords = getCanvasCoordinates(event);
|
||||||
const now = Date.now();
|
const clickedCardData = findCardAt(coords.x, coords.y);
|
||||||
const timeSinceLastTap = now - lastTapTime;
|
|
||||||
|
|
||||||
if (timeSinceLastTap < 300 && timeSinceLastTap > 0) {
|
if (clickedCardData) {
|
||||||
const coords = getCanvasCoordinates(event);
|
const { card, stackIndex } = clickedCardData;
|
||||||
const clickedCard = findCardAt(coords.x, coords.y, currentGame.cards);
|
const movableStack = getCardStackForMove(card, stackIndex);
|
||||||
|
|
||||||
if (clickedCard) {
|
if (movableStack) {
|
||||||
// 클릭된 카드가 이동 가능한 묶음의 시작점인지 확인
|
const destinationStackId = getBestMoveForStack(movableStack);
|
||||||
const movableStack = getCardStackForMove(clickedCard);
|
|
||||||
if (movableStack) {
|
|
||||||
// 이동 가능한 묶음에 대해 최적의 목적지 스택을 찾음
|
|
||||||
const destinationStackId = getBestMoveForStack(movableStack);
|
|
||||||
|
|
||||||
if (destinationStackId) {
|
if (destinationStackId) {
|
||||||
animateCardMove(movableStack, destinationStackId);
|
const destinationStackIndex = (parseInt(destinationStackId.split('-')[1]) || 1) - 1;
|
||||||
// TODO: 애니메이션 완료 후 moveCardLocally 호출
|
moveCardLocally(movableStack, stackIndex, destinationStackIndex);
|
||||||
} else {
|
updateGameOnServer(currentGame);
|
||||||
animateInvalidMove(clickedCard);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
animateInvalidMove(clickedCard);
|
animateInvalidMove(card);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
animateInvalidMove(card);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastTapTime = now;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 4. 게임 로직 및 애니메이션 ===
|
// === 4. 게임 로직 및 애니메이션 ===
|
||||||
|
|
||||||
function getBestMove(cardData) {
|
function getBestMoveForStack(cardsToMove) {
|
||||||
let bestDestination = null;
|
if (cardsToMove.length === 0) return null;
|
||||||
let bestScore = -1;
|
const firstCardToMove = cardsToMove[0];
|
||||||
|
|
||||||
for (let i = 1; i <= 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const destStackId = `tableau-${i}`;
|
const destStackId = `tableau-${i + 1}`;
|
||||||
if (destStackId === cardData.stack) continue;
|
const destStackCards = currentGame.tableau[i];
|
||||||
|
|
||||||
const destStackCards = currentGame.cards.filter(c => c.stack === destStackId);
|
if (destStackCards.length === 0) {
|
||||||
const destTopCard = destStackCards.length > 0 ? destStackCards[destStackCards.length - 1] : null;
|
if (firstCardToMove.rank === 13) {
|
||||||
|
return destStackId;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const destTopCard = destStackCards[destStackCards.length - 1];
|
||||||
|
|
||||||
|
if (firstCardToMove.rank === destTopCard.rank - 1) {
|
||||||
|
return destStackId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return bestDestination;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function isValidMove(cardsToMove, destinationStackIndex) {
|
||||||
* 🔴 수정: 카드 묶음을 이동시키는 로직 (완전한 구현)
|
|
||||||
*/
|
|
||||||
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;
|
if (cardsToMove.length === 0) return false;
|
||||||
|
|
||||||
const firstCardToMove = cardsToMove[0];
|
const firstCardToMove = cardsToMove[0];
|
||||||
const destStackCards = currentGame.cards.filter(c => c.stack === destinationStackId);
|
const destStackCards = currentGame.tableau[destinationStackIndex];
|
||||||
|
|
||||||
if (destStackCards.length === 0) {
|
if (destStackCards.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
@ -529,70 +477,61 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return false;
|
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) {
|
function animateCardMove(cardsToMove, destinationStackId) {
|
||||||
isAnimating = true;
|
isAnimating = true;
|
||||||
animationProgress = 0;
|
animationProgress = 0;
|
||||||
|
|
||||||
// 묶음의 각 카드에 대해 애니메이션 시작/끝 위치 계산
|
const startPos = getCardPosition(cardsToMove[0], draggedCards.sourceStackIndex);
|
||||||
const animatedCards = cardsToMove.map(card => {
|
const endPos = getCardDestinationPosition(cardsToMove, destinationStackId);
|
||||||
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
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 애니메이션 루프
|
animatedCard = {
|
||||||
function updateAnimation() {
|
cards: cardsToMove,
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
startX: startPos.x,
|
||||||
draw(); // 현재 게임 상태 그리기
|
startY: startPos.y,
|
||||||
|
endX: endPos.x,
|
||||||
animatedCards.forEach((animCard, index) => {
|
endY: endPos.y,
|
||||||
const startX = animCard.startX + (animCard.endX - animCard.startX) * animationProgress;
|
destinationStack: destinationStackId
|
||||||
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) {
|
function animateInvalidMove(card) {
|
||||||
let shakeStartTime = Date.Now();
|
let shakeStartTime = Date.now();
|
||||||
const shakeDuration = 500;
|
const shakeDuration = 500;
|
||||||
shakenCard = {
|
shakenCard = {
|
||||||
card: card,
|
card: card,
|
||||||
originalX: getCardPosition(card).x,
|
originalX: getCardPosition(card, findStackIndexForCard(card)).x,
|
||||||
originalY: getCardPosition(card).y
|
originalY: getCardPosition(card, findStackIndexForCard(card)).y
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateShake() {
|
function updateShake() {
|
||||||
const elapsedTime = Date.Now() - shakeStartTime;
|
const elapsedTime = Date.now() - shakeStartTime;
|
||||||
if (elapsedTime > shakeDuration) {
|
if (elapsedTime > shakeDuration) {
|
||||||
shakeOffset = 0;
|
shakeOffset = 0;
|
||||||
dimOpacity = 0;
|
dimOpacity = 0;
|
||||||
@ -643,22 +582,49 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// === 5. 헬퍼 함수 ===
|
// === 5. 헬퍼 함수 ===
|
||||||
|
|
||||||
function getCardPosition(card) {
|
function findStackAt(x, y) {
|
||||||
const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2;
|
const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2;
|
||||||
const startY = cardHeight * 0.5;
|
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 x = startX + stackIndex * (cardWidth + cardWidth * 0.5);
|
||||||
const y = startY + cardIndexInStack * cardOverlapY;
|
const y = startY + cardIndexInStack * cardOverlapY;
|
||||||
return { x, y };
|
return { x, y };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardDestinationPosition(card, destinationStackId) {
|
function getCardDestinationPosition(cardsToMove, destinationStackId) {
|
||||||
const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2;
|
const startX = (canvas.width - cardWidth * 10 - (cardWidth * 0.5 * 9)) / 2;
|
||||||
const startY = cardHeight * 0.5;
|
const startY = cardHeight * 0.5;
|
||||||
const destStackCards = currentGame.cards.filter(c => c.stack === destinationStackId);
|
const destStackIndex = (parseInt(destinationStackId.split('-')[1]) || 1) - 1;
|
||||||
const stackIndex = (parseInt(destinationStackId.split('-')[1]) || 1) - 1;
|
const destStackCards = currentGame.tableau[destStackIndex];
|
||||||
const x = startX + stackIndex * (cardWidth + cardWidth * 0.5);
|
const x = startX + destStackIndex * (cardWidth + cardWidth * 0.5);
|
||||||
const y = startY + destStackCards.length * cardOverlapY;
|
const y = startY + destStackCards.length * cardOverlapY;
|
||||||
return { x, y };
|
return { x, y };
|
||||||
}
|
}
|
||||||
@ -678,70 +644,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (suit === 'diamond') 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() {
|
async function startNewGame() {
|
||||||
if (!assetsLoaded) return;
|
if (!assetsLoaded) return;
|
||||||
const numSuits = suitCountSelect.value;
|
const numSuits = suitCountSelect.value;
|
||||||
const numCards = cardCountSelect.value; // "3,4" 또는 "4,5" 등 문자열로 가져옵니다.
|
const numCards = cardCountSelect.value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// API 요청 URL에 두 가지 파라미터를 모두 포함시킵니다.
|
|
||||||
const response = await fetch(`${API_BASE_URL}/new?numSuits=${numSuits}&numCards=${numCards}`);
|
const response = await fetch(`${API_BASE_URL}/new?numSuits=${numSuits}&numCards=${numCards}`);
|
||||||
currentGame = await response.json();
|
currentGame = await response.json();
|
||||||
draw();
|
draw();
|
||||||
@ -750,5 +657,5 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startNewGame();
|
updateCardCountOptions();
|
||||||
});
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user