/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Card class: represents a card in hand or on table var Card = Container.expand(function () { var self = Container.call(this); // Card data: {id, name, value, desc} self.cardData = null; self.isFaceUp = true; self.owner = null; // 'player' or 'bot' self.index = 0; // hand index // Card graphics self.cardAsset = null; self.textName = null; self.textValue = null; // Set card data and visuals self.setCard = function (cardData, faceUp) { self.cardData = cardData; self.isFaceUp = faceUp; self.removeChildren(); // Map card id to correct card asset number (1-8) var cardNumber = 0; if (faceUp) { // Find the index in CARD_DECK to get the correct number (1-8) for (var i = 0; i < CARD_DECK.length; ++i) { if (CARD_DECK[i].id === cardData.id) { cardNumber = i + 1; break; } } } var assetId = faceUp ? 'card' + cardNumber : 'cardBack'; self.cardAsset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // No value text needed; image contains all card visuals }; // Flip card face up/down self.flip = function (faceUp) { if (self.cardData) { self.setCard(self.cardData, faceUp); } }; return self; }); // Simple popup for messages var Popup = Container.expand(function () { var self = Container.call(this); self.bg = self.attachAsset('cardBack', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1 }); self.bg.tint = 0x222222; self.bg.alpha = 0.95; self.text = new Text2('', { size: 72, fill: "#fff" }); self.text.anchor.set(0.5, 0.5); self.text.x = 0; self.text.y = 0; self.addChild(self.text); self.setText = function (str) { self.text.setText(str); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x2d2d44 }); /**** * Game Code ****/ // Heart shape for score display // Add brown desk background using a colored box var boardBg = LK.getAsset('deskBg', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2 }); game.addChild(boardBg); // Simple heart for score // Back of card // Princess // Countess // King // Prince // Handmaid // Baron // Priest // Guard // Card assets (simple colored boxes for each card type) // Card definitions (Love Letter 8-card deck) var CARD_DECK = [ // id, name, value, desc, count { id: 'guard', name: 'Guard', value: 1, desc: 'Guess a card', count: 5 }, { id: 'priest', name: 'Priest', value: 2, desc: 'See hand', count: 2 }, { id: 'baron', name: 'Baron', value: 3, desc: 'Compare hands', count: 2 }, { id: 'handmaid', name: 'Handmaid', value: 4, desc: 'Immunity', count: 2 }, { id: 'prince', name: 'Prince', value: 5, desc: 'Discard hand', count: 2 }, { id: 'king', name: 'King', value: 6, desc: 'Trade hands', count: 1 }, { id: 'countess', name: 'Countess', value: 7, desc: 'Must play if with King/Prince', count: 1 }, { id: 'princess', name: 'Princess', value: 8, desc: 'Lose if discarded', count: 1 }]; // Build deck array function buildDeck() { var deck = []; for (var i = 0; i < CARD_DECK.length; ++i) { for (var j = 0; j < CARD_DECK[i].count; ++j) { deck.push({ id: CARD_DECK[i].id, name: CARD_DECK[i].name, value: CARD_DECK[i].value, desc: CARD_DECK[i].desc }); } } return deck; } // Shuffle deck function shuffle(deck) { for (var i = deck.length - 1; i > 0; --i) { var j = Math.floor(Math.random() * (i + 1)); var t = deck[i]; deck[i] = deck[j]; deck[j] = t; } } // Game state var playerHand = []; var botHand = []; var deck = []; var discardPile = []; var playerProtected = false; var botProtected = false; var playerOut = false; var botOut = false; var playerScore = 0; var botScore = 0; var roundOver = false; var currentTurn = 'player'; // 'player' or 'bot' var popup = null; var playerCardNodes = []; var botCardNodes = []; var discardNodes = []; var heartsPlayer = []; var heartsBot = []; var guiText = null; var roundTarget = 3; // First to 3 wins // Card positions var playerHandY = 2732 - 650; // Move player cards up by 300px var botHandY = 650; // Move bot cards down by 300px var handX = 2048 / 2; var handSpacing = 400; // Scoreboard panel and text globals var scoreboardPanel = null; var playerNameText = null; var botNameText = null; // GUI setup function setupGUI() { // Remove all scoreboard and GUI elements if present if (guiText) LK.gui.top.removeChild(guiText); for (var i = 0; i < heartsPlayer.length; ++i) LK.gui.top.removeChild(heartsPlayer[i]); for (var i = 0; i < heartsBot.length; ++i) LK.gui.top.removeChild(heartsBot[i]); if (scoreboardPanel) LK.gui.left.removeChild(scoreboardPanel); if (playerNameText) LK.gui.left.removeChild(playerNameText); if (botNameText) LK.gui.left.removeChild(botNameText); heartsPlayer = []; heartsBot = []; scoreboardPanel = null; playerNameText = null; botNameText = null; guiText = null; // Add scoreboard panel and text to left middle (avoid top left 100x100) scoreboardPanel = LK.getAsset('botMenuBox', { anchorX: 0.5, anchorY: 0.5, x: 300, y: LK.gui.height / 2 }); LK.gui.left.addChild(scoreboardPanel); // Player score playerNameText = new Text2("You: " + playerScore, { size: 64, fill: "#fff" }); playerNameText.anchor.set(0.5, 0.5); playerNameText.x = 300; playerNameText.y = LK.gui.height / 2 - 40; LK.gui.left.addChild(playerNameText); // Bot score botNameText = new Text2("Bot: " + botScore, { size: 64, fill: "#fff" }); botNameText.anchor.set(0.5, 0.5); botNameText.x = 300; botNameText.y = LK.gui.height / 2 + 40; LK.gui.left.addChild(botNameText); } // Start a new round function startRound() { // Reset state deck = buildDeck(); shuffle(deck); discardPile = []; playerHand = []; botHand = []; playerProtected = false; botProtected = false; playerOut = false; botOut = false; roundOver = false; currentTurn = 'player'; // Remove old cards for (var i = 0; i < playerCardNodes.length; ++i) game.removeChild(playerCardNodes[i]); for (var i = 0; i < botCardNodes.length; ++i) game.removeChild(botCardNodes[i]); for (var i = 0; i < discardNodes.length; ++i) game.removeChild(discardNodes[i]); playerCardNodes = []; botCardNodes = []; discardNodes = []; // Remove popup if (popup) { game.removeChild(popup); popup = null; } // Remove one card face down (not used in 2p, but for deduction) deck.pop(); // Deal 1 card to each playerHand.push(deck.pop()); botHand.push(deck.pop()); // Draw 1 more for player to start playerDraw(); // Show hands renderHands(); // Show message showPopup("Your turn!", 1200, function () { // Player's turn currentTurn = 'player'; enablePlayerPlay(); }); } // Draw a card for player function playerDraw() { if (deck.length > 0) { playerHand.push(deck.pop()); } } // Draw a card for bot function botDraw() { if (deck.length > 0) { botHand.push(deck.pop()); } } // Render hands function renderHands() { // Remove old for (var i = 0; i < playerCardNodes.length; ++i) game.removeChild(playerCardNodes[i]); for (var i = 0; i < botCardNodes.length; ++i) game.removeChild(botCardNodes[i]); playerCardNodes = []; botCardNodes = []; // Player hand (always face up) var playerCount = playerHand.length; var playerCardWidth = 480 * 1.6; var playerCardSpacing = playerCardWidth + 40; // 40px gap between cards var playerHandTotalWidth = playerCount > 1 ? playerCount * playerCardWidth + (playerCount - 1) * 40 : playerCardWidth; for (var i = 0; i < playerCount; ++i) { var c = new Card(); c.setCard(playerHand[i], true); c.owner = 'player'; c.index = i; // Spread cards side by side, centered horizontally c.x = handX - playerHandTotalWidth / 2 + playerCardWidth / 2 + i * playerCardSpacing; c.y = playerHandY; c.scaleX = c.scaleY = 1.44; game.addChild(c); playerCardNodes.push(c); } // Bot hand (always 1 card, face down) var botCount = botHand.length; var botCardWidth = 480 * 1.6; var botCardSpacing = botCardWidth + 40; var botHandTotalWidth = botCount > 1 ? botCount * botCardWidth + (botCount - 1) * 40 : botCardWidth; for (var i = 0; i < botCount; ++i) { var c = new Card(); c.setCard(botHand[i], false); c.owner = 'bot'; c.index = i; // Spread cards side by side, centered horizontally c.x = handX - botHandTotalWidth / 2 + botCardWidth / 2 + i * botCardSpacing; c.y = botHandY; c.scaleX = c.scaleY = 1.44; game.addChild(c); botCardNodes.push(c); } // Discard pile for (var i = 0; i < discardNodes.length; ++i) game.removeChild(discardNodes[i]); discardNodes = []; for (var i = 0; i < discardPile.length; ++i) { var c = new Card(); c.setCard(discardPile[i], true); c.x = 2048 - 200; c.y = 2732 / 2 - 200 + (i - 2) * 40; // Move discard pile up by 200px c.scaleX = c.scaleY = 0.7; game.addChild(c); discardNodes.push(c); } } // Show popup message function showPopup(msg, duration, cb) { if (popup) { game.removeChild(popup); popup = null; } popup = new Popup(); popup.setText(msg); popup.x = 2048 / 2; popup.y = 2732 / 2; game.addChild(popup); if (duration) { LK.setTimeout(function () { if (popup) { game.removeChild(popup); popup = null; } if (cb) cb(); }, duration); } } // Enable player to play a card function enablePlayerPlay() { // Only allow if not out if (playerOut || roundOver) return; // Add .down event to player's cards for (var i = 0; i < playerCardNodes.length; ++i) { (function (idx) { playerCardNodes[idx].down = function (x, y, obj) { // Play this card playerPlayCard(idx); }; })(i); } } // Disable player input function disablePlayerPlay() { for (var i = 0; i < playerCardNodes.length; ++i) { playerCardNodes[i].down = undefined; } } // Player plays a card function playerPlayCard(idx) { if (playerOut || roundOver) return; disablePlayerPlay(); // Play card at idx var card = playerHand[idx]; var otherIdx = idx === 0 ? 1 : 0; var keepCard = playerHand[otherIdx]; // Countess rule: must play Countess if holding King or Prince if (card.id !== 'countess' && keepCard && keepCard.id === 'countess' && (card.id === 'king' || card.id === 'prince')) { showPopup("You must play Countess!", 1200, function () { enablePlayerPlay(); }); return; } // Remove card from hand var played = playerHand.splice(idx, 1)[0]; discardPile.push(played); // Animate card to discard renderHands(); // Card effect resolveCardEffect('player', played, function () { // If not out, keep card in hand if (!playerOut && keepCard) { playerHand = [keepCard]; } renderHands(); // Next: bot's turn if (!roundOver) { LK.setTimeout(function () { botTurn(); }, 900); } }); } // Bot's turn function botTurn() { if (botOut || roundOver) return; // Draw botDraw(); renderHands(); // Choose card to play var idx = botChooseCard(); var card = botHand[idx]; var otherIdx = idx === 0 ? 1 : 0; var keepCard = botHand[otherIdx]; // Countess rule if (card.id !== 'countess' && keepCard && keepCard.id === 'countess' && (card.id === 'king' || card.id === 'prince')) { idx = otherIdx; card = botHand[idx]; otherIdx = idx === 0 ? 1 : 0; keepCard = botHand[otherIdx]; } // Remove card from hand var played = botHand.splice(idx, 1)[0]; discardPile.push(played); // Animate card to discard renderHands(); // Card effect resolveCardEffect('bot', played, function () { // If not out, keep card in hand if (!botOut && keepCard) { botHand = [keepCard]; } renderHands(); // Next: player's turn if (!roundOver) { LK.setTimeout(function () { playerDraw(); renderHands(); enablePlayerPlay(); }, 900); } }); } // Bot chooses which card to play (simple AI) function botChooseCard() { // If must play Countess if (botHand.length == 2) { var c0 = botHand[0], c1 = botHand[1]; if (c0.id === 'countess' && (c1.id === 'king' || c1.id === 'prince')) return 0; if (c1.id === 'countess' && (c0.id === 'king' || c0.id === 'prince')) return 1; } // Prefer not to play Princess for (var i = 0; i < botHand.length; ++i) { if (botHand[i].id !== 'princess') return i; } // Otherwise, play first return 0; } // Card effect resolution function resolveCardEffect(who, card, cb) { // who: 'player' or 'bot' // card: card object // cb: callback after effect // Helper: get opponent function getOpponent() { return who === 'player' ? 'bot' : 'player'; } function getHand(who) { return who === 'player' ? playerHand : botHand; } function setProtected(who, val) { if (who === 'player') playerProtected = val;else botProtected = val; } function isProtected(who) { return who === 'player' ? playerProtected : botProtected; } function setOut(who) { if (who === 'player') playerOut = true;else botOut = true; } // Card effects if (card.id === 'guard') { // Guess a card (not Guard) if (who === 'player') { // Show options to guess showGuardGuess(function (guessId) { if (botProtected) { showPopup("Bot is protected!", 1000, cb); } else if (botHand[0].id === guessId) { showPopup("Correct! Bot had " + botHand[0].name + ".", 1200, function () { setOut('bot'); endRound(); cb(); }); return; } else { showPopup("Wrong guess.", 1000, cb); } }); return; } else { // Bot guesses randomly (not Guard) if (playerProtected) { showPopup("You are protected!", 1000, cb); } else { // Guess random card (not Guard) var guessable = ['priest', 'baron', 'handmaid', 'prince', 'king', 'countess', 'princess']; var guessId = guessable[Math.floor(Math.random() * guessable.length)]; if (playerHand[0].id === guessId) { showPopup("Bot guessed " + CARD_DECK.filter(function (c) { return c.id === guessId; })[0].name + " and was right!", 1200, function () { setOut('player'); endRound(); cb(); }); return; } else { showPopup("Bot guessed " + CARD_DECK.filter(function (c) { return c.id === guessId; })[0].name + " and was wrong.", 1000, cb); } } } } else if (card.id === 'priest') { // See opponent's hand if (who === 'player') { if (botProtected) { showPopup("Bot is protected!", 1000, cb); } else { showPopup("Bot has " + botHand[0].name + ".", 1500, cb); } } else { if (playerProtected) { showPopup("You are protected!", 1000, cb); } else { showPopup("Bot looks at your hand.", 1000, cb); } } } else if (card.id === 'baron') { // Compare hands, lower is out if (who === 'player') { if (botProtected) { showPopup("Bot is protected!", 1000, cb); } else { var p = playerHand[0].value, b = botHand[0].value; if (p > b) { showPopup("You win! (" + playerHand[0].name + " > " + botHand[0].name + ")", 1200, function () { setOut('bot'); endRound(); cb(); }); return; } else if (b > p) { showPopup("You lose! (" + playerHand[0].name + " < " + botHand[0].name + ")", 1200, function () { setOut('player'); endRound(); cb(); }); return; } else { showPopup("Tie! (" + playerHand[0].name + " = " + botHand[0].name + ")", 1000, cb); } } } else { if (playerProtected) { showPopup("You are protected!", 1000, cb); } else { var p = playerHand[0].value, b = botHand[0].value; if (b > p) { showPopup("Bot wins! (" + botHand[0].name + " > " + playerHand[0].name + ")", 1200, function () { setOut('player'); endRound(); cb(); }); return; } else if (p > b) { showPopup("Bot loses! (" + botHand[0].name + " < " + playerHand[0].name + ")", 1200, function () { setOut('bot'); endRound(); cb(); }); return; } else { showPopup("Tie! (" + playerHand[0].name + " = " + botHand[0].name + ")", 1000, cb); } } } } else if (card.id === 'handmaid') { // Immunity until next turn setProtected(who, true); showPopup((who === 'player' ? "You" : "Bot") + " are protected until next turn.", 1000, cb); } else if (card.id === 'prince') { // Choose a player to discard hand if (who === 'player') { // If bot is protected, must target self if (botProtected && !playerProtected) { showPopup("Bot is protected. You discard your hand.", 1000, function () { princeDiscard('player', cb); }); } else if (playerProtected && !botProtected) { showPopup("You are protected. Bot discards hand.", 1000, function () { princeDiscard('bot', cb); }); } else if (playerProtected && botProtected) { showPopup("Both protected. Nothing happens.", 1000, cb); } else { // Choose target showPrinceTarget(function (target) { princeDiscard(target, cb); }); return; } } else { // Bot: prefer to target player if not protected if (!playerProtected) { showPopup("Bot makes you discard your hand.", 1000, function () { princeDiscard('player', cb); }); } else if (!botProtected) { showPopup("Bot discards its own hand.", 1000, function () { princeDiscard('bot', cb); }); } else { showPopup("Both protected. Nothing happens.", 1000, cb); } } } else if (card.id === 'king') { // Trade hands if (who === 'player') { if (botProtected) { showPopup("Bot is protected!", 1000, cb); } else { var tmp = playerHand[0]; playerHand[0] = botHand[0]; botHand[0] = tmp; showPopup("You swapped hands!", 1000, cb); } } else { if (playerProtected) { showPopup("You are protected!", 1000, cb); } else { var tmp = playerHand[0]; playerHand[0] = botHand[0]; botHand[0] = tmp; showPopup("Bot swapped hands!", 1000, cb); } } } else if (card.id === 'countess') { // No effect showPopup((who === 'player' ? "You" : "Bot") + " played Countess.", 1000, cb); } else if (card.id === 'princess') { // If discarded, out showPopup((who === 'player' ? "You" : "Bot") + " discarded the Princess and is out!", 1200, function () { setOut(who); endRound(); cb(); }); return; } else { showPopup("No effect.", 1000, cb); } } // Guard guess UI function showGuardGuess(cb) { // Show options for player to guess (not Guard) var opts = ['priest', 'baron', 'handmaid', 'prince', 'king', 'countess', 'princess']; var buttons = []; var y0 = 2732 / 2 - 120; for (var i = 0; i < opts.length; ++i) { (function (idx) { var c = new Card(); var cardData = CARD_DECK.filter(function (cd) { return cd.id === opts[idx]; })[0]; c.setCard(cardData, true); c.x = 2048 / 2 - (opts.length / 2 - idx) * 180; c.y = y0; c.scaleX = c.scaleY = 0.7; c.down = function (x, y, obj) { // Remove all for (var j = 0; j < buttons.length; ++j) game.removeChild(buttons[j]); cb(opts[idx]); }; game.addChild(c); buttons.push(c); })(i); } } // Prince target UI function showPrinceTarget(cb) { var buttons = []; var y0 = 2732 / 2 - 120; // Player var c1 = new Card(); c1.setCard(playerHand[0], true); c1.x = 2048 / 2 - 120; c1.y = y0; c1.scaleX = c1.scaleY = 0.8; c1.down = function (x, y, obj) { for (var j = 0; j < buttons.length; ++j) game.removeChild(buttons[j]); cb('player'); }; game.addChild(c1); buttons.push(c1); // Bot (face down) var c2 = new Card(); c2.setCard(botHand[0], false); c2.x = 2048 / 2 + 120; c2.y = y0; c2.scaleX = c2.scaleY = 0.8; c2.down = function (x, y, obj) { for (var j = 0; j < buttons.length; ++j) game.removeChild(buttons[j]); cb('bot'); }; game.addChild(c2); buttons.push(c2); } // Prince discard effect function princeDiscard(who, cb) { if (who === 'player') { var card = playerHand[0]; discardPile.push(card); if (card.id === 'princess') { showPopup("You discarded the Princess and are out!", 1200, function () { playerOut = true; endRound(); cb(); }); return; } else { // Draw new card if (deck.length > 0) { playerHand[0] = deck.pop(); showPopup("You drew a new card.", 1000, cb); } else { playerHand = []; showPopup("No cards left to draw.", 1000, cb); } } } else { var card = botHand[0]; discardPile.push(card); if (card.id === 'princess') { showPopup("Bot discarded the Princess and is out!", 1200, function () { botOut = true; endRound(); cb(); }); return; } else { // Draw new card if (deck.length > 0) { botHand[0] = deck.pop(); showPopup("Bot drew a new card.", 1000, cb); } else { botHand = []; showPopup("No cards left to draw.", 1000, cb); } } } } // End round: check for winner function endRound() { roundOver = true; disablePlayerPlay(); // Reveal bot's hand for (var i = 0; i < botCardNodes.length; ++i) { botCardNodes[i].flip(true); } // Who wins? var winner = null; if (playerOut && botOut) { winner = null; } else if (playerOut) { winner = 'bot'; } else if (botOut) { winner = 'player'; } else if (deck.length === 0) { // Compare hands if (playerHand[0].value > botHand[0].value) winner = 'player';else if (botHand[0].value > playerHand[0].value) winner = 'bot';else winner = null; } // Update score if (winner === 'player') { playerScore += 1; if (playerNameText) playerNameText.setText("You: " + playerScore); if (botNameText) botNameText.setText("Bot: " + botScore); showPopup("You win the round!", 1800, function () { checkGameEnd(); }); } else if (winner === 'bot') { botScore += 1; if (playerNameText) playerNameText.setText("You: " + playerScore); if (botNameText) botNameText.setText("Bot: " + botScore); showPopup("Bot wins the round!", 1800, function () { checkGameEnd(); }); } else { if (playerNameText) playerNameText.setText("You: " + playerScore); if (botNameText) botNameText.setText("Bot: " + botScore); showPopup("Round is a tie!", 1500, function () { checkGameEnd(); }); } } // Check for game end function checkGameEnd() { if (playerScore >= roundTarget) { LK.setScore(playerScore); LK.showYouWin(); } else if (botScore >= roundTarget) { LK.setScore(playerScore); LK.showGameOver(); } else { // Start next round startRound(); } } // On every update, remove protection at start of player's turn only game.update = function () { // Remove protection at start of player's turn only if (currentTurn === 'player' && playerProtected) playerProtected = false; if (currentTurn === 'bot' && botProtected) botProtected = false; }; // Message box for in-game messages var messageBox = null; function showMessageBox(msg) { // Remove previous message box if present if (messageBox) { game.removeChild(messageBox); messageBox = null; } messageBox = new Popup(); messageBox.setText(msg); messageBox.x = 2048 / 2; messageBox.y = 2732 - 350; // Near bottom center, above player cards messageBox.scaleX = 0.9; messageBox.scaleY = 0.7; game.addChild(messageBox); } function hideMessageBox() { if (messageBox) { game.removeChild(messageBox); messageBox = null; } } // Setup GUI and start game setupGUI(); startRound();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Card class: represents a card in hand or on table
var Card = Container.expand(function () {
var self = Container.call(this);
// Card data: {id, name, value, desc}
self.cardData = null;
self.isFaceUp = true;
self.owner = null; // 'player' or 'bot'
self.index = 0; // hand index
// Card graphics
self.cardAsset = null;
self.textName = null;
self.textValue = null;
// Set card data and visuals
self.setCard = function (cardData, faceUp) {
self.cardData = cardData;
self.isFaceUp = faceUp;
self.removeChildren();
// Map card id to correct card asset number (1-8)
var cardNumber = 0;
if (faceUp) {
// Find the index in CARD_DECK to get the correct number (1-8)
for (var i = 0; i < CARD_DECK.length; ++i) {
if (CARD_DECK[i].id === cardData.id) {
cardNumber = i + 1;
break;
}
}
}
var assetId = faceUp ? 'card' + cardNumber : 'cardBack';
self.cardAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// No value text needed; image contains all card visuals
};
// Flip card face up/down
self.flip = function (faceUp) {
if (self.cardData) {
self.setCard(self.cardData, faceUp);
}
};
return self;
});
// Simple popup for messages
var Popup = Container.expand(function () {
var self = Container.call(this);
self.bg = self.attachAsset('cardBack', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1
});
self.bg.tint = 0x222222;
self.bg.alpha = 0.95;
self.text = new Text2('', {
size: 72,
fill: "#fff"
});
self.text.anchor.set(0.5, 0.5);
self.text.x = 0;
self.text.y = 0;
self.addChild(self.text);
self.setText = function (str) {
self.text.setText(str);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2d2d44
});
/****
* Game Code
****/
// Heart shape for score display
// Add brown desk background using a colored box
var boardBg = LK.getAsset('deskBg', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2
});
game.addChild(boardBg);
// Simple heart for score
// Back of card
// Princess
// Countess
// King
// Prince
// Handmaid
// Baron
// Priest
// Guard
// Card assets (simple colored boxes for each card type)
// Card definitions (Love Letter 8-card deck)
var CARD_DECK = [
// id, name, value, desc, count
{
id: 'guard',
name: 'Guard',
value: 1,
desc: 'Guess a card',
count: 5
}, {
id: 'priest',
name: 'Priest',
value: 2,
desc: 'See hand',
count: 2
}, {
id: 'baron',
name: 'Baron',
value: 3,
desc: 'Compare hands',
count: 2
}, {
id: 'handmaid',
name: 'Handmaid',
value: 4,
desc: 'Immunity',
count: 2
}, {
id: 'prince',
name: 'Prince',
value: 5,
desc: 'Discard hand',
count: 2
}, {
id: 'king',
name: 'King',
value: 6,
desc: 'Trade hands',
count: 1
}, {
id: 'countess',
name: 'Countess',
value: 7,
desc: 'Must play if with King/Prince',
count: 1
}, {
id: 'princess',
name: 'Princess',
value: 8,
desc: 'Lose if discarded',
count: 1
}];
// Build deck array
function buildDeck() {
var deck = [];
for (var i = 0; i < CARD_DECK.length; ++i) {
for (var j = 0; j < CARD_DECK[i].count; ++j) {
deck.push({
id: CARD_DECK[i].id,
name: CARD_DECK[i].name,
value: CARD_DECK[i].value,
desc: CARD_DECK[i].desc
});
}
}
return deck;
}
// Shuffle deck
function shuffle(deck) {
for (var i = deck.length - 1; i > 0; --i) {
var j = Math.floor(Math.random() * (i + 1));
var t = deck[i];
deck[i] = deck[j];
deck[j] = t;
}
}
// Game state
var playerHand = [];
var botHand = [];
var deck = [];
var discardPile = [];
var playerProtected = false;
var botProtected = false;
var playerOut = false;
var botOut = false;
var playerScore = 0;
var botScore = 0;
var roundOver = false;
var currentTurn = 'player'; // 'player' or 'bot'
var popup = null;
var playerCardNodes = [];
var botCardNodes = [];
var discardNodes = [];
var heartsPlayer = [];
var heartsBot = [];
var guiText = null;
var roundTarget = 3; // First to 3 wins
// Card positions
var playerHandY = 2732 - 650; // Move player cards up by 300px
var botHandY = 650; // Move bot cards down by 300px
var handX = 2048 / 2;
var handSpacing = 400;
// Scoreboard panel and text globals
var scoreboardPanel = null;
var playerNameText = null;
var botNameText = null;
// GUI setup
function setupGUI() {
// Remove all scoreboard and GUI elements if present
if (guiText) LK.gui.top.removeChild(guiText);
for (var i = 0; i < heartsPlayer.length; ++i) LK.gui.top.removeChild(heartsPlayer[i]);
for (var i = 0; i < heartsBot.length; ++i) LK.gui.top.removeChild(heartsBot[i]);
if (scoreboardPanel) LK.gui.left.removeChild(scoreboardPanel);
if (playerNameText) LK.gui.left.removeChild(playerNameText);
if (botNameText) LK.gui.left.removeChild(botNameText);
heartsPlayer = [];
heartsBot = [];
scoreboardPanel = null;
playerNameText = null;
botNameText = null;
guiText = null;
// Add scoreboard panel and text to left middle (avoid top left 100x100)
scoreboardPanel = LK.getAsset('botMenuBox', {
anchorX: 0.5,
anchorY: 0.5,
x: 300,
y: LK.gui.height / 2
});
LK.gui.left.addChild(scoreboardPanel);
// Player score
playerNameText = new Text2("You: " + playerScore, {
size: 64,
fill: "#fff"
});
playerNameText.anchor.set(0.5, 0.5);
playerNameText.x = 300;
playerNameText.y = LK.gui.height / 2 - 40;
LK.gui.left.addChild(playerNameText);
// Bot score
botNameText = new Text2("Bot: " + botScore, {
size: 64,
fill: "#fff"
});
botNameText.anchor.set(0.5, 0.5);
botNameText.x = 300;
botNameText.y = LK.gui.height / 2 + 40;
LK.gui.left.addChild(botNameText);
}
// Start a new round
function startRound() {
// Reset state
deck = buildDeck();
shuffle(deck);
discardPile = [];
playerHand = [];
botHand = [];
playerProtected = false;
botProtected = false;
playerOut = false;
botOut = false;
roundOver = false;
currentTurn = 'player';
// Remove old cards
for (var i = 0; i < playerCardNodes.length; ++i) game.removeChild(playerCardNodes[i]);
for (var i = 0; i < botCardNodes.length; ++i) game.removeChild(botCardNodes[i]);
for (var i = 0; i < discardNodes.length; ++i) game.removeChild(discardNodes[i]);
playerCardNodes = [];
botCardNodes = [];
discardNodes = [];
// Remove popup
if (popup) {
game.removeChild(popup);
popup = null;
}
// Remove one card face down (not used in 2p, but for deduction)
deck.pop();
// Deal 1 card to each
playerHand.push(deck.pop());
botHand.push(deck.pop());
// Draw 1 more for player to start
playerDraw();
// Show hands
renderHands();
// Show message
showPopup("Your turn!", 1200, function () {
// Player's turn
currentTurn = 'player';
enablePlayerPlay();
});
}
// Draw a card for player
function playerDraw() {
if (deck.length > 0) {
playerHand.push(deck.pop());
}
}
// Draw a card for bot
function botDraw() {
if (deck.length > 0) {
botHand.push(deck.pop());
}
}
// Render hands
function renderHands() {
// Remove old
for (var i = 0; i < playerCardNodes.length; ++i) game.removeChild(playerCardNodes[i]);
for (var i = 0; i < botCardNodes.length; ++i) game.removeChild(botCardNodes[i]);
playerCardNodes = [];
botCardNodes = [];
// Player hand (always face up)
var playerCount = playerHand.length;
var playerCardWidth = 480 * 1.6;
var playerCardSpacing = playerCardWidth + 40; // 40px gap between cards
var playerHandTotalWidth = playerCount > 1 ? playerCount * playerCardWidth + (playerCount - 1) * 40 : playerCardWidth;
for (var i = 0; i < playerCount; ++i) {
var c = new Card();
c.setCard(playerHand[i], true);
c.owner = 'player';
c.index = i;
// Spread cards side by side, centered horizontally
c.x = handX - playerHandTotalWidth / 2 + playerCardWidth / 2 + i * playerCardSpacing;
c.y = playerHandY;
c.scaleX = c.scaleY = 1.44;
game.addChild(c);
playerCardNodes.push(c);
}
// Bot hand (always 1 card, face down)
var botCount = botHand.length;
var botCardWidth = 480 * 1.6;
var botCardSpacing = botCardWidth + 40;
var botHandTotalWidth = botCount > 1 ? botCount * botCardWidth + (botCount - 1) * 40 : botCardWidth;
for (var i = 0; i < botCount; ++i) {
var c = new Card();
c.setCard(botHand[i], false);
c.owner = 'bot';
c.index = i;
// Spread cards side by side, centered horizontally
c.x = handX - botHandTotalWidth / 2 + botCardWidth / 2 + i * botCardSpacing;
c.y = botHandY;
c.scaleX = c.scaleY = 1.44;
game.addChild(c);
botCardNodes.push(c);
}
// Discard pile
for (var i = 0; i < discardNodes.length; ++i) game.removeChild(discardNodes[i]);
discardNodes = [];
for (var i = 0; i < discardPile.length; ++i) {
var c = new Card();
c.setCard(discardPile[i], true);
c.x = 2048 - 200;
c.y = 2732 / 2 - 200 + (i - 2) * 40; // Move discard pile up by 200px
c.scaleX = c.scaleY = 0.7;
game.addChild(c);
discardNodes.push(c);
}
}
// Show popup message
function showPopup(msg, duration, cb) {
if (popup) {
game.removeChild(popup);
popup = null;
}
popup = new Popup();
popup.setText(msg);
popup.x = 2048 / 2;
popup.y = 2732 / 2;
game.addChild(popup);
if (duration) {
LK.setTimeout(function () {
if (popup) {
game.removeChild(popup);
popup = null;
}
if (cb) cb();
}, duration);
}
}
// Enable player to play a card
function enablePlayerPlay() {
// Only allow if not out
if (playerOut || roundOver) return;
// Add .down event to player's cards
for (var i = 0; i < playerCardNodes.length; ++i) {
(function (idx) {
playerCardNodes[idx].down = function (x, y, obj) {
// Play this card
playerPlayCard(idx);
};
})(i);
}
}
// Disable player input
function disablePlayerPlay() {
for (var i = 0; i < playerCardNodes.length; ++i) {
playerCardNodes[i].down = undefined;
}
}
// Player plays a card
function playerPlayCard(idx) {
if (playerOut || roundOver) return;
disablePlayerPlay();
// Play card at idx
var card = playerHand[idx];
var otherIdx = idx === 0 ? 1 : 0;
var keepCard = playerHand[otherIdx];
// Countess rule: must play Countess if holding King or Prince
if (card.id !== 'countess' && keepCard && keepCard.id === 'countess' && (card.id === 'king' || card.id === 'prince')) {
showPopup("You must play Countess!", 1200, function () {
enablePlayerPlay();
});
return;
}
// Remove card from hand
var played = playerHand.splice(idx, 1)[0];
discardPile.push(played);
// Animate card to discard
renderHands();
// Card effect
resolveCardEffect('player', played, function () {
// If not out, keep card in hand
if (!playerOut && keepCard) {
playerHand = [keepCard];
}
renderHands();
// Next: bot's turn
if (!roundOver) {
LK.setTimeout(function () {
botTurn();
}, 900);
}
});
}
// Bot's turn
function botTurn() {
if (botOut || roundOver) return;
// Draw
botDraw();
renderHands();
// Choose card to play
var idx = botChooseCard();
var card = botHand[idx];
var otherIdx = idx === 0 ? 1 : 0;
var keepCard = botHand[otherIdx];
// Countess rule
if (card.id !== 'countess' && keepCard && keepCard.id === 'countess' && (card.id === 'king' || card.id === 'prince')) {
idx = otherIdx;
card = botHand[idx];
otherIdx = idx === 0 ? 1 : 0;
keepCard = botHand[otherIdx];
}
// Remove card from hand
var played = botHand.splice(idx, 1)[0];
discardPile.push(played);
// Animate card to discard
renderHands();
// Card effect
resolveCardEffect('bot', played, function () {
// If not out, keep card in hand
if (!botOut && keepCard) {
botHand = [keepCard];
}
renderHands();
// Next: player's turn
if (!roundOver) {
LK.setTimeout(function () {
playerDraw();
renderHands();
enablePlayerPlay();
}, 900);
}
});
}
// Bot chooses which card to play (simple AI)
function botChooseCard() {
// If must play Countess
if (botHand.length == 2) {
var c0 = botHand[0],
c1 = botHand[1];
if (c0.id === 'countess' && (c1.id === 'king' || c1.id === 'prince')) return 0;
if (c1.id === 'countess' && (c0.id === 'king' || c0.id === 'prince')) return 1;
}
// Prefer not to play Princess
for (var i = 0; i < botHand.length; ++i) {
if (botHand[i].id !== 'princess') return i;
}
// Otherwise, play first
return 0;
}
// Card effect resolution
function resolveCardEffect(who, card, cb) {
// who: 'player' or 'bot'
// card: card object
// cb: callback after effect
// Helper: get opponent
function getOpponent() {
return who === 'player' ? 'bot' : 'player';
}
function getHand(who) {
return who === 'player' ? playerHand : botHand;
}
function setProtected(who, val) {
if (who === 'player') playerProtected = val;else botProtected = val;
}
function isProtected(who) {
return who === 'player' ? playerProtected : botProtected;
}
function setOut(who) {
if (who === 'player') playerOut = true;else botOut = true;
}
// Card effects
if (card.id === 'guard') {
// Guess a card (not Guard)
if (who === 'player') {
// Show options to guess
showGuardGuess(function (guessId) {
if (botProtected) {
showPopup("Bot is protected!", 1000, cb);
} else if (botHand[0].id === guessId) {
showPopup("Correct! Bot had " + botHand[0].name + ".", 1200, function () {
setOut('bot');
endRound();
cb();
});
return;
} else {
showPopup("Wrong guess.", 1000, cb);
}
});
return;
} else {
// Bot guesses randomly (not Guard)
if (playerProtected) {
showPopup("You are protected!", 1000, cb);
} else {
// Guess random card (not Guard)
var guessable = ['priest', 'baron', 'handmaid', 'prince', 'king', 'countess', 'princess'];
var guessId = guessable[Math.floor(Math.random() * guessable.length)];
if (playerHand[0].id === guessId) {
showPopup("Bot guessed " + CARD_DECK.filter(function (c) {
return c.id === guessId;
})[0].name + " and was right!", 1200, function () {
setOut('player');
endRound();
cb();
});
return;
} else {
showPopup("Bot guessed " + CARD_DECK.filter(function (c) {
return c.id === guessId;
})[0].name + " and was wrong.", 1000, cb);
}
}
}
} else if (card.id === 'priest') {
// See opponent's hand
if (who === 'player') {
if (botProtected) {
showPopup("Bot is protected!", 1000, cb);
} else {
showPopup("Bot has " + botHand[0].name + ".", 1500, cb);
}
} else {
if (playerProtected) {
showPopup("You are protected!", 1000, cb);
} else {
showPopup("Bot looks at your hand.", 1000, cb);
}
}
} else if (card.id === 'baron') {
// Compare hands, lower is out
if (who === 'player') {
if (botProtected) {
showPopup("Bot is protected!", 1000, cb);
} else {
var p = playerHand[0].value,
b = botHand[0].value;
if (p > b) {
showPopup("You win! (" + playerHand[0].name + " > " + botHand[0].name + ")", 1200, function () {
setOut('bot');
endRound();
cb();
});
return;
} else if (b > p) {
showPopup("You lose! (" + playerHand[0].name + " < " + botHand[0].name + ")", 1200, function () {
setOut('player');
endRound();
cb();
});
return;
} else {
showPopup("Tie! (" + playerHand[0].name + " = " + botHand[0].name + ")", 1000, cb);
}
}
} else {
if (playerProtected) {
showPopup("You are protected!", 1000, cb);
} else {
var p = playerHand[0].value,
b = botHand[0].value;
if (b > p) {
showPopup("Bot wins! (" + botHand[0].name + " > " + playerHand[0].name + ")", 1200, function () {
setOut('player');
endRound();
cb();
});
return;
} else if (p > b) {
showPopup("Bot loses! (" + botHand[0].name + " < " + playerHand[0].name + ")", 1200, function () {
setOut('bot');
endRound();
cb();
});
return;
} else {
showPopup("Tie! (" + playerHand[0].name + " = " + botHand[0].name + ")", 1000, cb);
}
}
}
} else if (card.id === 'handmaid') {
// Immunity until next turn
setProtected(who, true);
showPopup((who === 'player' ? "You" : "Bot") + " are protected until next turn.", 1000, cb);
} else if (card.id === 'prince') {
// Choose a player to discard hand
if (who === 'player') {
// If bot is protected, must target self
if (botProtected && !playerProtected) {
showPopup("Bot is protected. You discard your hand.", 1000, function () {
princeDiscard('player', cb);
});
} else if (playerProtected && !botProtected) {
showPopup("You are protected. Bot discards hand.", 1000, function () {
princeDiscard('bot', cb);
});
} else if (playerProtected && botProtected) {
showPopup("Both protected. Nothing happens.", 1000, cb);
} else {
// Choose target
showPrinceTarget(function (target) {
princeDiscard(target, cb);
});
return;
}
} else {
// Bot: prefer to target player if not protected
if (!playerProtected) {
showPopup("Bot makes you discard your hand.", 1000, function () {
princeDiscard('player', cb);
});
} else if (!botProtected) {
showPopup("Bot discards its own hand.", 1000, function () {
princeDiscard('bot', cb);
});
} else {
showPopup("Both protected. Nothing happens.", 1000, cb);
}
}
} else if (card.id === 'king') {
// Trade hands
if (who === 'player') {
if (botProtected) {
showPopup("Bot is protected!", 1000, cb);
} else {
var tmp = playerHand[0];
playerHand[0] = botHand[0];
botHand[0] = tmp;
showPopup("You swapped hands!", 1000, cb);
}
} else {
if (playerProtected) {
showPopup("You are protected!", 1000, cb);
} else {
var tmp = playerHand[0];
playerHand[0] = botHand[0];
botHand[0] = tmp;
showPopup("Bot swapped hands!", 1000, cb);
}
}
} else if (card.id === 'countess') {
// No effect
showPopup((who === 'player' ? "You" : "Bot") + " played Countess.", 1000, cb);
} else if (card.id === 'princess') {
// If discarded, out
showPopup((who === 'player' ? "You" : "Bot") + " discarded the Princess and is out!", 1200, function () {
setOut(who);
endRound();
cb();
});
return;
} else {
showPopup("No effect.", 1000, cb);
}
}
// Guard guess UI
function showGuardGuess(cb) {
// Show options for player to guess (not Guard)
var opts = ['priest', 'baron', 'handmaid', 'prince', 'king', 'countess', 'princess'];
var buttons = [];
var y0 = 2732 / 2 - 120;
for (var i = 0; i < opts.length; ++i) {
(function (idx) {
var c = new Card();
var cardData = CARD_DECK.filter(function (cd) {
return cd.id === opts[idx];
})[0];
c.setCard(cardData, true);
c.x = 2048 / 2 - (opts.length / 2 - idx) * 180;
c.y = y0;
c.scaleX = c.scaleY = 0.7;
c.down = function (x, y, obj) {
// Remove all
for (var j = 0; j < buttons.length; ++j) game.removeChild(buttons[j]);
cb(opts[idx]);
};
game.addChild(c);
buttons.push(c);
})(i);
}
}
// Prince target UI
function showPrinceTarget(cb) {
var buttons = [];
var y0 = 2732 / 2 - 120;
// Player
var c1 = new Card();
c1.setCard(playerHand[0], true);
c1.x = 2048 / 2 - 120;
c1.y = y0;
c1.scaleX = c1.scaleY = 0.8;
c1.down = function (x, y, obj) {
for (var j = 0; j < buttons.length; ++j) game.removeChild(buttons[j]);
cb('player');
};
game.addChild(c1);
buttons.push(c1);
// Bot (face down)
var c2 = new Card();
c2.setCard(botHand[0], false);
c2.x = 2048 / 2 + 120;
c2.y = y0;
c2.scaleX = c2.scaleY = 0.8;
c2.down = function (x, y, obj) {
for (var j = 0; j < buttons.length; ++j) game.removeChild(buttons[j]);
cb('bot');
};
game.addChild(c2);
buttons.push(c2);
}
// Prince discard effect
function princeDiscard(who, cb) {
if (who === 'player') {
var card = playerHand[0];
discardPile.push(card);
if (card.id === 'princess') {
showPopup("You discarded the Princess and are out!", 1200, function () {
playerOut = true;
endRound();
cb();
});
return;
} else {
// Draw new card
if (deck.length > 0) {
playerHand[0] = deck.pop();
showPopup("You drew a new card.", 1000, cb);
} else {
playerHand = [];
showPopup("No cards left to draw.", 1000, cb);
}
}
} else {
var card = botHand[0];
discardPile.push(card);
if (card.id === 'princess') {
showPopup("Bot discarded the Princess and is out!", 1200, function () {
botOut = true;
endRound();
cb();
});
return;
} else {
// Draw new card
if (deck.length > 0) {
botHand[0] = deck.pop();
showPopup("Bot drew a new card.", 1000, cb);
} else {
botHand = [];
showPopup("No cards left to draw.", 1000, cb);
}
}
}
}
// End round: check for winner
function endRound() {
roundOver = true;
disablePlayerPlay();
// Reveal bot's hand
for (var i = 0; i < botCardNodes.length; ++i) {
botCardNodes[i].flip(true);
}
// Who wins?
var winner = null;
if (playerOut && botOut) {
winner = null;
} else if (playerOut) {
winner = 'bot';
} else if (botOut) {
winner = 'player';
} else if (deck.length === 0) {
// Compare hands
if (playerHand[0].value > botHand[0].value) winner = 'player';else if (botHand[0].value > playerHand[0].value) winner = 'bot';else winner = null;
}
// Update score
if (winner === 'player') {
playerScore += 1;
if (playerNameText) playerNameText.setText("You: " + playerScore);
if (botNameText) botNameText.setText("Bot: " + botScore);
showPopup("You win the round!", 1800, function () {
checkGameEnd();
});
} else if (winner === 'bot') {
botScore += 1;
if (playerNameText) playerNameText.setText("You: " + playerScore);
if (botNameText) botNameText.setText("Bot: " + botScore);
showPopup("Bot wins the round!", 1800, function () {
checkGameEnd();
});
} else {
if (playerNameText) playerNameText.setText("You: " + playerScore);
if (botNameText) botNameText.setText("Bot: " + botScore);
showPopup("Round is a tie!", 1500, function () {
checkGameEnd();
});
}
}
// Check for game end
function checkGameEnd() {
if (playerScore >= roundTarget) {
LK.setScore(playerScore);
LK.showYouWin();
} else if (botScore >= roundTarget) {
LK.setScore(playerScore);
LK.showGameOver();
} else {
// Start next round
startRound();
}
}
// On every update, remove protection at start of player's turn only
game.update = function () {
// Remove protection at start of player's turn only
if (currentTurn === 'player' && playerProtected) playerProtected = false;
if (currentTurn === 'bot' && botProtected) botProtected = false;
};
// Message box for in-game messages
var messageBox = null;
function showMessageBox(msg) {
// Remove previous message box if present
if (messageBox) {
game.removeChild(messageBox);
messageBox = null;
}
messageBox = new Popup();
messageBox.setText(msg);
messageBox.x = 2048 / 2;
messageBox.y = 2732 - 350; // Near bottom center, above player cards
messageBox.scaleX = 0.9;
messageBox.scaleY = 0.7;
game.addChild(messageBox);
}
function hideMessageBox() {
if (messageBox) {
game.removeChild(messageBox);
messageBox = null;
}
}
// Setup GUI and start game
setupGUI();
startRound();