/****
* 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');