/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Card class var Card = Container.expand(function () { var self = Container.call(this); // Card properties self.rank = 1; // 1=Ace, 13=King self.suit = 0; // 0=spades (for MVP, only one suit) self.faceUp = false; self.selected = false; // Card graphics // Add a drop shadow and rounded rectangle background for realism // Scale card assets to fit calculated CARD_WIDTH and CARD_HEIGHT var scaleX = typeof CARD_WIDTH !== "undefined" ? CARD_WIDTH / BASE_CARD_WIDTH : 1; var scaleY = typeof CARD_HEIGHT !== "undefined" ? CARD_HEIGHT / BASE_CARD_HEIGHT : 1; var cardShadow = LK.getAsset('cardFace', { anchorX: 0.5, anchorY: 0.5, tint: 0x222222, alpha: 0.18, scaleX: 1.04 * scaleX, scaleY: 1.04 * scaleY }); self.addChild(cardShadow); var cardFace = self.attachAsset('cardFace', { anchorX: 0.5, anchorY: 0.5, scaleX: scaleX, scaleY: scaleY }); var cardBack = self.attachAsset('cardBack', { anchorX: 0.5, anchorY: 0.5, scaleX: scaleX, scaleY: scaleY }); cardBack.visible = true; cardFace.visible = false; // Corner labels (rank + suit) for top-left and bottom-right var labelTL = new Text2('', { size: Math.floor(cardFace.height * 0.22), fill: 0xffffff, align: "left" }); labelTL.anchor.set(0, 0); labelTL.x = -cardFace.width / 2 + Math.floor(cardFace.width * 0.08); labelTL.y = -cardFace.height / 2 + Math.floor(cardFace.height * 0.08); self.addChild(labelTL); var labelBR = new Text2('', { size: Math.floor(cardFace.height * 0.22), fill: 0xffffff, align: "right" }); labelBR.anchor.set(1, 1); labelBR.x = cardFace.width / 2 - Math.floor(cardFace.width * 0.08); labelBR.y = cardFace.height / 2 - Math.floor(cardFace.height * 0.08); self.addChild(labelBR); // Set card data self.setCard = function (rank, suit, faceUp) { self.rank = rank; self.suit = suit; self.faceUp = faceUp; self.updateFace(); }; // Update card face/back and label self.updateFace = function () { cardFace.visible = self.faceUp; cardBack.visible = !self.faceUp; // Only spades for MVP var suitChar = '♠'; var rankStr = ''; if (self.rank === 1) rankStr = 'A';else if (self.rank === 11) rankStr = 'J';else if (self.rank === 12) rankStr = 'Q';else if (self.rank === 13) rankStr = 'K';else rankStr = '' + self.rank; // Show only in corners if (typeof labelTL !== "undefined" && typeof labelBR !== "undefined") { var labelText = rankStr + suitChar; labelTL.setText(labelText); labelBR.setText(labelText); labelTL.visible = true; labelBR.visible = true; if (self.faceUp) { labelTL.setStyle({ fill: 0xffffff, alpha: 1 }); labelBR.setStyle({ fill: 0xffffff, alpha: 1 }); } else { labelTL.setStyle({ fill: 0xcccccc, alpha: 0.45 }); labelBR.setStyle({ fill: 0xcccccc, alpha: 0.45 }); } } // Optionally, show a faint spider on the back for realism self.updateSelected(); }; // Visual feedback for selection self.updateSelected = function () { if (self.selected) { cardFace.tint = 0x99ccff; cardBack.tint = 0x99ccff; } else { cardFace.tint = 0xffffff; cardBack.tint = 0xffffff; } }; // Flip card self.flip = function (faceUp) { self.faceUp = faceUp; self.updateFace(); }; // Touch events self.down = function (x, y, obj) { if (self.faceUp) { // Let game handle selection if (game) game.onCardDown(self, x, y, obj); } }; return self; }); // Stock pile class (for dealing new rows) var StockPile = Container.expand(function () { var self = Container.call(this); self.cards = []; // Add card to stock self.addCard = function (card) { self.cards.push(card); self.addChild(card); card.x = 0; card.y = 0; }; // Deal one card per tableau pile self.dealToTableau = function (tableauPiles) { if (self.cards.length < tableauPiles.length) return false; for (var i = 0; i < tableauPiles.length; i++) { var card = self.cards.shift(); card.flip(true); tableauPiles[i].addCard(card); } return true; }; // Is empty? self.isEmpty = function () { return self.cards.length === 0; }; return self; }); // Tableau pile class var TableauPile = Container.expand(function () { var self = Container.call(this); self.cards = []; // Add card to pile self.addCard = function (card) { self.cards.push(card); self.addChild(card); self.layout(); }; // Remove card(s) from pile starting at index self.removeCardsFrom = function (idx) { var removed = []; while (self.cards.length > idx) { var c = self.cards.pop(); c.parent.removeChild(c); removed.unshift(c); } self.layout(); return removed; }; // Get top card self.topCard = function () { if (self.cards.length === 0) return null; return self.cards[self.cards.length - 1]; }; // Layout cards in pile self.layout = function () { // Increase vertical spacing between cards for more space in each column var CARD_VERTICAL_SPACING = 110; for (var i = 0; i < self.cards.length; i++) { var c = self.cards[i]; // Animate to new position using tween tween.stop(c, { x: true, y: true }); // Stop any previous tweens on x/y tween(c, { x: 0, y: i * CARD_VERTICAL_SPACING }, { duration: 400, easing: tween.cubicOut }); c.zIndex = i; } }; // Reveal top card if needed self.revealTop = function () { var top = self.topCard(); if (top && !top.faceUp) { top.flip(true); } }; // Get index of card in pile self.indexOf = function (card) { for (var i = 0; i < self.cards.length; i++) { if (self.cards[i] === card) return i; } return -1; }; // Can move sequence starting at idx? self.canMoveSequence = function (idx) { // All cards from idx to end must be face up and in descending order for (var i = idx; i < self.cards.length - 1; i++) { var c1 = self.cards[i]; var c2 = self.cards[i + 1]; if (!c1.faceUp || !c2.faceUp) return false; if (c1.rank !== c2.rank + 1) return false; } return self.cards[idx].faceUp; }; // Can accept a sequence of cards (cardsArr)? self.canAccept = function (cardsArr) { if (self.cards.length === 0) { // Only King can be placed on empty pile return cardsArr[0].rank === 13; } else { var top = self.topCard(); var moving = cardsArr[0]; return top.faceUp && top.rank === moving.rank + 1; } }; // Remove completed set (K-A) self.removeCompleteSet = function () { // Check if last 13 cards are a complete set if (self.cards.length < 13) return false; for (var i = 0; i < 13; i++) { var c = self.cards[self.cards.length - 1 - i]; if (!c.faceUp || c.rank !== 13 - i) return false; } // Remove them var removed = []; for (var i = 0; i < 13; i++) { var c = self.cards.pop(); if (c.parent) c.parent.removeChild(c); removed.unshift(c); } self.layout(); // Add to collected area (handled by animation in game logic) return removed; // Return the collected set for animation/cleanup }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xcccccc }); /**** * Game Code ****/ // (Removed stock label) // Show number of stacks left as card decks on stock pile var stockDecks = []; function updateStockDecks() { // Remove old for (var i = 0; i < stockDecks.length; i++) { if (stockDecks[i].parent) stockDecks[i].parent.removeChild(stockDecks[i]); } stockDecks = []; // Each "stack" is 10 cards (8 stacks of 10 = 80, but Spider uses 50 in stock: 5 stacks of 10) var stacksLeft = Math.floor(stockPile.cards.length / TABLEAU_COUNT); for (var i = 0; i < stacksLeft; i++) { var deckImg = LK.getAsset('cardBack', { anchorX: 0.5, anchorY: 0.5, scaleX: CARD_WIDTH / BASE_CARD_WIDTH * 0.9, scaleY: CARD_HEIGHT / BASE_CARD_HEIGHT * 0.9 }); // Center bottom: horizontally centered, stacked with slight offset var centerX = GAME_WIDTH / 2; var bottomY = GAME_HEIGHT - CARD_HEIGHT / 2 - 40; deckImg.x = centerX + (i - (stacksLeft - 1) / 2) * 24; deckImg.y = bottomY - i * 6; // Do not blur or fade the stack count deckImg.alpha = 1; game.addChild(deckImg); stockDecks.push(deckImg); } } // Add a large faded spider icon to the background, centered and scaled for portrait var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; // Spider background: scale to fit width, center var backgroundSpider = LK.getAsset('spider', { anchorX: 0.5, anchorY: 0.5, scaleX: 10, scaleY: 10 }); backgroundSpider.x = GAME_WIDTH / 2; backgroundSpider.y = GAME_HEIGHT / 2; backgroundSpider.alpha = 0.08; game.addChildAt(backgroundSpider, 0); // Ensure it's behind all other elements // Card and layout constants for portrait var TABLEAU_COUNT = 8; var GAME_MARGIN_X = 40; var GAME_MARGIN_Y = 120; // Get original card asset size var tempCard = LK.getAsset('cardFace', { anchorX: 0.5, anchorY: 0.5 }); var BASE_CARD_WIDTH = tempCard.width; var BASE_CARD_HEIGHT = tempCard.height; // Calculate max card width to fit 8 columns with margin var availableWidth = GAME_WIDTH - 2 * GAME_MARGIN_X; var CARD_WIDTH = Math.floor(availableWidth / TABLEAU_COUNT); var CARD_HEIGHT = Math.floor(BASE_CARD_HEIGHT * (CARD_WIDTH / BASE_CARD_WIDTH)); // No spacing between columns var TABLEAU_SPACING = 0; // Top offset for vertical layout (fit 2 rows of cards + margin) var TABLEAU_TOP = GAME_MARGIN_Y + CARD_HEIGHT + 40; // Center columns horizontally var totalTableauWidth = TABLEAU_COUNT * CARD_WIDTH + (TABLEAU_COUNT - 1) * TABLEAU_SPACING; var TABLEAU_LEFT = Math.round((GAME_WIDTH - totalTableauWidth) / 2); // Stock and completed set positions (fit to portrait) var STOCK_X = GAME_WIDTH - CARD_WIDTH - GAME_MARGIN_X; var STOCK_Y = GAME_MARGIN_Y; var COMPLETED_X = GAME_MARGIN_X + CARD_WIDTH / 2; var COMPLETED_Y = GAME_MARGIN_Y; // Clean up tempCard if (tempCard.parent) tempCard.parent.removeChild(tempCard); // Game state var tableauPiles = []; var stockPile = null; var completedSets = 0; var movesCount = 0; // Moves counter var draggingCards = null; var draggingFromPile = null; var dragOffsetX = 0; var dragOffsetY = 0; var dragStartX = 0; var dragStartY = 0; var dragValidTarget = null; var dragTargetPile = null; var canDeal = true; // Undo stack var undoStack = []; // Helper: auto-collect completed sets from all tableau piles function autoCollectSets() { var collected = false; for (var auto_i = 0; auto_i < tableauPiles.length; auto_i++) { var pile = tableauPiles[auto_i]; var removed; while (removed = pile.removeCompleteSet()) { LK.getSound('card_set').play(); completedSets += 1; updateScore(); updateCompletedSpiders(); var spider = LK.getAsset('spider', { anchorX: 0.5, anchorY: 0.5 }); spider.x = pile.x + CARD_WIDTH / 2; spider.y = pile.y + pile.cards.length * 40 + CARD_HEIGHT / 2; game.addChild(spider); (function (spider, setIndex, removedCards) { tween(spider, { y: COMPLETED_Y, x: COMPLETED_X + setIndex * (CARD_WIDTH * 0.7) }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { if (spider.parent) spider.parent.removeChild(spider); updateCompletedSpiders(); // Remove finished set cards from game for (var rc = 0; rc < removedCards.length; rc++) { if (removedCards[rc].parent) removedCards[rc].parent.removeChild(removedCards[rc]); } } }); })(spider, completedSets - 1, removed); collected = true; } } // Win condition: 8 sets if (completedSets >= 8) { LK.showYouWin(); return; } } // Helper: clone game state for undo function cloneGameState() { // Deep copy of tableau piles, stock, completedSets, movesCount var tableau = []; for (var i = 0; i < tableauPiles.length; i++) { var pile = tableauPiles[i]; var pileArr = []; for (var j = 0; j < pile.cards.length; j++) { var c = pile.cards[j]; pileArr.push({ rank: c.rank, suit: c.suit, faceUp: c.faceUp }); } tableau.push(pileArr); } var stock = []; for (var i = 0; i < stockPile.cards.length; i++) { var c = stockPile.cards[i]; stock.push({ rank: c.rank, suit: c.suit, faceUp: c.faceUp }); } return { tableau: tableau, stock: stock, completedSets: completedSets, movesCount: movesCount }; } // Helper: restore game state from undo function restoreGameState(state) { // Remove all cards for (var i = 0; i < tableauPiles.length; i++) { var pile = tableauPiles[i]; while (pile.cards.length > 0) { var c = pile.cards.pop(); if (c.parent) c.parent.removeChild(c); } } while (stockPile.cards.length > 0) { var c = stockPile.cards.pop(); if (c.parent) c.parent.removeChild(c); } // Restore tableau for (var i = 0; i < tableauPiles.length; i++) { var pileArr = state.tableau[i]; for (var j = 0; j < pileArr.length; j++) { var cdata = pileArr[j]; var card = new Card(); card.setCard(cdata.rank, cdata.suit, cdata.faceUp); tableauPiles[i].addCard(card); } } // Restore stock for (var i = 0; i < state.stock.length; i++) { var cdata = state.stock[i]; var card = new Card(); card.setCard(cdata.rank, cdata.suit, cdata.faceUp); stockPile.addCard(card); } completedSets = state.completedSets; movesCount = state.movesCount; updateScore(); updateCompletedSpiders(); canDeal = true; updateStockDecks(); draggingCards = null; draggingFromPile = null; dragValidTarget = null; dragTargetPile = null; } // Push undo state function pushUndoState() { // Limit undo stack to 50 if (undoStack.length > 50) undoStack.shift(); undoStack.push(cloneGameState()); } // GUI var scoreTxt = new Text2('0', { size: 90, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var movesTxt = new Text2('Moves: 0', { size: 70, fill: 0xffffff }); movesTxt.anchor.set(0.5, 0); // Place moves counter under the sets counter (scoreTxt) movesTxt.x = GAME_WIDTH / 2; movesTxt.y = scoreTxt.y + scoreTxt.height + 10; LK.gui.top.addChild(movesTxt); // Undo button (now shown as an arrow icon) var undoBtn = new Text2('↶', { size: 180, fill: 0xffffff }); undoBtn.anchor.set(1, 1); // Place at the bottom right corner of the screen (using LK.gui.bottomRight) undoBtn.x = -40; undoBtn.y = -40; LK.gui.bottomRight.addChild(undoBtn); // Moves counter next to undo button var movesTxtBR = new Text2('0', { size: 70, fill: 0xffffff }); movesTxtBR.anchor.set(1, 1); // Place to the left of the undo button, aligned at bottom right movesTxtBR.x = undoBtn.x - 120; movesTxtBR.y = undoBtn.y - 10; LK.gui.bottomRight.addChild(movesTxtBR); // Undo button handler (moved here after undoBtn is defined) undoBtn.down = function (x, y, obj) { if (undoStack.length === 0) return; var prev = undoStack.pop(); // When redoing, do not decrease movesCount; keep current movesCount var currentMoves = movesCount; restoreGameState(prev); // Restore everything except movesCount (keep current) movesCount = currentMoves; updateScore(); }; // Restart button (shown as a circular arrow) var restartBtn = new Text2('⟳', { size: 120, fill: 0xffffff }); restartBtn.anchor.set(0, 0); // Place at the top left corner of the screen (using LK.gui.topLeft) // Add extra space to the right of the pause button (assume pauseBtn is at x=40, width=80, so start at 40+80+24=144) restartBtn.x = 144; // Increased offset for more space between pause and restart restartBtn.y = 40; // Down from top edge, visually aligned LK.gui.topLeft.addChild(restartBtn); restartBtn.down = function (x, y, obj) { resetGame(); }; // Removed dealBtn from top right // Completed sets display var completedSpiders = []; // Initialize tableau piles for (var i = 0; i < TABLEAU_COUNT; i++) { var pile = new TableauPile(); // Align piles evenly across the screen, centered pile.x = TABLEAU_LEFT + i * (CARD_WIDTH + TABLEAU_SPACING) + CARD_WIDTH / 2; pile.y = TABLEAU_TOP; game.addChild(pile); tableauPiles.push(pile); } // Initialize stock pile stockPile = new StockPile(); stockPile.x = STOCK_X; stockPile.y = STOCK_Y; game.addChild(stockPile); // (Removed stock label) // Shuffle and deal cards function shuffleDeck() { // 8 decks of spades (104 cards) var deck = []; for (var d = 0; d < 8; d++) { for (var r = 1; r <= 13; r++) { var card = new Card(); card.setCard(r, 0, false); deck.push(card); } } // Fisher-Yates shuffle for (var i = deck.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var tmp = deck[i]; deck[i] = deck[j]; deck[j] = tmp; } return deck; } function dealInitial() { var deck = shuffleDeck(); // 52 cards to tableau (first 4 piles get 7, rest get 6) for (var i = 0; i < TABLEAU_COUNT; i++) { var count = i < 4 ? 7 : 6; for (var j = 0; j < count; j++) { var card = deck.shift(); tableauPiles[i].addCard(card); } } // Flip top card of each pile for (var i = 0; i < TABLEAU_COUNT; i++) { tableauPiles[i].topCard().flip(true); } // Remaining 50 cards to stock while (deck.length > 0) { var card = deck.shift(); stockPile.addCard(card); } completedSets = 0; updateScore(); updateCompletedSpiders(); updateStockDecks(); canDeal = true; } function updateScore() { scoreTxt.setText('Sets: ' + completedSets); if (typeof movesTxt !== "undefined") { movesTxt.setText('Moves: ' + movesCount); } if (typeof movesTxtBR !== "undefined") { movesTxtBR.setText(movesCount + ''); } } function updateCompletedSpiders() { // Remove old for (var i = 0; i < completedSpiders.length; i++) { if (completedSpiders[i].parent) completedSpiders[i].parent.removeChild(completedSpiders[i]); } completedSpiders = []; // Add new for (var i = 0; i < completedSets; i++) { var spider = LK.getAsset('spider', { anchorX: 0.5, anchorY: 0.5 }); spider.x = COMPLETED_X + i * (CARD_WIDTH * 0.7); spider.y = COMPLETED_Y; game.addChild(spider); completedSpiders.push(spider); } } // Handle card selection and drag game.onCardDown = function (card, x, y, obj) { // Find which pile this card is in var pile = null; var idx = -1; for (var i = 0; i < tableauPiles.length; i++) { var p = tableauPiles[i]; var j = p.indexOf(card); if (j !== -1) { pile = p; idx = j; break; } } if (!pile) return; // Can move sequence? if (!pile.canMoveSequence(idx)) return; // Prepare drag pushUndoState(); draggingCards = []; for (var k = idx; k < pile.cards.length; k++) { var c = pile.cards[k]; c.selected = true; c.updateSelected(); draggingCards.push(c); } draggingFromPile = pile; // Try to auto-move to a valid pile var autoMoved = false; for (var t = 0; t < tableauPiles.length; t++) { var targetPile = tableauPiles[t]; if (targetPile === pile) continue; if (targetPile.canAccept(draggingCards)) { // Remove from old pile pile.removeCardsFrom(idx); // Add to new pile for (var m = 0; m < draggingCards.length; m++) { targetPile.addCard(draggingCards[m]); } // Play card move sound LK.getSound('card_move').play(); // Reveal top card in old pile pile.revealTop(); // Check for completed set var completedAny = false; while (targetPile.removeCompleteSet()) { // Play card set sound LK.getSound('card_set').play(); completedSets += 1; updateScore(); updateCompletedSpiders(); // Animate spider var spider = LK.getAsset('spider', { anchorX: 0.5, anchorY: 0.5 }); spider.x = targetPile.x + CARD_WIDTH / 2; spider.y = targetPile.y + targetPile.cards.length * 40 + CARD_HEIGHT / 2; game.addChild(spider); (function (spider, setIndex) { tween(spider, { y: COMPLETED_Y, x: COMPLETED_X + setIndex * (CARD_WIDTH * 0.7) }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { if (spider.parent) spider.parent.removeChild(spider); updateCompletedSpiders(); } }); })(spider, completedSets - 1); completedAny = true; } autoCollectSets(); // Win condition: 8 sets if (completedSets >= 8) { LK.showYouWin(); return; } // Deselect for (var m = 0; m < draggingCards.length; m++) { draggingCards[m].selected = false; draggingCards[m].updateSelected(); } movesCount += 1; // Increment moves on auto-move updateScore(); draggingCards = null; draggingFromPile = null; dragValidTarget = null; dragTargetPile = null; autoMoved = true; break; } } if (autoMoved) return; // Bring to front for manual drag for (var k = 0; k < draggingCards.length; k++) { game.addChild(draggingCards[k]); } // Offset dragOffsetX = card.x; dragOffsetY = card.y; dragStartX = card.parent.x + card.x; dragStartY = card.parent.y + card.y; }; // Handle drag move game.move = function (x, y, obj) { if (!draggingCards) return; // Move cards for (var i = 0; i < draggingCards.length; i++) { draggingCards[i].x = x - dragOffsetX; draggingCards[i].y = y - dragOffsetY + i * 60; } // Check for valid drop target dragValidTarget = null; dragTargetPile = null; for (var i = 0; i < tableauPiles.length; i++) { var pile = tableauPiles[i]; // Don't allow drop on self if (pile === draggingFromPile) continue; // Get pile bounds var px = pile.x; var py = pile.y + pile.cards.length * 40; var pw = CARD_WIDTH; var ph = CARD_HEIGHT; // If pile is empty, allow drop anywhere in the pile area if (pile.cards.length === 0) { px = pile.x; py = pile.y; pw = CARD_WIDTH; ph = CARD_HEIGHT; if (x > px && x < px + pw && y > py && y < py + ph) { if (pile.canAccept(draggingCards)) { dragValidTarget = pile; dragTargetPile = pile; break; } } } else { if (x > px && x < px + pw && y > py - 40 && y < py + ph) { if (pile.canAccept(draggingCards)) { dragValidTarget = pile; dragTargetPile = pile; break; } } } } }; // Handle drag end game.up = function (x, y, obj) { if (!draggingCards) return; // Drop if (dragValidTarget && dragTargetPile) { pushUndoState(); // Remove from old pile var idx = draggingFromPile.indexOf(draggingCards[0]); draggingFromPile.removeCardsFrom(idx); // Add to new pile for (var i = 0; i < draggingCards.length; i++) { dragTargetPile.addCard(draggingCards[i]); } // Play card move sound LK.getSound('card_move').play(); movesCount += 1; // Increment moves on manual drag updateScore(); // Reveal top card in old pile draggingFromPile.revealTop(); // Check for completed set var completedAny = false; while (dragTargetPile.removeCompleteSet()) { // Play card set sound LK.getSound('card_set').play(); completedSets += 1; updateScore(); updateCompletedSpiders(); // Animate spider var spider = LK.getAsset('spider', { anchorX: 0.5, anchorY: 0.5 }); spider.x = dragTargetPile.x + CARD_WIDTH / 2; spider.y = dragTargetPile.y + dragTargetPile.cards.length * 40 + CARD_HEIGHT / 2; game.addChild(spider); (function (spider, setIndex) { tween(spider, { y: COMPLETED_Y, x: COMPLETED_X + setIndex * (CARD_WIDTH * 0.7) }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { if (spider.parent) spider.parent.removeChild(spider); updateCompletedSpiders(); } }); })(spider, completedSets - 1); completedAny = true; } autoCollectSets(); // Win condition: 8 sets if (completedSets >= 8) { LK.showYouWin(); return; } } else { // Return to original pile for (var i = 0; i < draggingCards.length; i++) { draggingCards[i].x = dragStartX - draggingFromPile.x; draggingCards[i].y = dragStartY - draggingFromPile.y + i * 60; draggingFromPile.addChild(draggingCards[i]); } draggingFromPile.layout(); } // Deselect for (var i = 0; i < draggingCards.length; i++) { draggingCards[i].selected = false; draggingCards[i].updateSelected(); } draggingCards = null; draggingFromPile = null; dragValidTarget = null; dragTargetPile = null; }; // Deal button (shown above the stock pile decks) var dealBtn = new Text2('Deal', { size: 90, fill: 0xffffff }); dealBtn.anchor.set(0.5, 1); // Position: above the stock pile decks, centered horizontally dealBtn.x = GAME_WIDTH / 2; dealBtn.y = GAME_HEIGHT - CARD_HEIGHT / 2 - 40 - 30; game.addChild(dealBtn); // Deal new row dealBtn.down = function (x, y, obj) { if (!canDeal) return; // Only allow if all tableau piles have at least one card for (var i = 0; i < tableauPiles.length; i++) { if (tableauPiles[i].cards.length === 0) return; } if (stockPile.isEmpty()) return; canDeal = false; pushUndoState(); // Animate deal var dealt = stockPile.dealToTableau(tableauPiles); if (dealt) { // Play shuffle sound when dealing from stock LK.getSound('shuffle').play(); // Flip top card of each pile for (var i = 0; i < tableauPiles.length; i++) { tableauPiles[i].topCard().flip(true); } updateStockDecks(); autoCollectSets(); } // Allow next deal after short delay LK.setTimeout(function () { canDeal = true; }, 400); }; // Touch on stock pile (deal) stockPile.down = function (x, y, obj) { dealBtn.down(x, y, obj); }; // Reset game function resetGame() { // Remove all cards for (var i = 0; i < tableauPiles.length; i++) { var pile = tableauPiles[i]; while (pile.cards.length > 0) { var c = pile.cards.pop(); if (c.parent) c.parent.removeChild(c); } } while (stockPile.cards.length > 0) { var c = stockPile.cards.pop(); if (c.parent) c.parent.removeChild(c); } completedSets = 0; movesCount = 0; // Reset moves counter updateScore(); updateCompletedSpiders(); updateStockDecks(); canDeal = true; draggingCards = null; draggingFromPile = null; dragValidTarget = null; dragTargetPile = null; dealInitial(); } // Game over: no more moves function checkGameOver() { // If no moves and stock is empty var canMove = false; for (var i = 0; i < tableauPiles.length; i++) { var pile = tableauPiles[i]; for (var j = 0; j < pile.cards.length; j++) { if (pile.canMoveSequence(j)) { // Try to move to another pile var seq = []; for (var k = j; k < pile.cards.length; k++) seq.push(pile.cards[k]); for (var t = 0; t < tableauPiles.length; t++) { if (t === i) continue; if (tableauPiles[t].canAccept(seq)) { canMove = true; break; } } } if (canMove) break; } if (canMove) break; } if (!canMove && stockPile.isEmpty()) { LK.showGameOver(); } } // Game update game.update = function () { // (auto-collect logic moved to after every move, deal, and flip) // Check for game over checkGameOver(); }; // Start game dealInitial(); LK.playMusic('relaxing_bg');
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Card class
var Card = Container.expand(function () {
var self = Container.call(this);
// Card properties
self.rank = 1; // 1=Ace, 13=King
self.suit = 0; // 0=spades (for MVP, only one suit)
self.faceUp = false;
self.selected = false;
// Card graphics
// Add a drop shadow and rounded rectangle background for realism
// Scale card assets to fit calculated CARD_WIDTH and CARD_HEIGHT
var scaleX = typeof CARD_WIDTH !== "undefined" ? CARD_WIDTH / BASE_CARD_WIDTH : 1;
var scaleY = typeof CARD_HEIGHT !== "undefined" ? CARD_HEIGHT / BASE_CARD_HEIGHT : 1;
var cardShadow = LK.getAsset('cardFace', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x222222,
alpha: 0.18,
scaleX: 1.04 * scaleX,
scaleY: 1.04 * scaleY
});
self.addChild(cardShadow);
var cardFace = self.attachAsset('cardFace', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: scaleX,
scaleY: scaleY
});
var cardBack = self.attachAsset('cardBack', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: scaleX,
scaleY: scaleY
});
cardBack.visible = true;
cardFace.visible = false;
// Corner labels (rank + suit) for top-left and bottom-right
var labelTL = new Text2('', {
size: Math.floor(cardFace.height * 0.22),
fill: 0xffffff,
align: "left"
});
labelTL.anchor.set(0, 0);
labelTL.x = -cardFace.width / 2 + Math.floor(cardFace.width * 0.08);
labelTL.y = -cardFace.height / 2 + Math.floor(cardFace.height * 0.08);
self.addChild(labelTL);
var labelBR = new Text2('', {
size: Math.floor(cardFace.height * 0.22),
fill: 0xffffff,
align: "right"
});
labelBR.anchor.set(1, 1);
labelBR.x = cardFace.width / 2 - Math.floor(cardFace.width * 0.08);
labelBR.y = cardFace.height / 2 - Math.floor(cardFace.height * 0.08);
self.addChild(labelBR);
// Set card data
self.setCard = function (rank, suit, faceUp) {
self.rank = rank;
self.suit = suit;
self.faceUp = faceUp;
self.updateFace();
};
// Update card face/back and label
self.updateFace = function () {
cardFace.visible = self.faceUp;
cardBack.visible = !self.faceUp;
// Only spades for MVP
var suitChar = '♠';
var rankStr = '';
if (self.rank === 1) rankStr = 'A';else if (self.rank === 11) rankStr = 'J';else if (self.rank === 12) rankStr = 'Q';else if (self.rank === 13) rankStr = 'K';else rankStr = '' + self.rank;
// Show only in corners
if (typeof labelTL !== "undefined" && typeof labelBR !== "undefined") {
var labelText = rankStr + suitChar;
labelTL.setText(labelText);
labelBR.setText(labelText);
labelTL.visible = true;
labelBR.visible = true;
if (self.faceUp) {
labelTL.setStyle({
fill: 0xffffff,
alpha: 1
});
labelBR.setStyle({
fill: 0xffffff,
alpha: 1
});
} else {
labelTL.setStyle({
fill: 0xcccccc,
alpha: 0.45
});
labelBR.setStyle({
fill: 0xcccccc,
alpha: 0.45
});
}
}
// Optionally, show a faint spider on the back for realism
self.updateSelected();
};
// Visual feedback for selection
self.updateSelected = function () {
if (self.selected) {
cardFace.tint = 0x99ccff;
cardBack.tint = 0x99ccff;
} else {
cardFace.tint = 0xffffff;
cardBack.tint = 0xffffff;
}
};
// Flip card
self.flip = function (faceUp) {
self.faceUp = faceUp;
self.updateFace();
};
// Touch events
self.down = function (x, y, obj) {
if (self.faceUp) {
// Let game handle selection
if (game) game.onCardDown(self, x, y, obj);
}
};
return self;
});
// Stock pile class (for dealing new rows)
var StockPile = Container.expand(function () {
var self = Container.call(this);
self.cards = [];
// Add card to stock
self.addCard = function (card) {
self.cards.push(card);
self.addChild(card);
card.x = 0;
card.y = 0;
};
// Deal one card per tableau pile
self.dealToTableau = function (tableauPiles) {
if (self.cards.length < tableauPiles.length) return false;
for (var i = 0; i < tableauPiles.length; i++) {
var card = self.cards.shift();
card.flip(true);
tableauPiles[i].addCard(card);
}
return true;
};
// Is empty?
self.isEmpty = function () {
return self.cards.length === 0;
};
return self;
});
// Tableau pile class
var TableauPile = Container.expand(function () {
var self = Container.call(this);
self.cards = [];
// Add card to pile
self.addCard = function (card) {
self.cards.push(card);
self.addChild(card);
self.layout();
};
// Remove card(s) from pile starting at index
self.removeCardsFrom = function (idx) {
var removed = [];
while (self.cards.length > idx) {
var c = self.cards.pop();
c.parent.removeChild(c);
removed.unshift(c);
}
self.layout();
return removed;
};
// Get top card
self.topCard = function () {
if (self.cards.length === 0) return null;
return self.cards[self.cards.length - 1];
};
// Layout cards in pile
self.layout = function () {
// Increase vertical spacing between cards for more space in each column
var CARD_VERTICAL_SPACING = 110;
for (var i = 0; i < self.cards.length; i++) {
var c = self.cards[i];
// Animate to new position using tween
tween.stop(c, {
x: true,
y: true
}); // Stop any previous tweens on x/y
tween(c, {
x: 0,
y: i * CARD_VERTICAL_SPACING
}, {
duration: 400,
easing: tween.cubicOut
});
c.zIndex = i;
}
};
// Reveal top card if needed
self.revealTop = function () {
var top = self.topCard();
if (top && !top.faceUp) {
top.flip(true);
}
};
// Get index of card in pile
self.indexOf = function (card) {
for (var i = 0; i < self.cards.length; i++) {
if (self.cards[i] === card) return i;
}
return -1;
};
// Can move sequence starting at idx?
self.canMoveSequence = function (idx) {
// All cards from idx to end must be face up and in descending order
for (var i = idx; i < self.cards.length - 1; i++) {
var c1 = self.cards[i];
var c2 = self.cards[i + 1];
if (!c1.faceUp || !c2.faceUp) return false;
if (c1.rank !== c2.rank + 1) return false;
}
return self.cards[idx].faceUp;
};
// Can accept a sequence of cards (cardsArr)?
self.canAccept = function (cardsArr) {
if (self.cards.length === 0) {
// Only King can be placed on empty pile
return cardsArr[0].rank === 13;
} else {
var top = self.topCard();
var moving = cardsArr[0];
return top.faceUp && top.rank === moving.rank + 1;
}
};
// Remove completed set (K-A)
self.removeCompleteSet = function () {
// Check if last 13 cards are a complete set
if (self.cards.length < 13) return false;
for (var i = 0; i < 13; i++) {
var c = self.cards[self.cards.length - 1 - i];
if (!c.faceUp || c.rank !== 13 - i) return false;
}
// Remove them
var removed = [];
for (var i = 0; i < 13; i++) {
var c = self.cards.pop();
if (c.parent) c.parent.removeChild(c);
removed.unshift(c);
}
self.layout();
// Add to collected area (handled by animation in game logic)
return removed; // Return the collected set for animation/cleanup
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xcccccc
});
/****
* Game Code
****/
// (Removed stock label)
// Show number of stacks left as card decks on stock pile
var stockDecks = [];
function updateStockDecks() {
// Remove old
for (var i = 0; i < stockDecks.length; i++) {
if (stockDecks[i].parent) stockDecks[i].parent.removeChild(stockDecks[i]);
}
stockDecks = [];
// Each "stack" is 10 cards (8 stacks of 10 = 80, but Spider uses 50 in stock: 5 stacks of 10)
var stacksLeft = Math.floor(stockPile.cards.length / TABLEAU_COUNT);
for (var i = 0; i < stacksLeft; i++) {
var deckImg = LK.getAsset('cardBack', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: CARD_WIDTH / BASE_CARD_WIDTH * 0.9,
scaleY: CARD_HEIGHT / BASE_CARD_HEIGHT * 0.9
});
// Center bottom: horizontally centered, stacked with slight offset
var centerX = GAME_WIDTH / 2;
var bottomY = GAME_HEIGHT - CARD_HEIGHT / 2 - 40;
deckImg.x = centerX + (i - (stacksLeft - 1) / 2) * 24;
deckImg.y = bottomY - i * 6;
// Do not blur or fade the stack count
deckImg.alpha = 1;
game.addChild(deckImg);
stockDecks.push(deckImg);
}
}
// Add a large faded spider icon to the background, centered and scaled for portrait
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
// Spider background: scale to fit width, center
var backgroundSpider = LK.getAsset('spider', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 10,
scaleY: 10
});
backgroundSpider.x = GAME_WIDTH / 2;
backgroundSpider.y = GAME_HEIGHT / 2;
backgroundSpider.alpha = 0.08;
game.addChildAt(backgroundSpider, 0); // Ensure it's behind all other elements
// Card and layout constants for portrait
var TABLEAU_COUNT = 8;
var GAME_MARGIN_X = 40;
var GAME_MARGIN_Y = 120;
// Get original card asset size
var tempCard = LK.getAsset('cardFace', {
anchorX: 0.5,
anchorY: 0.5
});
var BASE_CARD_WIDTH = tempCard.width;
var BASE_CARD_HEIGHT = tempCard.height;
// Calculate max card width to fit 8 columns with margin
var availableWidth = GAME_WIDTH - 2 * GAME_MARGIN_X;
var CARD_WIDTH = Math.floor(availableWidth / TABLEAU_COUNT);
var CARD_HEIGHT = Math.floor(BASE_CARD_HEIGHT * (CARD_WIDTH / BASE_CARD_WIDTH));
// No spacing between columns
var TABLEAU_SPACING = 0;
// Top offset for vertical layout (fit 2 rows of cards + margin)
var TABLEAU_TOP = GAME_MARGIN_Y + CARD_HEIGHT + 40;
// Center columns horizontally
var totalTableauWidth = TABLEAU_COUNT * CARD_WIDTH + (TABLEAU_COUNT - 1) * TABLEAU_SPACING;
var TABLEAU_LEFT = Math.round((GAME_WIDTH - totalTableauWidth) / 2);
// Stock and completed set positions (fit to portrait)
var STOCK_X = GAME_WIDTH - CARD_WIDTH - GAME_MARGIN_X;
var STOCK_Y = GAME_MARGIN_Y;
var COMPLETED_X = GAME_MARGIN_X + CARD_WIDTH / 2;
var COMPLETED_Y = GAME_MARGIN_Y;
// Clean up tempCard
if (tempCard.parent) tempCard.parent.removeChild(tempCard);
// Game state
var tableauPiles = [];
var stockPile = null;
var completedSets = 0;
var movesCount = 0; // Moves counter
var draggingCards = null;
var draggingFromPile = null;
var dragOffsetX = 0;
var dragOffsetY = 0;
var dragStartX = 0;
var dragStartY = 0;
var dragValidTarget = null;
var dragTargetPile = null;
var canDeal = true;
// Undo stack
var undoStack = [];
// Helper: auto-collect completed sets from all tableau piles
function autoCollectSets() {
var collected = false;
for (var auto_i = 0; auto_i < tableauPiles.length; auto_i++) {
var pile = tableauPiles[auto_i];
var removed;
while (removed = pile.removeCompleteSet()) {
LK.getSound('card_set').play();
completedSets += 1;
updateScore();
updateCompletedSpiders();
var spider = LK.getAsset('spider', {
anchorX: 0.5,
anchorY: 0.5
});
spider.x = pile.x + CARD_WIDTH / 2;
spider.y = pile.y + pile.cards.length * 40 + CARD_HEIGHT / 2;
game.addChild(spider);
(function (spider, setIndex, removedCards) {
tween(spider, {
y: COMPLETED_Y,
x: COMPLETED_X + setIndex * (CARD_WIDTH * 0.7)
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
if (spider.parent) spider.parent.removeChild(spider);
updateCompletedSpiders();
// Remove finished set cards from game
for (var rc = 0; rc < removedCards.length; rc++) {
if (removedCards[rc].parent) removedCards[rc].parent.removeChild(removedCards[rc]);
}
}
});
})(spider, completedSets - 1, removed);
collected = true;
}
}
// Win condition: 8 sets
if (completedSets >= 8) {
LK.showYouWin();
return;
}
}
// Helper: clone game state for undo
function cloneGameState() {
// Deep copy of tableau piles, stock, completedSets, movesCount
var tableau = [];
for (var i = 0; i < tableauPiles.length; i++) {
var pile = tableauPiles[i];
var pileArr = [];
for (var j = 0; j < pile.cards.length; j++) {
var c = pile.cards[j];
pileArr.push({
rank: c.rank,
suit: c.suit,
faceUp: c.faceUp
});
}
tableau.push(pileArr);
}
var stock = [];
for (var i = 0; i < stockPile.cards.length; i++) {
var c = stockPile.cards[i];
stock.push({
rank: c.rank,
suit: c.suit,
faceUp: c.faceUp
});
}
return {
tableau: tableau,
stock: stock,
completedSets: completedSets,
movesCount: movesCount
};
}
// Helper: restore game state from undo
function restoreGameState(state) {
// Remove all cards
for (var i = 0; i < tableauPiles.length; i++) {
var pile = tableauPiles[i];
while (pile.cards.length > 0) {
var c = pile.cards.pop();
if (c.parent) c.parent.removeChild(c);
}
}
while (stockPile.cards.length > 0) {
var c = stockPile.cards.pop();
if (c.parent) c.parent.removeChild(c);
}
// Restore tableau
for (var i = 0; i < tableauPiles.length; i++) {
var pileArr = state.tableau[i];
for (var j = 0; j < pileArr.length; j++) {
var cdata = pileArr[j];
var card = new Card();
card.setCard(cdata.rank, cdata.suit, cdata.faceUp);
tableauPiles[i].addCard(card);
}
}
// Restore stock
for (var i = 0; i < state.stock.length; i++) {
var cdata = state.stock[i];
var card = new Card();
card.setCard(cdata.rank, cdata.suit, cdata.faceUp);
stockPile.addCard(card);
}
completedSets = state.completedSets;
movesCount = state.movesCount;
updateScore();
updateCompletedSpiders();
canDeal = true;
updateStockDecks();
draggingCards = null;
draggingFromPile = null;
dragValidTarget = null;
dragTargetPile = null;
}
// Push undo state
function pushUndoState() {
// Limit undo stack to 50
if (undoStack.length > 50) undoStack.shift();
undoStack.push(cloneGameState());
}
// GUI
var scoreTxt = new Text2('0', {
size: 90,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var movesTxt = new Text2('Moves: 0', {
size: 70,
fill: 0xffffff
});
movesTxt.anchor.set(0.5, 0);
// Place moves counter under the sets counter (scoreTxt)
movesTxt.x = GAME_WIDTH / 2;
movesTxt.y = scoreTxt.y + scoreTxt.height + 10;
LK.gui.top.addChild(movesTxt);
// Undo button (now shown as an arrow icon)
var undoBtn = new Text2('↶', {
size: 180,
fill: 0xffffff
});
undoBtn.anchor.set(1, 1);
// Place at the bottom right corner of the screen (using LK.gui.bottomRight)
undoBtn.x = -40;
undoBtn.y = -40;
LK.gui.bottomRight.addChild(undoBtn);
// Moves counter next to undo button
var movesTxtBR = new Text2('0', {
size: 70,
fill: 0xffffff
});
movesTxtBR.anchor.set(1, 1);
// Place to the left of the undo button, aligned at bottom right
movesTxtBR.x = undoBtn.x - 120;
movesTxtBR.y = undoBtn.y - 10;
LK.gui.bottomRight.addChild(movesTxtBR);
// Undo button handler (moved here after undoBtn is defined)
undoBtn.down = function (x, y, obj) {
if (undoStack.length === 0) return;
var prev = undoStack.pop();
// When redoing, do not decrease movesCount; keep current movesCount
var currentMoves = movesCount;
restoreGameState(prev);
// Restore everything except movesCount (keep current)
movesCount = currentMoves;
updateScore();
};
// Restart button (shown as a circular arrow)
var restartBtn = new Text2('⟳', {
size: 120,
fill: 0xffffff
});
restartBtn.anchor.set(0, 0);
// Place at the top left corner of the screen (using LK.gui.topLeft)
// Add extra space to the right of the pause button (assume pauseBtn is at x=40, width=80, so start at 40+80+24=144)
restartBtn.x = 144; // Increased offset for more space between pause and restart
restartBtn.y = 40; // Down from top edge, visually aligned
LK.gui.topLeft.addChild(restartBtn);
restartBtn.down = function (x, y, obj) {
resetGame();
};
// Removed dealBtn from top right
// Completed sets display
var completedSpiders = [];
// Initialize tableau piles
for (var i = 0; i < TABLEAU_COUNT; i++) {
var pile = new TableauPile();
// Align piles evenly across the screen, centered
pile.x = TABLEAU_LEFT + i * (CARD_WIDTH + TABLEAU_SPACING) + CARD_WIDTH / 2;
pile.y = TABLEAU_TOP;
game.addChild(pile);
tableauPiles.push(pile);
}
// Initialize stock pile
stockPile = new StockPile();
stockPile.x = STOCK_X;
stockPile.y = STOCK_Y;
game.addChild(stockPile);
// (Removed stock label)
// Shuffle and deal cards
function shuffleDeck() {
// 8 decks of spades (104 cards)
var deck = [];
for (var d = 0; d < 8; d++) {
for (var r = 1; r <= 13; r++) {
var card = new Card();
card.setCard(r, 0, false);
deck.push(card);
}
}
// Fisher-Yates shuffle
for (var i = deck.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = deck[i];
deck[i] = deck[j];
deck[j] = tmp;
}
return deck;
}
function dealInitial() {
var deck = shuffleDeck();
// 52 cards to tableau (first 4 piles get 7, rest get 6)
for (var i = 0; i < TABLEAU_COUNT; i++) {
var count = i < 4 ? 7 : 6;
for (var j = 0; j < count; j++) {
var card = deck.shift();
tableauPiles[i].addCard(card);
}
}
// Flip top card of each pile
for (var i = 0; i < TABLEAU_COUNT; i++) {
tableauPiles[i].topCard().flip(true);
}
// Remaining 50 cards to stock
while (deck.length > 0) {
var card = deck.shift();
stockPile.addCard(card);
}
completedSets = 0;
updateScore();
updateCompletedSpiders();
updateStockDecks();
canDeal = true;
}
function updateScore() {
scoreTxt.setText('Sets: ' + completedSets);
if (typeof movesTxt !== "undefined") {
movesTxt.setText('Moves: ' + movesCount);
}
if (typeof movesTxtBR !== "undefined") {
movesTxtBR.setText(movesCount + '');
}
}
function updateCompletedSpiders() {
// Remove old
for (var i = 0; i < completedSpiders.length; i++) {
if (completedSpiders[i].parent) completedSpiders[i].parent.removeChild(completedSpiders[i]);
}
completedSpiders = [];
// Add new
for (var i = 0; i < completedSets; i++) {
var spider = LK.getAsset('spider', {
anchorX: 0.5,
anchorY: 0.5
});
spider.x = COMPLETED_X + i * (CARD_WIDTH * 0.7);
spider.y = COMPLETED_Y;
game.addChild(spider);
completedSpiders.push(spider);
}
}
// Handle card selection and drag
game.onCardDown = function (card, x, y, obj) {
// Find which pile this card is in
var pile = null;
var idx = -1;
for (var i = 0; i < tableauPiles.length; i++) {
var p = tableauPiles[i];
var j = p.indexOf(card);
if (j !== -1) {
pile = p;
idx = j;
break;
}
}
if (!pile) return;
// Can move sequence?
if (!pile.canMoveSequence(idx)) return;
// Prepare drag
pushUndoState();
draggingCards = [];
for (var k = idx; k < pile.cards.length; k++) {
var c = pile.cards[k];
c.selected = true;
c.updateSelected();
draggingCards.push(c);
}
draggingFromPile = pile;
// Try to auto-move to a valid pile
var autoMoved = false;
for (var t = 0; t < tableauPiles.length; t++) {
var targetPile = tableauPiles[t];
if (targetPile === pile) continue;
if (targetPile.canAccept(draggingCards)) {
// Remove from old pile
pile.removeCardsFrom(idx);
// Add to new pile
for (var m = 0; m < draggingCards.length; m++) {
targetPile.addCard(draggingCards[m]);
}
// Play card move sound
LK.getSound('card_move').play();
// Reveal top card in old pile
pile.revealTop();
// Check for completed set
var completedAny = false;
while (targetPile.removeCompleteSet()) {
// Play card set sound
LK.getSound('card_set').play();
completedSets += 1;
updateScore();
updateCompletedSpiders();
// Animate spider
var spider = LK.getAsset('spider', {
anchorX: 0.5,
anchorY: 0.5
});
spider.x = targetPile.x + CARD_WIDTH / 2;
spider.y = targetPile.y + targetPile.cards.length * 40 + CARD_HEIGHT / 2;
game.addChild(spider);
(function (spider, setIndex) {
tween(spider, {
y: COMPLETED_Y,
x: COMPLETED_X + setIndex * (CARD_WIDTH * 0.7)
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
if (spider.parent) spider.parent.removeChild(spider);
updateCompletedSpiders();
}
});
})(spider, completedSets - 1);
completedAny = true;
}
autoCollectSets();
// Win condition: 8 sets
if (completedSets >= 8) {
LK.showYouWin();
return;
}
// Deselect
for (var m = 0; m < draggingCards.length; m++) {
draggingCards[m].selected = false;
draggingCards[m].updateSelected();
}
movesCount += 1; // Increment moves on auto-move
updateScore();
draggingCards = null;
draggingFromPile = null;
dragValidTarget = null;
dragTargetPile = null;
autoMoved = true;
break;
}
}
if (autoMoved) return;
// Bring to front for manual drag
for (var k = 0; k < draggingCards.length; k++) {
game.addChild(draggingCards[k]);
}
// Offset
dragOffsetX = card.x;
dragOffsetY = card.y;
dragStartX = card.parent.x + card.x;
dragStartY = card.parent.y + card.y;
};
// Handle drag move
game.move = function (x, y, obj) {
if (!draggingCards) return;
// Move cards
for (var i = 0; i < draggingCards.length; i++) {
draggingCards[i].x = x - dragOffsetX;
draggingCards[i].y = y - dragOffsetY + i * 60;
}
// Check for valid drop target
dragValidTarget = null;
dragTargetPile = null;
for (var i = 0; i < tableauPiles.length; i++) {
var pile = tableauPiles[i];
// Don't allow drop on self
if (pile === draggingFromPile) continue;
// Get pile bounds
var px = pile.x;
var py = pile.y + pile.cards.length * 40;
var pw = CARD_WIDTH;
var ph = CARD_HEIGHT;
// If pile is empty, allow drop anywhere in the pile area
if (pile.cards.length === 0) {
px = pile.x;
py = pile.y;
pw = CARD_WIDTH;
ph = CARD_HEIGHT;
if (x > px && x < px + pw && y > py && y < py + ph) {
if (pile.canAccept(draggingCards)) {
dragValidTarget = pile;
dragTargetPile = pile;
break;
}
}
} else {
if (x > px && x < px + pw && y > py - 40 && y < py + ph) {
if (pile.canAccept(draggingCards)) {
dragValidTarget = pile;
dragTargetPile = pile;
break;
}
}
}
}
};
// Handle drag end
game.up = function (x, y, obj) {
if (!draggingCards) return;
// Drop
if (dragValidTarget && dragTargetPile) {
pushUndoState();
// Remove from old pile
var idx = draggingFromPile.indexOf(draggingCards[0]);
draggingFromPile.removeCardsFrom(idx);
// Add to new pile
for (var i = 0; i < draggingCards.length; i++) {
dragTargetPile.addCard(draggingCards[i]);
}
// Play card move sound
LK.getSound('card_move').play();
movesCount += 1; // Increment moves on manual drag
updateScore();
// Reveal top card in old pile
draggingFromPile.revealTop();
// Check for completed set
var completedAny = false;
while (dragTargetPile.removeCompleteSet()) {
// Play card set sound
LK.getSound('card_set').play();
completedSets += 1;
updateScore();
updateCompletedSpiders();
// Animate spider
var spider = LK.getAsset('spider', {
anchorX: 0.5,
anchorY: 0.5
});
spider.x = dragTargetPile.x + CARD_WIDTH / 2;
spider.y = dragTargetPile.y + dragTargetPile.cards.length * 40 + CARD_HEIGHT / 2;
game.addChild(spider);
(function (spider, setIndex) {
tween(spider, {
y: COMPLETED_Y,
x: COMPLETED_X + setIndex * (CARD_WIDTH * 0.7)
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
if (spider.parent) spider.parent.removeChild(spider);
updateCompletedSpiders();
}
});
})(spider, completedSets - 1);
completedAny = true;
}
autoCollectSets();
// Win condition: 8 sets
if (completedSets >= 8) {
LK.showYouWin();
return;
}
} else {
// Return to original pile
for (var i = 0; i < draggingCards.length; i++) {
draggingCards[i].x = dragStartX - draggingFromPile.x;
draggingCards[i].y = dragStartY - draggingFromPile.y + i * 60;
draggingFromPile.addChild(draggingCards[i]);
}
draggingFromPile.layout();
}
// Deselect
for (var i = 0; i < draggingCards.length; i++) {
draggingCards[i].selected = false;
draggingCards[i].updateSelected();
}
draggingCards = null;
draggingFromPile = null;
dragValidTarget = null;
dragTargetPile = null;
};
// Deal button (shown above the stock pile decks)
var dealBtn = new Text2('Deal', {
size: 90,
fill: 0xffffff
});
dealBtn.anchor.set(0.5, 1);
// Position: above the stock pile decks, centered horizontally
dealBtn.x = GAME_WIDTH / 2;
dealBtn.y = GAME_HEIGHT - CARD_HEIGHT / 2 - 40 - 30;
game.addChild(dealBtn);
// Deal new row
dealBtn.down = function (x, y, obj) {
if (!canDeal) return;
// Only allow if all tableau piles have at least one card
for (var i = 0; i < tableauPiles.length; i++) {
if (tableauPiles[i].cards.length === 0) return;
}
if (stockPile.isEmpty()) return;
canDeal = false;
pushUndoState();
// Animate deal
var dealt = stockPile.dealToTableau(tableauPiles);
if (dealt) {
// Play shuffle sound when dealing from stock
LK.getSound('shuffle').play();
// Flip top card of each pile
for (var i = 0; i < tableauPiles.length; i++) {
tableauPiles[i].topCard().flip(true);
}
updateStockDecks();
autoCollectSets();
}
// Allow next deal after short delay
LK.setTimeout(function () {
canDeal = true;
}, 400);
};
// Touch on stock pile (deal)
stockPile.down = function (x, y, obj) {
dealBtn.down(x, y, obj);
};
// Reset game
function resetGame() {
// Remove all cards
for (var i = 0; i < tableauPiles.length; i++) {
var pile = tableauPiles[i];
while (pile.cards.length > 0) {
var c = pile.cards.pop();
if (c.parent) c.parent.removeChild(c);
}
}
while (stockPile.cards.length > 0) {
var c = stockPile.cards.pop();
if (c.parent) c.parent.removeChild(c);
}
completedSets = 0;
movesCount = 0; // Reset moves counter
updateScore();
updateCompletedSpiders();
updateStockDecks();
canDeal = true;
draggingCards = null;
draggingFromPile = null;
dragValidTarget = null;
dragTargetPile = null;
dealInitial();
}
// Game over: no more moves
function checkGameOver() {
// If no moves and stock is empty
var canMove = false;
for (var i = 0; i < tableauPiles.length; i++) {
var pile = tableauPiles[i];
for (var j = 0; j < pile.cards.length; j++) {
if (pile.canMoveSequence(j)) {
// Try to move to another pile
var seq = [];
for (var k = j; k < pile.cards.length; k++) seq.push(pile.cards[k]);
for (var t = 0; t < tableauPiles.length; t++) {
if (t === i) continue;
if (tableauPiles[t].canAccept(seq)) {
canMove = true;
break;
}
}
}
if (canMove) break;
}
if (canMove) break;
}
if (!canMove && stockPile.isEmpty()) {
LK.showGameOver();
}
}
// Game update
game.update = function () {
// (auto-collect logic moved to after every move, deal, and flip)
// Check for game over
checkGameOver();
};
// Start game
dealInitial();
LK.playMusic('relaxing_bg');