User prompt
at the bot choose screen bot boxes are wrong object can you made them one colour
User prompt
Can you add a menu to chose how many bot we want? 1-2-3 bot for example
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'shape')' in or related to this line: 'var boardBg = LK.getAsset(LK.init.shape('deskBg', {' Line Number: 109
User prompt
Background is wrong add a brown desk background
User prompt
Can you add a board backgroun
Code edit (1 edits merged)
Please save this source code
User prompt
Love Letter Duel: Me vs Bot
Initial prompt
Made me a me vs bot love letter game.
/****
* 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();