User prompt
Please fix the bug: 'Timeout.tick error: Cannot read properties of undefined (reading 'length')' in or related to this line: 'if (game.deck.length === 0) {' Line Number: 70
User prompt
Please fix the bug: 'Timeout.tick error: Cannot read properties of undefined (reading 'length')' in or related to this line: 'var topDiscard = game.discardPile.length > 0 ? game.discardPile[game.discardPile.length - 1] : null;' Line Number: 39
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'fill')' in or related to this line: 'infoText.style.fill = color || "#fff";' Line Number: 387
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'fill')' in or related to this line: 'rankText.style.fill = "#222";' Line Number: 165
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'fill')' in or related to this line: 'if (typeof rankText.setFill === "function") {' Line Number: 157
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'fill')' in or related to this line: 'rankText.style.fill = "#d22";' Line Number: 139
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'fill')' in or related to this line: 'rankText.style.fill = "#222";' Line Number: 152
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'fill')' in or related to this line: 'rankText.style.fill = "#222";' Line Number: 147
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'fill')' in or related to this line: 'rankText.style.fill = "#d22";' Line Number: 135
Code edit (1 edits merged)
Please save this source code
User prompt
Thirty-One Blitz
Initial prompt
31
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Simple AI class (draws/discards at intervals) var AIPlayer = Container.expand(function () { var self = Container.call(this); self.hand = []; self.name = "AI"; self.knocked = false; // AI makes a move (draw/discard/knock) self.makeMove = function () { if (game.gameOver) return; // If hand value >= 28, knock with 50% chance, always knock at 31 var best = bestHandValue(self.hand); if (best.value === 31) { game.aiKnock(); return; } if (best.value >= 28 && Math.random() < 0.5) { game.aiKnock(); return; } // Draw from discard if it helps, else draw from deck var topDiscard = game.discardPile && game.discardPile.length > 0 ? game.discardPile[game.discardPile.length - 1] : null; var wantDiscard = false; if (topDiscard) { // Would taking this card improve hand? var replaced = -1, bestWithDiscard = best; for (var i = 0; i < self.hand.length; ++i) { var testHand = self.hand.slice(); testHand[i] = topDiscard; var val = bestHandValue(testHand); if (val.value > bestWithDiscard.value) { bestWithDiscard = val; replaced = i; } } if (bestWithDiscard.value > best.value) { wantDiscard = true; } } if (wantDiscard) { // Take from discard var card = game.discardPile.pop(); self.hand.push(card); // Discard lowest value card var idx = lowestValueCardIndex(self.hand); var discard = self.hand.splice(idx, 1)[0]; game.discardPile.push(discard); game.animateAIDraw(card, 'discard'); game.animateAIDiscard(discard); } else { // Draw from deck if (!game.deck || game.deck.length === 0) { // Can't draw, must knock game.aiKnock(); return; } var card = game.deck.pop(); self.hand.push(card); // Discard lowest value card var idx = lowestValueCardIndex(self.hand); var discard = self.hand.splice(idx, 1)[0]; game.discardPile.push(discard); game.animateAIDraw(card, 'deck'); game.animateAIDiscard(discard); } }; return self; }); // Card class: represents a single card (visual and data) var Card = Container.expand(function () { var self = Container.call(this); // Card data self.suit = null; // '♠', '♥', '♦', '♣' self.rank = null; // 1-13 (Ace-King) self.faceUp = true; self.cardIndex = -1; // index in deck, for uniqueness // Visuals var cardFront = self.attachAsset('cardFront', { anchorX: 0.5, anchorY: 0.5 }); var cardBack = self.attachAsset('cardBack', { anchorX: 0.5, anchorY: 0.5 }); cardBack.visible = false; // Text overlays var rankText = new Text2('', { size: 70, fill: "#222" }); rankText.anchor.set(0.5, 0.2); self.addChild(rankText); var suitText = new Text2('', { size: 90, fill: "#222" }); suitText.anchor.set(0.5, 0.6); self.addChild(suitText); // Set card data and visuals self.setCard = function (suit, rank, faceUp, cardIndex) { self.suit = suit; self.rank = rank; self.faceUp = faceUp !== false; self.cardIndex = cardIndex; self.updateVisual(); }; // Update visuals based on faceUp self.updateVisual = function () { cardFront.visible = self.faceUp; cardBack.visible = !self.faceUp; if (self.faceUp) { rankText.setText(cardRankToString(self.rank)); suitText.setText(self.suit); // Color for suit if (self.suit === '♥' || self.suit === '♦') { if (rankText.style && typeof rankText.style.setFill === "function") { rankText.style.setFill("#d22"); suitText.style.setFill("#d22"); } else { if (typeof rankText.setFill === "function") { rankText.setFill("#d22"); suitText.setFill("#d22"); } else { if (rankText.style) { rankText.style.fill = "#d22"; } if (suitText.style) { suitText.style.fill = "#d22"; } } } } else { if (rankText.style && typeof rankText.style.setFill === "function") { rankText.style.setFill("#222"); suitText.style.setFill("#222"); } else { if (rankText.style && typeof rankText.style.setFill === "function") { rankText.style.setFill("#222"); suitText.style.setFill("#222"); } else { // fallback for older Text2 versions if (typeof rankText.setFill === "function") { rankText.setFill("#222"); suitText.setFill("#222"); } else { if (typeof rankText.setFill === "function") { rankText.setFill("#222"); suitText.setFill("#222"); } else if (rankText.style) { rankText.style.fill = "#222"; suitText.style.fill = "#222"; } } } } } } else { rankText.setText(''); suitText.setText(''); } }; // Flip card self.flip = function (toFaceUp) { self.faceUp = toFaceUp; self.updateVisual(); }; // For drag highlight self.highlight = function (color, duration) { tween(self, { scaleX: 1.1, scaleY: 1.1 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: duration || 100, easing: tween.easeIn }); } }); }; // For simple card tap feedback self.flash = function () { LK.effects.flashObject(self, 0xffff99, 200); }; // Card tap event (handled in game) self.down = function (x, y, obj) {}; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x0a3a1a }); /**** * Game Code ****/ // Draw pile highlight // Discard pile highlight // Card front (white, will overlay suit/rank) // Card back // Card data var SUITS = ['♠', '♥', '♦', '♣']; var RANKS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; // Ace-King // Layout var HAND_Y = 2200; var HAND_X_START = 400; var HAND_SPACING = 260; var AI_HAND_Y = 400; var AI_HAND_X_START = 400; var PILE_X = 1024; var DRAW_PILE_Y = 1200; var DISCARD_PILE_Y = 1500; // Game state var deck = []; var discardPile = []; var playerHand = []; var aiHand = []; var ai = null; var selectedCardIndex = -1; var draggingCard = null; var dragOffsetX = 0; var dragOffsetY = 0; var canDraw = true; var canDiscard = false; var gameOver = false; var aiMoveTimeout = null; var playerKnocked = false; var aiKnocked = false; var turn = 'player'; // 'player' or 'ai' var timerText = null; var timer = null; var timeLeft = 60; // seconds // GUI var infoText = null; var knockButton = null; var discardHighlight = null; var drawHighlight = null; // Helper: Card rank to string function cardRankToString(rank) { if (rank === 1) return 'A'; if (rank === 11) return 'J'; if (rank === 12) return 'Q'; if (rank === 13) return 'K'; return '' + rank; } // Helper: Card value for 31 function cardValue(card) { if (!card) return 0; if (card.rank > 10) return 10; if (card.rank === 1) return 11; return card.rank; } // Helper: Best hand value (max sum of one suit) function bestHandValue(hand) { var suitTotals = { '♠': 0, '♥': 0, '♦': 0, '♣': 0 }; for (var i = 0; i < hand.length; ++i) { var c = hand[i]; suitTotals[c.suit] += cardValue(c); } var bestSuit = '♠', bestValue = suitTotals['♠']; for (var s = 0; s < SUITS.length; ++s) { if (suitTotals[SUITS[s]] > bestValue) { bestSuit = SUITS[s]; bestValue = suitTotals[SUITS[s]]; } } return { suit: bestSuit, value: bestValue }; } // Helper: Find lowest value card index in hand function lowestValueCardIndex(hand) { var best = bestHandValue(hand); var minIdx = -1, minVal = 100; for (var i = 0; i < hand.length; ++i) { var c = hand[i]; // If not in best suit, prefer to discard if (c.suit !== best.suit) return i; // Otherwise, lowest value in best suit var v = cardValue(c); if (v < minVal) { minVal = v; minIdx = i; } } return minIdx >= 0 ? minIdx : 0; } // Helper: Shuffle array function shuffle(arr) { for (var i = arr.length - 1; i > 0; --i) { var j = Math.floor(Math.random() * (i + 1)); var t = arr[i]; arr[i] = arr[j]; arr[j] = t; } } // Helper: Create a new deck (array of Card objects) function createDeck() { var cards = []; var idx = 0; for (var s = 0; s < SUITS.length; ++s) { for (var r = 0; r < RANKS.length; ++r) { var c = new Card(); c.setCard(SUITS[s], RANKS[r], false, idx++); cards.push(c); } } shuffle(cards); return cards; } // Helper: Remove all children from a container function removeAll(container) { while (container.children.length > 0) { var c = container.children[0]; if (c.destroy) c.destroy();else container.removeChild(c); } } // Helper: Animate card to position function animateCardTo(card, x, y, onFinish) { tween(card, { x: x, y: y }, { duration: 250, easing: tween.cubicOut, onFinish: onFinish }); } // Helper: Show info text function showInfo(msg, color) { infoText.setText(msg); if (typeof infoText.setFill === "function") { infoText.setFill(color || "#fff"); } else if (infoText.style) { infoText.style.fill = color || "#fff"; } infoText.visible = true; tween(infoText, { alpha: 1 }, { duration: 100 }); LK.setTimeout(function () { tween(infoText, { alpha: 0 }, { duration: 600, onFinish: function onFinish() { infoText.visible = false; } }); }, 1200); } // Helper: End game function endGame(msg, color) { gameOver = true; showInfo(msg, color); LK.setTimeout(function () { LK.showGameOver(); }, 1200); } // Helper: Win game function winGame(msg, color) { gameOver = true; showInfo(msg, color); LK.setTimeout(function () { LK.showYouWin(); }, 1200); } // Helper: Update timer function updateTimer() { timerText.setText(timeLeft + "s"); if (timeLeft <= 0) { endGame("Time's up!", "#f44"); } } // Helper: Lay out player's hand function layoutPlayerHand() { for (var i = 0; i < playerHand.length; ++i) { var c = playerHand[i]; c.x = HAND_X_START + i * HAND_SPACING; c.y = HAND_Y; c.zIndex = 10 + i; c.scaleX = c.scaleY = 1; c.faceUp = true; c.updateVisual(); } } // Helper: Lay out AI's hand (face down) function layoutAIHand() { for (var i = 0; i < aiHand.length; ++i) { var c = aiHand[i]; c.x = AI_HAND_X_START + i * HAND_SPACING; c.y = AI_HAND_Y; c.zIndex = 10 + i; c.faceUp = false; c.updateVisual(); } } // Helper: Lay out piles function layoutPiles() { // Draw pile if (deck.length > 0) { var top = deck[deck.length - 1]; top.x = PILE_X - 180; top.y = DRAW_PILE_Y; top.zIndex = 100; top.faceUp = false; top.updateVisual(); top.visible = true; } // Discard pile if (discardPile.length > 0) { var top = discardPile[discardPile.length - 1]; top.x = PILE_X + 180; top.y = DISCARD_PILE_Y; top.zIndex = 100; top.faceUp = true; top.updateVisual(); top.visible = true; } } // Helper: Remove all cards from stage function removeAllCards() { for (var i = 0; i < deck.length; ++i) deck[i].visible = false; for (var i = 0; i < discardPile.length; ++i) discardPile[i].visible = false; for (var i = 0; i < playerHand.length; ++i) playerHand[i].visible = false; for (var i = 0; i < aiHand.length; ++i) aiHand[i].visible = false; } // Helper: Animate AI draw game.animateAIDraw = function (card, from) { card.visible = true; if (from === 'deck') { card.x = PILE_X - 180; card.y = DRAW_PILE_Y; } else { card.x = PILE_X + 180; card.y = DISCARD_PILE_Y; } animateCardTo(card, AI_HAND_X_START + (aiHand.length - 1) * HAND_SPACING, AI_HAND_Y, function () { layoutAIHand(); }); }; // Helper: Animate AI discard game.animateAIDiscard = function (card) { animateCardTo(card, PILE_X + 180, DISCARD_PILE_Y, function () { layoutPiles(); }); }; // Helper: AI knock game.aiKnock = function () { aiKnocked = true; showInfo("AI knocks!", "#ff0"); // Reveal AI hand for (var i = 0; i < aiHand.length; ++i) { aiHand[i].faceUp = true; aiHand[i].updateVisual(); } // Compare hands after short delay LK.setTimeout(function () { var playerVal = bestHandValue(playerHand).value; var aiVal = bestHandValue(aiHand).value; if (aiVal > playerVal) { endGame("AI wins! (" + aiVal + " vs " + playerVal + ")", "#f44"); } else if (playerVal > aiVal) { winGame("You win! (" + playerVal + " vs " + aiVal + ")", "#4f4"); } else { endGame("Draw! (" + playerVal + ")", "#fff"); } }, 1200); }; // Helper: Player knock function playerKnock() { if (gameOver || playerKnocked) return; playerKnocked = true; showInfo("You knock!", "#ff0"); // Reveal AI hand for (var i = 0; i < aiHand.length; ++i) { aiHand[i].faceUp = true; aiHand[i].updateVisual(); } // Compare hands after short delay LK.setTimeout(function () { var playerVal = bestHandValue(playerHand).value; var aiVal = bestHandValue(aiHand).value; if (playerVal > aiVal) { winGame("You win! (" + playerVal + " vs " + aiVal + ")", "#4f4"); } else if (aiVal > playerVal) { endGame("AI wins! (" + aiVal + " vs " + playerVal + ")", "#f44"); } else { endGame("Draw! (" + playerVal + ")", "#fff"); } }, 1200); } // Helper: Start AI move timer function startAIMove() { if (aiMoveTimeout) LK.clearTimeout(aiMoveTimeout); aiMoveTimeout = LK.setTimeout(function () { if (!gameOver && ai && !aiKnocked) { ai.makeMove(); // If AI didn't knock, return turn to player if (!aiKnocked) { turn = 'player'; canDraw = true; canDiscard = false; showInfo("Your turn!", "#fff"); } } }, 900); } // Helper: Start timer function startTimer() { if (timer) LK.clearInterval(timer); timer = LK.setInterval(function () { if (gameOver) return; timeLeft -= 1; updateTimer(); }, 1000); } // Helper: Reset game state function resetGame() { // Remove all cards removeAllCards(); deck = createDeck(); discardPile = []; playerHand = []; aiHand = []; ai = new AIPlayer(); selectedCardIndex = -1; draggingCard = null; canDraw = true; canDiscard = false; gameOver = false; aiMoveTimeout = null; playerKnocked = false; aiKnocked = false; turn = 'player'; timeLeft = 60; // Deal 3 cards to each for (var i = 0; i < 3; ++i) { var c = deck.pop(); c.faceUp = true; c.visible = true; playerHand.push(c); game.addChild(c); } for (var i = 0; i < 3; ++i) { var c = deck.pop(); c.faceUp = false; c.visible = true; aiHand.push(c); game.addChild(c); } // Discard one card to start var c = deck.pop(); c.faceUp = true; c.visible = true; discardPile.push(c); game.addChild(c); // Add remaining deck cards to stage (face down) for (var i = 0; i < deck.length; ++i) { deck[i].faceUp = false; deck[i].visible = true; game.addChild(deck[i]); } // Add AI hand to stage for (var i = 0; i < aiHand.length; ++i) { aiHand[i].faceUp = false; aiHand[i].visible = true; game.addChild(aiHand[i]); } // Layout layoutPlayerHand(); layoutAIHand(); layoutPiles(); // Update GUI updateHandValueText(); updateTimer(); // Start timer startTimer(); // Show info showInfo("Your turn!", "#fff"); } // Helper: Update hand value text function updateHandValueText() { var val = bestHandValue(playerHand); handValueText.setText("Best: " + val.value + " " + val.suit); if (val.value === 31) { winGame("31! You win!", "#4f4"); } } // GUI: Info text infoText = new Text2('', { size: 90, fill: "#fff" }); infoText.anchor.set(0.5, 0.5); infoText.x = 1024; infoText.y = 900; infoText.alpha = 0; infoText.visible = false; LK.gui.top.addChild(infoText); // GUI: Timer timerText = new Text2('60s', { size: 90, fill: "#fff" }); timerText.anchor.set(0.5, 0); timerText.x = 1024; timerText.y = 110; LK.gui.top.addChild(timerText); // GUI: Hand value var handValueText = new Text2('', { size: 80, fill: "#fff" }); handValueText.anchor.set(0.5, 0); handValueText.x = 1024; handValueText.y = HAND_Y + 200; LK.gui.top.addChild(handValueText); // GUI: Knock button knockButton = new Text2('Knock', { size: 90, fill: "#fff", font: "Impact" }); knockButton.anchor.set(0.5, 0.5); knockButton.x = 1800; knockButton.y = HAND_Y + 100; knockButton.interactive = true; knockButton.buttonMode = true; knockButton.alpha = 0.95; knockButton.down = function (x, y, obj) { if (gameOver || !canDraw) return; playerKnock(); }; LK.gui.bottom.addChild(knockButton); // GUI: Discard highlight discardHighlight = LK.getAsset('discardHighlight', { anchorX: 0.5, anchorY: 0.5, x: PILE_X + 180, y: DISCARD_PILE_Y }); discardHighlight.alpha = 0; game.addChild(discardHighlight); // GUI: Draw highlight drawHighlight = LK.getAsset('drawHighlight', { anchorX: 0.5, anchorY: 0.5, x: PILE_X - 180, y: DRAW_PILE_Y }); drawHighlight.alpha = 0; game.addChild(drawHighlight); // --- Input Handling --- // Touch/click down game.down = function (x, y, obj) { if (gameOver) return; // If it's player's turn if (turn !== 'player') return; // Check if drawing from deck if (canDraw && deck.length > 0) { var top = deck[deck.length - 1]; var dx = x - (PILE_X - 180), dy = y - DRAW_PILE_Y; if (Math.abs(dx) < 110 && Math.abs(dy) < 160) { // Draw from deck canDraw = false; canDiscard = true; var card = deck.pop(); card.faceUp = true; card.visible = true; playerHand.push(card); animateCardTo(card, HAND_X_START + (playerHand.length - 1) * HAND_SPACING, HAND_Y, function () { layoutPlayerHand(); updateHandValueText(); }); drawHighlight.alpha = 0.2; LK.setTimeout(function () { drawHighlight.alpha = 0; }, 200); return; } } // Check if drawing from discard if (canDraw && discardPile.length > 0) { var top = discardPile[discardPile.length - 1]; var dx = x - (PILE_X + 180), dy = y - DISCARD_PILE_Y; if (Math.abs(dx) < 110 && Math.abs(dy) < 160) { // Draw from discard canDraw = false; canDiscard = true; var card = discardPile.pop(); card.faceUp = true; card.visible = true; playerHand.push(card); animateCardTo(card, HAND_X_START + (playerHand.length - 1) * HAND_SPACING, HAND_Y, function () { layoutPlayerHand(); updateHandValueText(); }); discardHighlight.alpha = 0.2; LK.setTimeout(function () { discardHighlight.alpha = 0; }, 200); return; } } // If can discard, check if a card in hand is tapped if (canDiscard) { for (var i = 0; i < playerHand.length; ++i) { var c = playerHand[i]; var dx = x - c.x, dy = y - c.y; if (Math.abs(dx) < 110 && Math.abs(dy) < 160) { // Discard this card canDiscard = false; var discard = playerHand.splice(i, 1)[0]; discard.faceUp = true; discard.visible = true; discardPile.push(discard); animateCardTo(discard, PILE_X + 180, DISCARD_PILE_Y, function () { layoutPiles(); }); discardHighlight.alpha = 0.2; LK.setTimeout(function () { discardHighlight.alpha = 0; }, 200); layoutPlayerHand(); updateHandValueText(); // End turn, AI moves turn = 'ai'; startAIMove(); return; } } } }; // Touch/click up game.up = function (x, y, obj) { // No drag in this version }; // Touch/click move game.move = function (x, y, obj) { // No drag in this version }; // Main update loop game.update = function () { // No per-frame logic needed }; // Start game resetGame();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Simple AI class (draws/discards at intervals)
var AIPlayer = Container.expand(function () {
var self = Container.call(this);
self.hand = [];
self.name = "AI";
self.knocked = false;
// AI makes a move (draw/discard/knock)
self.makeMove = function () {
if (game.gameOver) return;
// If hand value >= 28, knock with 50% chance, always knock at 31
var best = bestHandValue(self.hand);
if (best.value === 31) {
game.aiKnock();
return;
}
if (best.value >= 28 && Math.random() < 0.5) {
game.aiKnock();
return;
}
// Draw from discard if it helps, else draw from deck
var topDiscard = game.discardPile && game.discardPile.length > 0 ? game.discardPile[game.discardPile.length - 1] : null;
var wantDiscard = false;
if (topDiscard) {
// Would taking this card improve hand?
var replaced = -1,
bestWithDiscard = best;
for (var i = 0; i < self.hand.length; ++i) {
var testHand = self.hand.slice();
testHand[i] = topDiscard;
var val = bestHandValue(testHand);
if (val.value > bestWithDiscard.value) {
bestWithDiscard = val;
replaced = i;
}
}
if (bestWithDiscard.value > best.value) {
wantDiscard = true;
}
}
if (wantDiscard) {
// Take from discard
var card = game.discardPile.pop();
self.hand.push(card);
// Discard lowest value card
var idx = lowestValueCardIndex(self.hand);
var discard = self.hand.splice(idx, 1)[0];
game.discardPile.push(discard);
game.animateAIDraw(card, 'discard');
game.animateAIDiscard(discard);
} else {
// Draw from deck
if (!game.deck || game.deck.length === 0) {
// Can't draw, must knock
game.aiKnock();
return;
}
var card = game.deck.pop();
self.hand.push(card);
// Discard lowest value card
var idx = lowestValueCardIndex(self.hand);
var discard = self.hand.splice(idx, 1)[0];
game.discardPile.push(discard);
game.animateAIDraw(card, 'deck');
game.animateAIDiscard(discard);
}
};
return self;
});
// Card class: represents a single card (visual and data)
var Card = Container.expand(function () {
var self = Container.call(this);
// Card data
self.suit = null; // '♠', '♥', '♦', '♣'
self.rank = null; // 1-13 (Ace-King)
self.faceUp = true;
self.cardIndex = -1; // index in deck, for uniqueness
// Visuals
var cardFront = self.attachAsset('cardFront', {
anchorX: 0.5,
anchorY: 0.5
});
var cardBack = self.attachAsset('cardBack', {
anchorX: 0.5,
anchorY: 0.5
});
cardBack.visible = false;
// Text overlays
var rankText = new Text2('', {
size: 70,
fill: "#222"
});
rankText.anchor.set(0.5, 0.2);
self.addChild(rankText);
var suitText = new Text2('', {
size: 90,
fill: "#222"
});
suitText.anchor.set(0.5, 0.6);
self.addChild(suitText);
// Set card data and visuals
self.setCard = function (suit, rank, faceUp, cardIndex) {
self.suit = suit;
self.rank = rank;
self.faceUp = faceUp !== false;
self.cardIndex = cardIndex;
self.updateVisual();
};
// Update visuals based on faceUp
self.updateVisual = function () {
cardFront.visible = self.faceUp;
cardBack.visible = !self.faceUp;
if (self.faceUp) {
rankText.setText(cardRankToString(self.rank));
suitText.setText(self.suit);
// Color for suit
if (self.suit === '♥' || self.suit === '♦') {
if (rankText.style && typeof rankText.style.setFill === "function") {
rankText.style.setFill("#d22");
suitText.style.setFill("#d22");
} else {
if (typeof rankText.setFill === "function") {
rankText.setFill("#d22");
suitText.setFill("#d22");
} else {
if (rankText.style) {
rankText.style.fill = "#d22";
}
if (suitText.style) {
suitText.style.fill = "#d22";
}
}
}
} else {
if (rankText.style && typeof rankText.style.setFill === "function") {
rankText.style.setFill("#222");
suitText.style.setFill("#222");
} else {
if (rankText.style && typeof rankText.style.setFill === "function") {
rankText.style.setFill("#222");
suitText.style.setFill("#222");
} else {
// fallback for older Text2 versions
if (typeof rankText.setFill === "function") {
rankText.setFill("#222");
suitText.setFill("#222");
} else {
if (typeof rankText.setFill === "function") {
rankText.setFill("#222");
suitText.setFill("#222");
} else if (rankText.style) {
rankText.style.fill = "#222";
suitText.style.fill = "#222";
}
}
}
}
}
} else {
rankText.setText('');
suitText.setText('');
}
};
// Flip card
self.flip = function (toFaceUp) {
self.faceUp = toFaceUp;
self.updateVisual();
};
// For drag highlight
self.highlight = function (color, duration) {
tween(self, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: duration || 100,
easing: tween.easeIn
});
}
});
};
// For simple card tap feedback
self.flash = function () {
LK.effects.flashObject(self, 0xffff99, 200);
};
// Card tap event (handled in game)
self.down = function (x, y, obj) {};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x0a3a1a
});
/****
* Game Code
****/
// Draw pile highlight
// Discard pile highlight
// Card front (white, will overlay suit/rank)
// Card back
// Card data
var SUITS = ['♠', '♥', '♦', '♣'];
var RANKS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; // Ace-King
// Layout
var HAND_Y = 2200;
var HAND_X_START = 400;
var HAND_SPACING = 260;
var AI_HAND_Y = 400;
var AI_HAND_X_START = 400;
var PILE_X = 1024;
var DRAW_PILE_Y = 1200;
var DISCARD_PILE_Y = 1500;
// Game state
var deck = [];
var discardPile = [];
var playerHand = [];
var aiHand = [];
var ai = null;
var selectedCardIndex = -1;
var draggingCard = null;
var dragOffsetX = 0;
var dragOffsetY = 0;
var canDraw = true;
var canDiscard = false;
var gameOver = false;
var aiMoveTimeout = null;
var playerKnocked = false;
var aiKnocked = false;
var turn = 'player'; // 'player' or 'ai'
var timerText = null;
var timer = null;
var timeLeft = 60; // seconds
// GUI
var infoText = null;
var knockButton = null;
var discardHighlight = null;
var drawHighlight = null;
// Helper: Card rank to string
function cardRankToString(rank) {
if (rank === 1) return 'A';
if (rank === 11) return 'J';
if (rank === 12) return 'Q';
if (rank === 13) return 'K';
return '' + rank;
}
// Helper: Card value for 31
function cardValue(card) {
if (!card) return 0;
if (card.rank > 10) return 10;
if (card.rank === 1) return 11;
return card.rank;
}
// Helper: Best hand value (max sum of one suit)
function bestHandValue(hand) {
var suitTotals = {
'♠': 0,
'♥': 0,
'♦': 0,
'♣': 0
};
for (var i = 0; i < hand.length; ++i) {
var c = hand[i];
suitTotals[c.suit] += cardValue(c);
}
var bestSuit = '♠',
bestValue = suitTotals['♠'];
for (var s = 0; s < SUITS.length; ++s) {
if (suitTotals[SUITS[s]] > bestValue) {
bestSuit = SUITS[s];
bestValue = suitTotals[SUITS[s]];
}
}
return {
suit: bestSuit,
value: bestValue
};
}
// Helper: Find lowest value card index in hand
function lowestValueCardIndex(hand) {
var best = bestHandValue(hand);
var minIdx = -1,
minVal = 100;
for (var i = 0; i < hand.length; ++i) {
var c = hand[i];
// If not in best suit, prefer to discard
if (c.suit !== best.suit) return i;
// Otherwise, lowest value in best suit
var v = cardValue(c);
if (v < minVal) {
minVal = v;
minIdx = i;
}
}
return minIdx >= 0 ? minIdx : 0;
}
// Helper: Shuffle array
function shuffle(arr) {
for (var i = arr.length - 1; i > 0; --i) {
var j = Math.floor(Math.random() * (i + 1));
var t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
// Helper: Create a new deck (array of Card objects)
function createDeck() {
var cards = [];
var idx = 0;
for (var s = 0; s < SUITS.length; ++s) {
for (var r = 0; r < RANKS.length; ++r) {
var c = new Card();
c.setCard(SUITS[s], RANKS[r], false, idx++);
cards.push(c);
}
}
shuffle(cards);
return cards;
}
// Helper: Remove all children from a container
function removeAll(container) {
while (container.children.length > 0) {
var c = container.children[0];
if (c.destroy) c.destroy();else container.removeChild(c);
}
}
// Helper: Animate card to position
function animateCardTo(card, x, y, onFinish) {
tween(card, {
x: x,
y: y
}, {
duration: 250,
easing: tween.cubicOut,
onFinish: onFinish
});
}
// Helper: Show info text
function showInfo(msg, color) {
infoText.setText(msg);
if (typeof infoText.setFill === "function") {
infoText.setFill(color || "#fff");
} else if (infoText.style) {
infoText.style.fill = color || "#fff";
}
infoText.visible = true;
tween(infoText, {
alpha: 1
}, {
duration: 100
});
LK.setTimeout(function () {
tween(infoText, {
alpha: 0
}, {
duration: 600,
onFinish: function onFinish() {
infoText.visible = false;
}
});
}, 1200);
}
// Helper: End game
function endGame(msg, color) {
gameOver = true;
showInfo(msg, color);
LK.setTimeout(function () {
LK.showGameOver();
}, 1200);
}
// Helper: Win game
function winGame(msg, color) {
gameOver = true;
showInfo(msg, color);
LK.setTimeout(function () {
LK.showYouWin();
}, 1200);
}
// Helper: Update timer
function updateTimer() {
timerText.setText(timeLeft + "s");
if (timeLeft <= 0) {
endGame("Time's up!", "#f44");
}
}
// Helper: Lay out player's hand
function layoutPlayerHand() {
for (var i = 0; i < playerHand.length; ++i) {
var c = playerHand[i];
c.x = HAND_X_START + i * HAND_SPACING;
c.y = HAND_Y;
c.zIndex = 10 + i;
c.scaleX = c.scaleY = 1;
c.faceUp = true;
c.updateVisual();
}
}
// Helper: Lay out AI's hand (face down)
function layoutAIHand() {
for (var i = 0; i < aiHand.length; ++i) {
var c = aiHand[i];
c.x = AI_HAND_X_START + i * HAND_SPACING;
c.y = AI_HAND_Y;
c.zIndex = 10 + i;
c.faceUp = false;
c.updateVisual();
}
}
// Helper: Lay out piles
function layoutPiles() {
// Draw pile
if (deck.length > 0) {
var top = deck[deck.length - 1];
top.x = PILE_X - 180;
top.y = DRAW_PILE_Y;
top.zIndex = 100;
top.faceUp = false;
top.updateVisual();
top.visible = true;
}
// Discard pile
if (discardPile.length > 0) {
var top = discardPile[discardPile.length - 1];
top.x = PILE_X + 180;
top.y = DISCARD_PILE_Y;
top.zIndex = 100;
top.faceUp = true;
top.updateVisual();
top.visible = true;
}
}
// Helper: Remove all cards from stage
function removeAllCards() {
for (var i = 0; i < deck.length; ++i) deck[i].visible = false;
for (var i = 0; i < discardPile.length; ++i) discardPile[i].visible = false;
for (var i = 0; i < playerHand.length; ++i) playerHand[i].visible = false;
for (var i = 0; i < aiHand.length; ++i) aiHand[i].visible = false;
}
// Helper: Animate AI draw
game.animateAIDraw = function (card, from) {
card.visible = true;
if (from === 'deck') {
card.x = PILE_X - 180;
card.y = DRAW_PILE_Y;
} else {
card.x = PILE_X + 180;
card.y = DISCARD_PILE_Y;
}
animateCardTo(card, AI_HAND_X_START + (aiHand.length - 1) * HAND_SPACING, AI_HAND_Y, function () {
layoutAIHand();
});
};
// Helper: Animate AI discard
game.animateAIDiscard = function (card) {
animateCardTo(card, PILE_X + 180, DISCARD_PILE_Y, function () {
layoutPiles();
});
};
// Helper: AI knock
game.aiKnock = function () {
aiKnocked = true;
showInfo("AI knocks!", "#ff0");
// Reveal AI hand
for (var i = 0; i < aiHand.length; ++i) {
aiHand[i].faceUp = true;
aiHand[i].updateVisual();
}
// Compare hands after short delay
LK.setTimeout(function () {
var playerVal = bestHandValue(playerHand).value;
var aiVal = bestHandValue(aiHand).value;
if (aiVal > playerVal) {
endGame("AI wins! (" + aiVal + " vs " + playerVal + ")", "#f44");
} else if (playerVal > aiVal) {
winGame("You win! (" + playerVal + " vs " + aiVal + ")", "#4f4");
} else {
endGame("Draw! (" + playerVal + ")", "#fff");
}
}, 1200);
};
// Helper: Player knock
function playerKnock() {
if (gameOver || playerKnocked) return;
playerKnocked = true;
showInfo("You knock!", "#ff0");
// Reveal AI hand
for (var i = 0; i < aiHand.length; ++i) {
aiHand[i].faceUp = true;
aiHand[i].updateVisual();
}
// Compare hands after short delay
LK.setTimeout(function () {
var playerVal = bestHandValue(playerHand).value;
var aiVal = bestHandValue(aiHand).value;
if (playerVal > aiVal) {
winGame("You win! (" + playerVal + " vs " + aiVal + ")", "#4f4");
} else if (aiVal > playerVal) {
endGame("AI wins! (" + aiVal + " vs " + playerVal + ")", "#f44");
} else {
endGame("Draw! (" + playerVal + ")", "#fff");
}
}, 1200);
}
// Helper: Start AI move timer
function startAIMove() {
if (aiMoveTimeout) LK.clearTimeout(aiMoveTimeout);
aiMoveTimeout = LK.setTimeout(function () {
if (!gameOver && ai && !aiKnocked) {
ai.makeMove();
// If AI didn't knock, return turn to player
if (!aiKnocked) {
turn = 'player';
canDraw = true;
canDiscard = false;
showInfo("Your turn!", "#fff");
}
}
}, 900);
}
// Helper: Start timer
function startTimer() {
if (timer) LK.clearInterval(timer);
timer = LK.setInterval(function () {
if (gameOver) return;
timeLeft -= 1;
updateTimer();
}, 1000);
}
// Helper: Reset game state
function resetGame() {
// Remove all cards
removeAllCards();
deck = createDeck();
discardPile = [];
playerHand = [];
aiHand = [];
ai = new AIPlayer();
selectedCardIndex = -1;
draggingCard = null;
canDraw = true;
canDiscard = false;
gameOver = false;
aiMoveTimeout = null;
playerKnocked = false;
aiKnocked = false;
turn = 'player';
timeLeft = 60;
// Deal 3 cards to each
for (var i = 0; i < 3; ++i) {
var c = deck.pop();
c.faceUp = true;
c.visible = true;
playerHand.push(c);
game.addChild(c);
}
for (var i = 0; i < 3; ++i) {
var c = deck.pop();
c.faceUp = false;
c.visible = true;
aiHand.push(c);
game.addChild(c);
}
// Discard one card to start
var c = deck.pop();
c.faceUp = true;
c.visible = true;
discardPile.push(c);
game.addChild(c);
// Add remaining deck cards to stage (face down)
for (var i = 0; i < deck.length; ++i) {
deck[i].faceUp = false;
deck[i].visible = true;
game.addChild(deck[i]);
}
// Add AI hand to stage
for (var i = 0; i < aiHand.length; ++i) {
aiHand[i].faceUp = false;
aiHand[i].visible = true;
game.addChild(aiHand[i]);
}
// Layout
layoutPlayerHand();
layoutAIHand();
layoutPiles();
// Update GUI
updateHandValueText();
updateTimer();
// Start timer
startTimer();
// Show info
showInfo("Your turn!", "#fff");
}
// Helper: Update hand value text
function updateHandValueText() {
var val = bestHandValue(playerHand);
handValueText.setText("Best: " + val.value + " " + val.suit);
if (val.value === 31) {
winGame("31! You win!", "#4f4");
}
}
// GUI: Info text
infoText = new Text2('', {
size: 90,
fill: "#fff"
});
infoText.anchor.set(0.5, 0.5);
infoText.x = 1024;
infoText.y = 900;
infoText.alpha = 0;
infoText.visible = false;
LK.gui.top.addChild(infoText);
// GUI: Timer
timerText = new Text2('60s', {
size: 90,
fill: "#fff"
});
timerText.anchor.set(0.5, 0);
timerText.x = 1024;
timerText.y = 110;
LK.gui.top.addChild(timerText);
// GUI: Hand value
var handValueText = new Text2('', {
size: 80,
fill: "#fff"
});
handValueText.anchor.set(0.5, 0);
handValueText.x = 1024;
handValueText.y = HAND_Y + 200;
LK.gui.top.addChild(handValueText);
// GUI: Knock button
knockButton = new Text2('Knock', {
size: 90,
fill: "#fff",
font: "Impact"
});
knockButton.anchor.set(0.5, 0.5);
knockButton.x = 1800;
knockButton.y = HAND_Y + 100;
knockButton.interactive = true;
knockButton.buttonMode = true;
knockButton.alpha = 0.95;
knockButton.down = function (x, y, obj) {
if (gameOver || !canDraw) return;
playerKnock();
};
LK.gui.bottom.addChild(knockButton);
// GUI: Discard highlight
discardHighlight = LK.getAsset('discardHighlight', {
anchorX: 0.5,
anchorY: 0.5,
x: PILE_X + 180,
y: DISCARD_PILE_Y
});
discardHighlight.alpha = 0;
game.addChild(discardHighlight);
// GUI: Draw highlight
drawHighlight = LK.getAsset('drawHighlight', {
anchorX: 0.5,
anchorY: 0.5,
x: PILE_X - 180,
y: DRAW_PILE_Y
});
drawHighlight.alpha = 0;
game.addChild(drawHighlight);
// --- Input Handling ---
// Touch/click down
game.down = function (x, y, obj) {
if (gameOver) return;
// If it's player's turn
if (turn !== 'player') return;
// Check if drawing from deck
if (canDraw && deck.length > 0) {
var top = deck[deck.length - 1];
var dx = x - (PILE_X - 180),
dy = y - DRAW_PILE_Y;
if (Math.abs(dx) < 110 && Math.abs(dy) < 160) {
// Draw from deck
canDraw = false;
canDiscard = true;
var card = deck.pop();
card.faceUp = true;
card.visible = true;
playerHand.push(card);
animateCardTo(card, HAND_X_START + (playerHand.length - 1) * HAND_SPACING, HAND_Y, function () {
layoutPlayerHand();
updateHandValueText();
});
drawHighlight.alpha = 0.2;
LK.setTimeout(function () {
drawHighlight.alpha = 0;
}, 200);
return;
}
}
// Check if drawing from discard
if (canDraw && discardPile.length > 0) {
var top = discardPile[discardPile.length - 1];
var dx = x - (PILE_X + 180),
dy = y - DISCARD_PILE_Y;
if (Math.abs(dx) < 110 && Math.abs(dy) < 160) {
// Draw from discard
canDraw = false;
canDiscard = true;
var card = discardPile.pop();
card.faceUp = true;
card.visible = true;
playerHand.push(card);
animateCardTo(card, HAND_X_START + (playerHand.length - 1) * HAND_SPACING, HAND_Y, function () {
layoutPlayerHand();
updateHandValueText();
});
discardHighlight.alpha = 0.2;
LK.setTimeout(function () {
discardHighlight.alpha = 0;
}, 200);
return;
}
}
// If can discard, check if a card in hand is tapped
if (canDiscard) {
for (var i = 0; i < playerHand.length; ++i) {
var c = playerHand[i];
var dx = x - c.x,
dy = y - c.y;
if (Math.abs(dx) < 110 && Math.abs(dy) < 160) {
// Discard this card
canDiscard = false;
var discard = playerHand.splice(i, 1)[0];
discard.faceUp = true;
discard.visible = true;
discardPile.push(discard);
animateCardTo(discard, PILE_X + 180, DISCARD_PILE_Y, function () {
layoutPiles();
});
discardHighlight.alpha = 0.2;
LK.setTimeout(function () {
discardHighlight.alpha = 0;
}, 200);
layoutPlayerHand();
updateHandValueText();
// End turn, AI moves
turn = 'ai';
startAIMove();
return;
}
}
}
};
// Touch/click up
game.up = function (x, y, obj) {
// No drag in this version
};
// Touch/click move
game.move = function (x, y, obj) {
// No drag in this version
};
// Main update loop
game.update = function () {
// No per-frame logic needed
};
// Start game
resetGame();