/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var AIPlayer = Container.expand(function () {
var self = Container.call(this);
self.difficulty = 0.7; // AI skill level (0.0 to 1.0)
self.isThinking = false;
self.thinkingTime = 1500; // Time AI takes to "think"
// Find best shot for AI
self.findBestShot = function () {
var playerGroup = currentPlayer === 1 ? player1Group : player2Group;
var targetBalls = [];
// Find target balls based on player group
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
if (ball.isPotted || ball.type === 'cue') continue;
if (!playerGroup) {
// No groups assigned yet, target any numbered ball
if (ball.type !== 'eight') {
targetBalls.push(ball);
}
} else if (ball.type === playerGroup) {
targetBalls.push(ball);
} else if (ball.type === 'eight' && allGroupBallsPotted(playerGroup)) {
targetBalls.push(ball);
}
}
if (targetBalls.length === 0) return null;
// Find best target ball (closest to a pocket)
var bestTarget = null;
var bestScore = -1;
for (var i = 0; i < targetBalls.length; i++) {
var ball = targetBalls[i];
var score = self.evaluateBallShot(ball);
if (score > bestScore) {
bestScore = score;
bestTarget = ball;
}
}
return bestTarget;
};
// Evaluate how good a shot on a specific ball would be
self.evaluateBallShot = function (targetBall) {
var bestPocketScore = 0;
for (var i = 0; i < pockets.length; i++) {
var pocket = pockets[i];
var distanceToPocket = Math.sqrt(Math.pow(targetBall.x - pocket.x, 2) + Math.pow(targetBall.y - pocket.y, 2));
// Check if there's a clear path from cue ball to target ball
var cueToBallClear = self.isPathClear(cueBall.x, cueBall.y, targetBall.x, targetBall.y);
// Check if there's a clear path from target ball to pocket
var ballToPocketClear = self.isPathClear(targetBall.x, targetBall.y, pocket.x, pocket.y);
if (cueToBallClear && ballToPocketClear) {
var score = (1000 - distanceToPocket) / 1000;
if (score > bestPocketScore) {
bestPocketScore = score;
}
}
}
return bestPocketScore;
};
// Check if path between two points is clear of other balls
self.isPathClear = function (x1, y1, x2, y2) {
var dx = x2 - x1;
var dy = y2 - y1;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 10) return true;
var steps = Math.floor(distance / 20);
var stepX = dx / steps;
var stepY = dy / steps;
for (var step = 1; step < steps; step++) {
var checkX = x1 + stepX * step;
var checkY = y1 + stepY * step;
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
if (ball.isPotted || ball === cueBall) continue;
var ballDist = Math.sqrt(Math.pow(checkX - ball.x, 2) + Math.pow(checkY - ball.y, 2));
if (ballDist < ball.radius + 10) {
return false;
}
}
}
return true;
};
// Calculate shot angle and power for target ball
self.calculateShot = function (targetBall) {
if (!targetBall) return null;
var bestShot = null;
var bestScore = -1;
for (var i = 0; i < pockets.length; i++) {
var pocket = pockets[i];
// Calculate angle from target ball to pocket
var ballToPocketDx = pocket.x - targetBall.x;
var ballToPocketDy = pocket.y - targetBall.y;
var ballToPocketAngle = Math.atan2(ballToPocketDy, ballToPocketDx);
// Calculate where cue ball should hit target ball
var hitDistance = targetBall.radius + cueBall.radius + 5;
var hitX = targetBall.x - Math.cos(ballToPocketAngle) * hitDistance;
var hitY = targetBall.y - Math.sin(ballToPocketAngle) * hitDistance;
// Calculate shot from cue ball to hit point
var cueToDx = hitX - cueBall.x;
var cueToDy = hitY - cueBall.y;
var cueToDistance = Math.sqrt(cueToDx * cueToDx + cueToDy * cueToDy);
if (cueToDistance > 50 && self.isPathClear(cueBall.x, cueBall.y, hitX, hitY)) {
var shotAngle = Math.atan2(cueToDy, cueToDx);
var power = Math.min(cueToDistance / 30, 20);
// Add some randomness based on difficulty
var angleError = (1 - self.difficulty) * (Math.random() - 0.5) * 0.3;
var powerError = (1 - self.difficulty) * (Math.random() - 0.5) * 0.3;
shotAngle += angleError;
power += powerError;
var score = self.evaluateBallShot(targetBall);
if (score > bestScore) {
bestScore = score;
bestShot = {
angle: shotAngle,
power: Math.max(5, Math.min(25, power)),
targetBall: targetBall,
pocket: pocket
};
}
}
}
return bestShot;
};
// Execute AI turn
self.takeTurn = function () {
if (currentPlayer === 1 || ballsMoving || gameWon) return;
self.isThinking = true;
gameStatusText.setText('AI shooting...');
var targetBall = self.findBestShot();
var shot = self.calculateShot(targetBall);
if (shot) {
// Calculate force components
var forceX = Math.cos(shot.angle) * shot.power;
var forceY = Math.sin(shot.angle) * shot.power;
// Execute shot
cueBall.hit(forceX, forceY);
turnEnded = true;
} else {
// No good shot found, take a random shot
var randomAngle = Math.random() * Math.PI * 2;
var randomPower = 5 + Math.random() * 10;
var forceX = Math.cos(randomAngle) * randomPower;
var forceY = Math.sin(randomAngle) * randomPower;
cueBall.hit(forceX, forceY);
turnEnded = true;
}
self.isThinking = false;
};
return self;
});
var AimingSystem = Container.expand(function () {
var self = Container.call(this);
self.isAiming = false;
self.startX = 0;
self.startY = 0;
self.endX = 0;
self.endY = 0;
self.power = 0;
self.maxPower = 25;
var aimLine = self.attachAsset('aimLine', {
anchorX: 0,
anchorY: 0.5
});
var powerBarBg = self.attachAsset('powerBar', {
anchorX: 0.5,
anchorY: 0.5
});
var powerBarFill = self.attachAsset('powerFill', {
anchorX: 0,
anchorY: 0.5
});
aimLine.visible = false;
powerBarBg.visible = false;
powerBarFill.visible = false;
powerBarBg.x = 1024;
powerBarBg.y = 2500;
powerBarFill.x = 1024 - 150;
powerBarFill.y = 2500;
self.startAiming = function (x, y) {
self.isAiming = true;
self.startX = x;
self.startY = y;
aimLine.visible = true;
powerBarBg.visible = true;
powerBarFill.visible = true;
};
self.updateAiming = function (x, y) {
if (!self.isAiming) return;
self.endX = x;
self.endY = y;
var dx = self.endX - self.startX;
var dy = self.endY - self.startY;
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
// Update aim line
aimLine.x = self.startX;
aimLine.y = self.startY;
aimLine.rotation = angle;
aimLine.width = Math.min(distance, 200);
// Update power
self.power = Math.min(distance / 20, self.maxPower);
powerBarFill.width = self.power / self.maxPower * 300;
if (self.power < 5) {
powerBarFill.tint = 0x00FF00;
} else if (self.power < 10) {
powerBarFill.tint = 0xFFFF00;
} else {
powerBarFill.tint = 0xFF0000;
}
};
self.shoot = function () {
if (!self.isAiming) return {
forceX: 0,
forceY: 0
};
var dx = self.endX - self.startX;
var dy = self.endY - self.startY;
var distance = Math.sqrt(dx * dx + dy * dy);
var forceX = dx / distance * self.power * 1.0;
var forceY = dy / distance * self.power * 1.0;
self.stopAiming();
return {
forceX: forceX,
forceY: forceY
};
};
self.stopAiming = function () {
self.isAiming = false;
aimLine.visible = false;
powerBarBg.visible = false;
powerBarFill.visible = false;
};
return self;
});
var Ball = Container.expand(function (type, ballNumber) {
var self = Container.call(this);
self.type = type || 'solid';
self.number = ballNumber || 1;
self.velocityX = 0;
self.velocityY = 0;
self.radius = 40;
self.friction = 0.98;
self.isPotted = false;
var ballGraphic;
var numberText;
if (type === 'cue') {
ballGraphic = self.attachAsset('cueBall', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'eight') {
ballGraphic = self.attachAsset('eightBall', {
anchorX: 0.5,
anchorY: 0.5
});
// Add number 8 to the 8-ball
numberText = new Text2('8', {
size: 54,
fill: 0xFFFFFF
});
numberText.anchor.set(0.5, 0.5);
self.addChild(numberText);
} else {
// Use specific colored balls based on number
var assetName = 'ball' + ballNumber;
ballGraphic = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Add number text to all numbered balls
numberText = new Text2(ballNumber.toString(), {
size: 48,
fill: type === 'stripe' ? 0x000000 : 0xFFFFFF
});
numberText.anchor.set(0.5, 0.5);
self.addChild(numberText);
}
self.update = function () {
if (self.isPotted) return;
// Apply physics
self.x += self.velocityX;
self.y += self.velocityY;
// Apply friction - make colored balls more slippery
var frictionValue = self.type === 'cue' ? self.friction : 0.995; // Much lower friction for colored balls to make them slippery
self.velocityX *= frictionValue;
self.velocityY *= frictionValue;
// Stop very slow movement
if (Math.abs(self.velocityX) < 0.1) self.velocityX = 0;
if (Math.abs(self.velocityY) < 0.1) self.velocityY = 0;
// Table bounds collision
var tableLeft = 124 + self.radius;
var tableRight = 1924 - self.radius;
var tableTop = 916 + self.radius;
var tableBottom = 1816 - self.radius;
if (self.x <= tableLeft || self.x >= tableRight) {
self.velocityX *= -0.8;
self.x = Math.max(tableLeft, Math.min(tableRight, self.x));
LK.getSound('railBounce').play();
}
if (self.y <= tableTop || self.y >= tableBottom) {
self.velocityY *= -0.8;
self.y = Math.max(tableTop, Math.min(tableBottom, self.y));
LK.getSound('railBounce').play();
}
};
self.hit = function (forceX, forceY) {
// Reduce white ball (cue ball) force multiplier to decrease its power
var forceMultiplier = self.type === 'cue' ? 1.5 : 1.0;
self.velocityX += forceX * forceMultiplier;
self.velocityY += forceY * forceMultiplier;
LK.getSound('ballHit').play();
};
self.isMoving = function () {
return Math.abs(self.velocityX) > 0.1 || Math.abs(self.velocityY) > 0.1;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a4c2b
});
/****
* Game Code
****/
// Game state variables
// Colorful solid balls (1-7)
// Gold
// Blue
// Red
// Purple
// Orange
// Green
// Brown
// Colorful stripe balls (9-15)
// Gold stripe
// Blue stripe
// Red stripe
// Purple stripe
// Orange stripe
// Green stripe
// Brown stripe
var currentPlayer = 1;
var player1Group = null; // 'solid' or 'stripe'
var player2Group = null;
var gameStarted = false;
var ballsMoving = false;
var turnEnded = false;
var gameWon = false;
var correctBallPotted = false; // Track if correct ball was potted this turn
var difficultySelected = true;
var selectedDifficulty = 'medium';
// Game objects
var balls = [];
var pockets = [];
var cueBall = null;
var aimingSystem = null;
var aiPlayer = null;
// UI elements
var playerTurnText = new Text2('Player Turn', {
size: 60,
fill: 0xFFFFFF
});
playerTurnText.anchor.set(0.5, 0);
var gameStatusText = new Text2('Touch and drag from cue ball to aim', {
size: 40,
fill: 0xFFFF00
});
gameStatusText.anchor.set(0.5, 0);
LK.gui.top.addChild(playerTurnText);
LK.gui.bottom.addChild(gameStatusText);
// Create table
var tableRail = game.addChild(LK.getAsset('tableRail', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
var poolTable = game.addChild(LK.getAsset('poolTable', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
// Create pockets
var pocketPositions = [{
x: 134,
y: 926
},
// top left
{
x: 1024,
y: 916
},
// top middle
{
x: 1914,
y: 926
},
// top right
{
x: 134,
y: 1806
},
// bottom left
{
x: 1024,
y: 1816
},
// bottom middle
{
x: 1914,
y: 1806
} // bottom right
];
for (var i = 0; i < pocketPositions.length; i++) {
var pocket = game.addChild(LK.getAsset('pocket', {
anchorX: 0.5,
anchorY: 0.5,
x: pocketPositions[i].x,
y: pocketPositions[i].y
}));
pockets.push(pocket);
}
// Create aiming system
aimingSystem = game.addChild(new AimingSystem());
// Initialize balls
function setupBalls() {
// Cue ball
cueBall = game.addChild(new Ball('cue', 0));
cueBall.x = 1024 - 400;
cueBall.y = 1366;
balls.push(cueBall);
// Ball rack positions (triangle formation)
var rackX = 1024 + 300;
var rackY = 1366;
var ballSpacing = 82;
var ballTypes = ['solid', 'stripe', 'solid', 'stripe', 'eight', 'stripe', 'solid', 'stripe', 'solid', 'stripe', 'solid', 'stripe', 'solid', 'stripe', 'solid'];
var ballNumbers = [1, 9, 2, 10, 8, 11, 3, 12, 4, 13, 5, 14, 6, 15, 7];
var ballIndex = 0;
// Create triangle rack
for (var row = 0; row < 5; row++) {
for (var col = 0; col <= row; col++) {
if (ballIndex < 15) {
var ball = game.addChild(new Ball(ballTypes[ballIndex], ballNumbers[ballIndex]));
ball.x = rackX + row * ballSpacing * 0.866;
ball.y = rackY - row * ballSpacing * 0.5 + col * ballSpacing;
balls.push(ball);
ballIndex++;
}
}
}
}
// Ball collision detection
function checkBallCollisions() {
for (var i = 0; i < balls.length; i++) {
for (var j = i + 1; j < balls.length; j++) {
var ball1 = balls[i];
var ball2 = balls[j];
if (ball1.isPotted || ball2.isPotted) continue;
var dx = ball2.x - ball1.x;
var dy = ball2.y - ball1.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < ball1.radius + ball2.radius) {
// Collision detected
var angle = Math.atan2(dy, dx);
var sin = Math.sin(angle);
var cos = Math.cos(angle);
// Separate balls
var overlap = ball1.radius + ball2.radius - distance;
ball1.x -= cos * overlap * 0.5;
ball1.y -= sin * overlap * 0.5;
ball2.x += cos * overlap * 0.5;
ball2.y += sin * overlap * 0.5;
// Exchange velocities (simplified)
var tempVx = ball1.velocityX;
var tempVy = ball1.velocityY;
// Apply original force transfer values
var ball1Force = ball1.type === 'cue' ? 1.0 : 1.0;
var ball2Force = ball2.type === 'cue' ? 1.0 : 1.0;
ball1.velocityX = ball2.velocityX * ball1Force;
ball1.velocityY = ball2.velocityY * ball1Force;
ball2.velocityX = tempVx * ball2Force;
ball2.velocityY = tempVy * ball2Force;
LK.getSound('ballHit').play();
}
}
}
}
// Check pocket collisions
function checkPocketCollisions() {
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
if (ball.isPotted) continue;
for (var j = 0; j < pockets.length; j++) {
var pocket = pockets[j];
var dx = ball.x - pocket.x;
var dy = ball.y - pocket.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 60) {
// Ball potted
ball.isPotted = true;
ball.visible = false;
ball.velocityX = 0;
ball.velocityY = 0;
LK.getSound('ballPocket').play();
handleBallPotted(ball);
break;
}
}
}
}
// Handle ball potted logic
function handleBallPotted(ball) {
if (ball.type === 'cue') {
// Scratch - end turn
gameStatusText.setText('Scratch! Turn ends.');
resetCueBall();
} else if (ball.type === 'eight') {
// 8-ball potted
var playerGroup = currentPlayer === 1 ? player1Group : player2Group;
if (playerGroup && allGroupBallsPotted(playerGroup)) {
// Win condition
gameStatusText.setText('Player ' + currentPlayer + ' Wins!');
gameWon = true;
LK.setScore(LK.getScore() + 100);
LK.setTimeout(function () {
LK.showYouWin();
}, 2000);
} else {
// 8-ball potted too early - lose
var winner = currentPlayer === 1 ? 2 : 1;
gameStatusText.setText('Player ' + winner + ' Wins! 8-ball potted early.');
gameWon = true;
LK.setTimeout(function () {
LK.showGameOver();
}, 2000);
}
} else {
// Regular ball potted
if (!player1Group && !player2Group) {
// First ball potted determines groups
if (currentPlayer === 1) {
player1Group = ball.type;
player2Group = ball.type === 'solid' ? 'stripe' : 'solid';
} else {
player2Group = ball.type;
player1Group = ball.type === 'solid' ? 'stripe' : 'solid';
}
}
var playerGroup = currentPlayer === 1 ? player1Group : player2Group;
if (ball.type === playerGroup) {
// Correct ball potted - continue turn
gameStatusText.setText('Good shot! Continue.');
correctBallPotted = true; // Player gets another shot
} else {
// Wrong ball potted - end turn
gameStatusText.setText('Wrong ball! Turn ends.');
}
}
}
// Check if all group balls are potted
function allGroupBallsPotted(group) {
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
if (ball.type === group && !ball.isPotted) {
return false;
}
}
return true;
}
// Reset cue ball after scratch
function resetCueBall() {
cueBall.x = 1024 - 400;
cueBall.y = 1366;
cueBall.isPotted = false;
cueBall.visible = true;
cueBall.velocityX = 0;
cueBall.velocityY = 0;
}
// Switch turns
function switchTurn() {
currentPlayer = currentPlayer === 1 ? 2 : 1;
if (currentPlayer === 1) {
playerTurnText.setText('Player Turn');
gameStatusText.setText('Touch and drag from cue ball to aim');
} else {
playerTurnText.setText('AI Turn');
gameStatusText.setText('AI is preparing...');
}
turnEnded = false;
correctBallPotted = false; // Reset flag for new turn
}
// Check if all balls stopped moving
function checkBallsMoving() {
ballsMoving = false;
for (var i = 0; i < balls.length; i++) {
if (balls[i].isMoving()) {
ballsMoving = true;
break;
}
}
if (!ballsMoving && turnEnded) {
// Turn is complete, check if should switch player
if (!correctBallPotted) {
// No correct ball potted, switch to next player
switchTurn();
} else {
// Correct ball was potted, same player continues
turnEnded = false;
correctBallPotted = false; // Reset flag for next shot
}
}
}
// Touch handling
var isDragging = false;
var dragStartX = 0;
var dragStartY = 0;
game.down = function (x, y, obj) {
if (gameWon || ballsMoving || currentPlayer !== 1) return;
// Check if touching near cue ball
var dx = x - cueBall.x;
var dy = y - cueBall.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 90) {
isDragging = true;
dragStartX = x;
dragStartY = y;
aimingSystem.startAiming(cueBall.x, cueBall.y);
}
};
game.move = function (x, y, obj) {
if (isDragging && !ballsMoving && currentPlayer === 1) {
aimingSystem.updateAiming(x, y);
}
};
game.up = function (x, y, obj) {
if (isDragging && !ballsMoving && currentPlayer === 1) {
var force = aimingSystem.shoot();
cueBall.hit(force.forceX, force.forceY);
isDragging = false;
turnEnded = true;
gameStatusText.setText('Balls in motion...');
}
};
// Initialize game immediately
setupBalls();
aiPlayer = game.addChild(new AIPlayer());
aiPlayer.difficulty = 1.0; // Maximum difficulty - perfect AI
gameStarted = true;
// Main game loop
game.update = function () {
if (!difficultySelected || !gameStarted || gameWon) return;
// Update all balls
for (var i = 0; i < balls.length; i++) {
balls[i].update();
}
// Check collisions
checkBallCollisions();
checkPocketCollisions();
// Check if balls are still moving
checkBallsMoving();
// Trigger AI turn if it's AI's turn and balls are not moving
if (currentPlayer === 2 && !ballsMoving && !turnEnded && !aiPlayer.isThinking) {
aiPlayer.takeTurn();
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var AIPlayer = Container.expand(function () {
var self = Container.call(this);
self.difficulty = 0.7; // AI skill level (0.0 to 1.0)
self.isThinking = false;
self.thinkingTime = 1500; // Time AI takes to "think"
// Find best shot for AI
self.findBestShot = function () {
var playerGroup = currentPlayer === 1 ? player1Group : player2Group;
var targetBalls = [];
// Find target balls based on player group
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
if (ball.isPotted || ball.type === 'cue') continue;
if (!playerGroup) {
// No groups assigned yet, target any numbered ball
if (ball.type !== 'eight') {
targetBalls.push(ball);
}
} else if (ball.type === playerGroup) {
targetBalls.push(ball);
} else if (ball.type === 'eight' && allGroupBallsPotted(playerGroup)) {
targetBalls.push(ball);
}
}
if (targetBalls.length === 0) return null;
// Find best target ball (closest to a pocket)
var bestTarget = null;
var bestScore = -1;
for (var i = 0; i < targetBalls.length; i++) {
var ball = targetBalls[i];
var score = self.evaluateBallShot(ball);
if (score > bestScore) {
bestScore = score;
bestTarget = ball;
}
}
return bestTarget;
};
// Evaluate how good a shot on a specific ball would be
self.evaluateBallShot = function (targetBall) {
var bestPocketScore = 0;
for (var i = 0; i < pockets.length; i++) {
var pocket = pockets[i];
var distanceToPocket = Math.sqrt(Math.pow(targetBall.x - pocket.x, 2) + Math.pow(targetBall.y - pocket.y, 2));
// Check if there's a clear path from cue ball to target ball
var cueToBallClear = self.isPathClear(cueBall.x, cueBall.y, targetBall.x, targetBall.y);
// Check if there's a clear path from target ball to pocket
var ballToPocketClear = self.isPathClear(targetBall.x, targetBall.y, pocket.x, pocket.y);
if (cueToBallClear && ballToPocketClear) {
var score = (1000 - distanceToPocket) / 1000;
if (score > bestPocketScore) {
bestPocketScore = score;
}
}
}
return bestPocketScore;
};
// Check if path between two points is clear of other balls
self.isPathClear = function (x1, y1, x2, y2) {
var dx = x2 - x1;
var dy = y2 - y1;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 10) return true;
var steps = Math.floor(distance / 20);
var stepX = dx / steps;
var stepY = dy / steps;
for (var step = 1; step < steps; step++) {
var checkX = x1 + stepX * step;
var checkY = y1 + stepY * step;
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
if (ball.isPotted || ball === cueBall) continue;
var ballDist = Math.sqrt(Math.pow(checkX - ball.x, 2) + Math.pow(checkY - ball.y, 2));
if (ballDist < ball.radius + 10) {
return false;
}
}
}
return true;
};
// Calculate shot angle and power for target ball
self.calculateShot = function (targetBall) {
if (!targetBall) return null;
var bestShot = null;
var bestScore = -1;
for (var i = 0; i < pockets.length; i++) {
var pocket = pockets[i];
// Calculate angle from target ball to pocket
var ballToPocketDx = pocket.x - targetBall.x;
var ballToPocketDy = pocket.y - targetBall.y;
var ballToPocketAngle = Math.atan2(ballToPocketDy, ballToPocketDx);
// Calculate where cue ball should hit target ball
var hitDistance = targetBall.radius + cueBall.radius + 5;
var hitX = targetBall.x - Math.cos(ballToPocketAngle) * hitDistance;
var hitY = targetBall.y - Math.sin(ballToPocketAngle) * hitDistance;
// Calculate shot from cue ball to hit point
var cueToDx = hitX - cueBall.x;
var cueToDy = hitY - cueBall.y;
var cueToDistance = Math.sqrt(cueToDx * cueToDx + cueToDy * cueToDy);
if (cueToDistance > 50 && self.isPathClear(cueBall.x, cueBall.y, hitX, hitY)) {
var shotAngle = Math.atan2(cueToDy, cueToDx);
var power = Math.min(cueToDistance / 30, 20);
// Add some randomness based on difficulty
var angleError = (1 - self.difficulty) * (Math.random() - 0.5) * 0.3;
var powerError = (1 - self.difficulty) * (Math.random() - 0.5) * 0.3;
shotAngle += angleError;
power += powerError;
var score = self.evaluateBallShot(targetBall);
if (score > bestScore) {
bestScore = score;
bestShot = {
angle: shotAngle,
power: Math.max(5, Math.min(25, power)),
targetBall: targetBall,
pocket: pocket
};
}
}
}
return bestShot;
};
// Execute AI turn
self.takeTurn = function () {
if (currentPlayer === 1 || ballsMoving || gameWon) return;
self.isThinking = true;
gameStatusText.setText('AI shooting...');
var targetBall = self.findBestShot();
var shot = self.calculateShot(targetBall);
if (shot) {
// Calculate force components
var forceX = Math.cos(shot.angle) * shot.power;
var forceY = Math.sin(shot.angle) * shot.power;
// Execute shot
cueBall.hit(forceX, forceY);
turnEnded = true;
} else {
// No good shot found, take a random shot
var randomAngle = Math.random() * Math.PI * 2;
var randomPower = 5 + Math.random() * 10;
var forceX = Math.cos(randomAngle) * randomPower;
var forceY = Math.sin(randomAngle) * randomPower;
cueBall.hit(forceX, forceY);
turnEnded = true;
}
self.isThinking = false;
};
return self;
});
var AimingSystem = Container.expand(function () {
var self = Container.call(this);
self.isAiming = false;
self.startX = 0;
self.startY = 0;
self.endX = 0;
self.endY = 0;
self.power = 0;
self.maxPower = 25;
var aimLine = self.attachAsset('aimLine', {
anchorX: 0,
anchorY: 0.5
});
var powerBarBg = self.attachAsset('powerBar', {
anchorX: 0.5,
anchorY: 0.5
});
var powerBarFill = self.attachAsset('powerFill', {
anchorX: 0,
anchorY: 0.5
});
aimLine.visible = false;
powerBarBg.visible = false;
powerBarFill.visible = false;
powerBarBg.x = 1024;
powerBarBg.y = 2500;
powerBarFill.x = 1024 - 150;
powerBarFill.y = 2500;
self.startAiming = function (x, y) {
self.isAiming = true;
self.startX = x;
self.startY = y;
aimLine.visible = true;
powerBarBg.visible = true;
powerBarFill.visible = true;
};
self.updateAiming = function (x, y) {
if (!self.isAiming) return;
self.endX = x;
self.endY = y;
var dx = self.endX - self.startX;
var dy = self.endY - self.startY;
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
// Update aim line
aimLine.x = self.startX;
aimLine.y = self.startY;
aimLine.rotation = angle;
aimLine.width = Math.min(distance, 200);
// Update power
self.power = Math.min(distance / 20, self.maxPower);
powerBarFill.width = self.power / self.maxPower * 300;
if (self.power < 5) {
powerBarFill.tint = 0x00FF00;
} else if (self.power < 10) {
powerBarFill.tint = 0xFFFF00;
} else {
powerBarFill.tint = 0xFF0000;
}
};
self.shoot = function () {
if (!self.isAiming) return {
forceX: 0,
forceY: 0
};
var dx = self.endX - self.startX;
var dy = self.endY - self.startY;
var distance = Math.sqrt(dx * dx + dy * dy);
var forceX = dx / distance * self.power * 1.0;
var forceY = dy / distance * self.power * 1.0;
self.stopAiming();
return {
forceX: forceX,
forceY: forceY
};
};
self.stopAiming = function () {
self.isAiming = false;
aimLine.visible = false;
powerBarBg.visible = false;
powerBarFill.visible = false;
};
return self;
});
var Ball = Container.expand(function (type, ballNumber) {
var self = Container.call(this);
self.type = type || 'solid';
self.number = ballNumber || 1;
self.velocityX = 0;
self.velocityY = 0;
self.radius = 40;
self.friction = 0.98;
self.isPotted = false;
var ballGraphic;
var numberText;
if (type === 'cue') {
ballGraphic = self.attachAsset('cueBall', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'eight') {
ballGraphic = self.attachAsset('eightBall', {
anchorX: 0.5,
anchorY: 0.5
});
// Add number 8 to the 8-ball
numberText = new Text2('8', {
size: 54,
fill: 0xFFFFFF
});
numberText.anchor.set(0.5, 0.5);
self.addChild(numberText);
} else {
// Use specific colored balls based on number
var assetName = 'ball' + ballNumber;
ballGraphic = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Add number text to all numbered balls
numberText = new Text2(ballNumber.toString(), {
size: 48,
fill: type === 'stripe' ? 0x000000 : 0xFFFFFF
});
numberText.anchor.set(0.5, 0.5);
self.addChild(numberText);
}
self.update = function () {
if (self.isPotted) return;
// Apply physics
self.x += self.velocityX;
self.y += self.velocityY;
// Apply friction - make colored balls more slippery
var frictionValue = self.type === 'cue' ? self.friction : 0.995; // Much lower friction for colored balls to make them slippery
self.velocityX *= frictionValue;
self.velocityY *= frictionValue;
// Stop very slow movement
if (Math.abs(self.velocityX) < 0.1) self.velocityX = 0;
if (Math.abs(self.velocityY) < 0.1) self.velocityY = 0;
// Table bounds collision
var tableLeft = 124 + self.radius;
var tableRight = 1924 - self.radius;
var tableTop = 916 + self.radius;
var tableBottom = 1816 - self.radius;
if (self.x <= tableLeft || self.x >= tableRight) {
self.velocityX *= -0.8;
self.x = Math.max(tableLeft, Math.min(tableRight, self.x));
LK.getSound('railBounce').play();
}
if (self.y <= tableTop || self.y >= tableBottom) {
self.velocityY *= -0.8;
self.y = Math.max(tableTop, Math.min(tableBottom, self.y));
LK.getSound('railBounce').play();
}
};
self.hit = function (forceX, forceY) {
// Reduce white ball (cue ball) force multiplier to decrease its power
var forceMultiplier = self.type === 'cue' ? 1.5 : 1.0;
self.velocityX += forceX * forceMultiplier;
self.velocityY += forceY * forceMultiplier;
LK.getSound('ballHit').play();
};
self.isMoving = function () {
return Math.abs(self.velocityX) > 0.1 || Math.abs(self.velocityY) > 0.1;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a4c2b
});
/****
* Game Code
****/
// Game state variables
// Colorful solid balls (1-7)
// Gold
// Blue
// Red
// Purple
// Orange
// Green
// Brown
// Colorful stripe balls (9-15)
// Gold stripe
// Blue stripe
// Red stripe
// Purple stripe
// Orange stripe
// Green stripe
// Brown stripe
var currentPlayer = 1;
var player1Group = null; // 'solid' or 'stripe'
var player2Group = null;
var gameStarted = false;
var ballsMoving = false;
var turnEnded = false;
var gameWon = false;
var correctBallPotted = false; // Track if correct ball was potted this turn
var difficultySelected = true;
var selectedDifficulty = 'medium';
// Game objects
var balls = [];
var pockets = [];
var cueBall = null;
var aimingSystem = null;
var aiPlayer = null;
// UI elements
var playerTurnText = new Text2('Player Turn', {
size: 60,
fill: 0xFFFFFF
});
playerTurnText.anchor.set(0.5, 0);
var gameStatusText = new Text2('Touch and drag from cue ball to aim', {
size: 40,
fill: 0xFFFF00
});
gameStatusText.anchor.set(0.5, 0);
LK.gui.top.addChild(playerTurnText);
LK.gui.bottom.addChild(gameStatusText);
// Create table
var tableRail = game.addChild(LK.getAsset('tableRail', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
var poolTable = game.addChild(LK.getAsset('poolTable', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
// Create pockets
var pocketPositions = [{
x: 134,
y: 926
},
// top left
{
x: 1024,
y: 916
},
// top middle
{
x: 1914,
y: 926
},
// top right
{
x: 134,
y: 1806
},
// bottom left
{
x: 1024,
y: 1816
},
// bottom middle
{
x: 1914,
y: 1806
} // bottom right
];
for (var i = 0; i < pocketPositions.length; i++) {
var pocket = game.addChild(LK.getAsset('pocket', {
anchorX: 0.5,
anchorY: 0.5,
x: pocketPositions[i].x,
y: pocketPositions[i].y
}));
pockets.push(pocket);
}
// Create aiming system
aimingSystem = game.addChild(new AimingSystem());
// Initialize balls
function setupBalls() {
// Cue ball
cueBall = game.addChild(new Ball('cue', 0));
cueBall.x = 1024 - 400;
cueBall.y = 1366;
balls.push(cueBall);
// Ball rack positions (triangle formation)
var rackX = 1024 + 300;
var rackY = 1366;
var ballSpacing = 82;
var ballTypes = ['solid', 'stripe', 'solid', 'stripe', 'eight', 'stripe', 'solid', 'stripe', 'solid', 'stripe', 'solid', 'stripe', 'solid', 'stripe', 'solid'];
var ballNumbers = [1, 9, 2, 10, 8, 11, 3, 12, 4, 13, 5, 14, 6, 15, 7];
var ballIndex = 0;
// Create triangle rack
for (var row = 0; row < 5; row++) {
for (var col = 0; col <= row; col++) {
if (ballIndex < 15) {
var ball = game.addChild(new Ball(ballTypes[ballIndex], ballNumbers[ballIndex]));
ball.x = rackX + row * ballSpacing * 0.866;
ball.y = rackY - row * ballSpacing * 0.5 + col * ballSpacing;
balls.push(ball);
ballIndex++;
}
}
}
}
// Ball collision detection
function checkBallCollisions() {
for (var i = 0; i < balls.length; i++) {
for (var j = i + 1; j < balls.length; j++) {
var ball1 = balls[i];
var ball2 = balls[j];
if (ball1.isPotted || ball2.isPotted) continue;
var dx = ball2.x - ball1.x;
var dy = ball2.y - ball1.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < ball1.radius + ball2.radius) {
// Collision detected
var angle = Math.atan2(dy, dx);
var sin = Math.sin(angle);
var cos = Math.cos(angle);
// Separate balls
var overlap = ball1.radius + ball2.radius - distance;
ball1.x -= cos * overlap * 0.5;
ball1.y -= sin * overlap * 0.5;
ball2.x += cos * overlap * 0.5;
ball2.y += sin * overlap * 0.5;
// Exchange velocities (simplified)
var tempVx = ball1.velocityX;
var tempVy = ball1.velocityY;
// Apply original force transfer values
var ball1Force = ball1.type === 'cue' ? 1.0 : 1.0;
var ball2Force = ball2.type === 'cue' ? 1.0 : 1.0;
ball1.velocityX = ball2.velocityX * ball1Force;
ball1.velocityY = ball2.velocityY * ball1Force;
ball2.velocityX = tempVx * ball2Force;
ball2.velocityY = tempVy * ball2Force;
LK.getSound('ballHit').play();
}
}
}
}
// Check pocket collisions
function checkPocketCollisions() {
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
if (ball.isPotted) continue;
for (var j = 0; j < pockets.length; j++) {
var pocket = pockets[j];
var dx = ball.x - pocket.x;
var dy = ball.y - pocket.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 60) {
// Ball potted
ball.isPotted = true;
ball.visible = false;
ball.velocityX = 0;
ball.velocityY = 0;
LK.getSound('ballPocket').play();
handleBallPotted(ball);
break;
}
}
}
}
// Handle ball potted logic
function handleBallPotted(ball) {
if (ball.type === 'cue') {
// Scratch - end turn
gameStatusText.setText('Scratch! Turn ends.');
resetCueBall();
} else if (ball.type === 'eight') {
// 8-ball potted
var playerGroup = currentPlayer === 1 ? player1Group : player2Group;
if (playerGroup && allGroupBallsPotted(playerGroup)) {
// Win condition
gameStatusText.setText('Player ' + currentPlayer + ' Wins!');
gameWon = true;
LK.setScore(LK.getScore() + 100);
LK.setTimeout(function () {
LK.showYouWin();
}, 2000);
} else {
// 8-ball potted too early - lose
var winner = currentPlayer === 1 ? 2 : 1;
gameStatusText.setText('Player ' + winner + ' Wins! 8-ball potted early.');
gameWon = true;
LK.setTimeout(function () {
LK.showGameOver();
}, 2000);
}
} else {
// Regular ball potted
if (!player1Group && !player2Group) {
// First ball potted determines groups
if (currentPlayer === 1) {
player1Group = ball.type;
player2Group = ball.type === 'solid' ? 'stripe' : 'solid';
} else {
player2Group = ball.type;
player1Group = ball.type === 'solid' ? 'stripe' : 'solid';
}
}
var playerGroup = currentPlayer === 1 ? player1Group : player2Group;
if (ball.type === playerGroup) {
// Correct ball potted - continue turn
gameStatusText.setText('Good shot! Continue.');
correctBallPotted = true; // Player gets another shot
} else {
// Wrong ball potted - end turn
gameStatusText.setText('Wrong ball! Turn ends.');
}
}
}
// Check if all group balls are potted
function allGroupBallsPotted(group) {
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
if (ball.type === group && !ball.isPotted) {
return false;
}
}
return true;
}
// Reset cue ball after scratch
function resetCueBall() {
cueBall.x = 1024 - 400;
cueBall.y = 1366;
cueBall.isPotted = false;
cueBall.visible = true;
cueBall.velocityX = 0;
cueBall.velocityY = 0;
}
// Switch turns
function switchTurn() {
currentPlayer = currentPlayer === 1 ? 2 : 1;
if (currentPlayer === 1) {
playerTurnText.setText('Player Turn');
gameStatusText.setText('Touch and drag from cue ball to aim');
} else {
playerTurnText.setText('AI Turn');
gameStatusText.setText('AI is preparing...');
}
turnEnded = false;
correctBallPotted = false; // Reset flag for new turn
}
// Check if all balls stopped moving
function checkBallsMoving() {
ballsMoving = false;
for (var i = 0; i < balls.length; i++) {
if (balls[i].isMoving()) {
ballsMoving = true;
break;
}
}
if (!ballsMoving && turnEnded) {
// Turn is complete, check if should switch player
if (!correctBallPotted) {
// No correct ball potted, switch to next player
switchTurn();
} else {
// Correct ball was potted, same player continues
turnEnded = false;
correctBallPotted = false; // Reset flag for next shot
}
}
}
// Touch handling
var isDragging = false;
var dragStartX = 0;
var dragStartY = 0;
game.down = function (x, y, obj) {
if (gameWon || ballsMoving || currentPlayer !== 1) return;
// Check if touching near cue ball
var dx = x - cueBall.x;
var dy = y - cueBall.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 90) {
isDragging = true;
dragStartX = x;
dragStartY = y;
aimingSystem.startAiming(cueBall.x, cueBall.y);
}
};
game.move = function (x, y, obj) {
if (isDragging && !ballsMoving && currentPlayer === 1) {
aimingSystem.updateAiming(x, y);
}
};
game.up = function (x, y, obj) {
if (isDragging && !ballsMoving && currentPlayer === 1) {
var force = aimingSystem.shoot();
cueBall.hit(force.forceX, force.forceY);
isDragging = false;
turnEnded = true;
gameStatusText.setText('Balls in motion...');
}
};
// Initialize game immediately
setupBalls();
aiPlayer = game.addChild(new AIPlayer());
aiPlayer.difficulty = 1.0; // Maximum difficulty - perfect AI
gameStarted = true;
// Main game loop
game.update = function () {
if (!difficultySelected || !gameStarted || gameWon) return;
// Update all balls
for (var i = 0; i < balls.length; i++) {
balls[i].update();
}
// Check collisions
checkBallCollisions();
checkPocketCollisions();
// Check if balls are still moving
checkBallsMoving();
// Trigger AI turn if it's AI's turn and balls are not moving
if (currentPlayer === 2 && !ballsMoving && !turnEnded && !aiPlayer.isThinking) {
aiPlayer.takeTurn();
}
};