In this tutorial, I will teach you how to build an 8 Ball Pool Multiplayer Billiards Game Using JavaScript. The complete source code of the JavaScript 8 Ball Pool Game is given in this guide.
I have also added Live Demo and Download buttons at the end of this tutorial, so you can easily download the code of this 8 Ball Pool game with a single click. You can try it yourself on your computer by downloading the code or even play it online using the Live Demo.
Features
- Player vs Player match
- Player vs Computer match
- Artificial Intelligence (AI) with various difficulty levels.
- Aim by moving the mouse.
- Left click: shoot.
- Increase/Decrease shot power.
Folder Structure
- assets
- Note:- All the sound and image files will go here. You can download the complete project including all the asset files at the end of this article.
- css
- game-layout.css
- script
- AI
- AIPolicy.js
- AITrainer.js
- Opponent.js
- game_objects
- Ball.js
- Player.js
- Score.js
- Stick.js
- geom
- Vector2.js
- input
- ButtonState.js
- Keyboard.js
- Mouse.js
- lib
- LAB.min.js
- menu
- Button.js
- Label.js
- MainMenu.js
- Menu.js
- system
- Color.js
- Keys.js
- Assets.js
- Canvas2D.js
- Game.js
- GamePolicy.js
- GameWorld.js
- Global.js
- AI
- index.html
JavaScript 8 Ball Pool Game Full Source Code
index.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF8">
<title>Classic Pool Game</title>
<link rel="stylesheet" type="text/css" href="css/game-layout.css"/>
<link rel="shortcut icon" type="image/png" href="assets/sprites/favicon.png"/>
<script src="script/lib/LAB.min.js"></script>
<script>
$LAB
.script('script/system/Keys.js').wait()
.script('script/system/Color.js').wait()
.script('script/geom/Vector2.js').wait()
.script('script/input/ButtonState.js').wait()
.script('script/input/Keyboard.js').wait()
.script('script/input/Mouse.js').wait()
.script('script/Global.js').wait()
.script('script/Canvas2D.js').wait()
.script('script/game_objects/Score.js').wait()
.script('script/game_objects/Ball.js').wait()
.script('script/game_objects/Stick.js').wait()
.script('script/menu/Label.js').wait()
.script('script/menu/Button.js').wait()
.script('script/menu/Menu.js').wait()
.script('script/menu/MainMenu.js').wait()
.script('script/AI/Opponent.js').wait()
.script('script/AI/AIPolicy.js').wait()
.script('script/AI/AITrainer.js').wait()
.script('script/game_objects/Player.js').wait()
.script('script/GamePolicy.js').wait()
.script('script/GameWorld.js').wait()
.script('script/Game.js').wait()
.script('script/Assets.js').wait(function () {
Game.start('gameArea','screen', 1500, 825);
});
</script>
</head>
<body style = "background-color:black">
<div id="gameArea">
<canvas id="screen" width="2000" height="1000"></canvas>
</div>
</body>
</html>css/game-layout.css
html, body {
margin: 0;
}
/*#screen{
padding-left: 0;
padding-right: 0;
margin-left: auto;
margin-right: auto;
display: block;
}*/script/AI/AIPolicy.js
function AIPolicy(){
}
AIPolicy.prototype.evaluate = function(state, gamePolicy){
let evaluation = 1;
for (var i = 0 ; i < state.balls.length; i++){
for(var j = i + 1 ; j < state.balls.length ; j++){
let firstBall = state.balls[i];
let secondBall = state.balls[j];
if(firstBall === state.whiteBall || secondBall === state.whiteBall
||
firstBall.inHole || secondBall.inHole){
continue;
}
evaluation += firstBall.position.distanceFrom(secondBall.position);
}
}
evaluation = evaluation/5800;
if(!gamePolicy.firstCollision){
evaluation+= 100;
}
evaluation += 2000 * gamePolicy.validBallsInsertedOnTurn;
gamePolicy.updateTurnOutcome();
if(gamePolicy.won){
if(!gamePolicy.foul){
evaluation += 10000;
}
else{
evaluation -= 10000;
}
}
if(gamePolicy.foul){
evaluation = evaluation - 3000;
}
return evaluation;
}script/AI/AITrainer.js
function AITrainer(){
this.AIPolicy = new AIPolicy();
}
AITrainer.prototype.init = function(state, gamePolicy){
AI.opponents = [];
AI.currentOpponent = new Opponent();
AI.finishedSession = true;
AI.iteration = 0;
AI.bestOpponentIndex = 0;
AI.bestOpponentEval = 0;
if(gamePolicy.foul){
//TO DO: Pick best position for the white ball.
state.whiteBall.position.x = 413;
state.whiteBall.position.y = 413;
state.whiteBall.inHole = false;
gamePolicy.foul = false;
}
AI.initialState = JSON.parse(JSON.stringify(state));
AI.initialGamePolicyState = JSON.parse(JSON.stringify(gamePolicy));
AI.state = state;
AI.gamePolicy = gamePolicy;
}
AITrainer.prototype.train = function(){
if(AI.iteration === TRAIN_ITER){
AI.finishedSession = true;
AI.playTurn();
return;
}
let ballsMoving = AI.state.ballsMoving();
if(!ballsMoving){
if(AI.iteration !== 0){
AI.currentOpponent.evaluation = AI.AIPolicy.evaluate(this.state, this.gamePolicy);
AI.opponents.push(JSON.parse(JSON.stringify(AI.currentOpponent)));
if(AI.currentOpponent.evaluation > AI.bestOpponentEval){
AI.bestOpponentEval = AI.currentOpponent.evaluation;
AI.bestOpponentIndex = AI.opponents.length - 1;
}
if(LOG){
console.log('-------------'+new Number(AI.iteration+1)+'--------------------');
console.log('Current evaluation: ' + AI.currentOpponent.evaluation);
console.log('Current power: ' + AI.currentOpponent.power);
console.log('Current rotation: ' + AI.currentOpponent.rotation);
console.log('---------------------------------');
}
}
AI.state.initiateState(AI.initialState.balls);
AI.gamePolicy.initiateState(AI.initialGamePolicyState);
AI.buildNewOpponent();
AI.simulate();
}
}
AITrainer.prototype.buildNewOpponent = function(){
if(AI.iteration % 10 === 0){
AI.currentOpponent = new Opponent();
AI.iteration++;
return;
}
let bestOpponent = AI.opponents[AI.bestOpponentIndex];
let newPower = bestOpponent.power;
newPower += + ((Math.random() * 30) - 15);
newPower = newPower < 20 ? 20 : newPower;
newPower = newPower > 75 ? 75 : newPower;
let newRotation = bestOpponent.rotation;
if(bestOpponent.evaluation > 0){
newRotation += (1/bestOpponent.evaluation)*(Math.random() * 2 * Math.PI - Math.PI)
}
else{
newRotation = (Math.random() * 2 * Math.PI - Math.PI);
}
AI.currentOpponent = new Opponent(newPower,newRotation);
AI.iteration++;
}
AITrainer.prototype.simulate = function(){
AI.state.stick.shoot(AI.currentOpponent.power, AI.currentOpponent.rotation);
}
AITrainer.prototype.playTurn = function(){
bestOpponent = AI.opponents[AI.bestOpponentIndex];
Game.gameWorld.stick.rotation = bestOpponent.rotation;
Game.gameWorld.stick.trackMouse = false;
setTimeout(() => {
Game.gameWorld.stick.visible = true;
Canvas2D.clear();
Game.gameWorld.draw();
Game.sound = true;
Game.gameWorld.initiateState(AI.initialState.balls);
Game.policy.initiateState(AI.initialGamePolicyState);
DISPLAY = true;
requestAnimationFrame(Game.mainLoop);
Game.gameWorld.stick
.shoot(
bestOpponent.power,
bestOpponent.rotation
);
Game.gameWorld.stick.trackMouse = true;
}, 1000);
}
AITrainer.prototype.opponentTrainingLoop = function(){
Game.sound = false;
DISPLAY = false;
if(DISPLAY_TRAINING){
if(!AI.finishedSession){
AI.train();
Game.gameWorld.handleInput(DELTA);
Game.gameWorld.update(DELTA);
Canvas2D.clear();
Game.gameWorld.draw();
Mouse.reset();
setTimeout(AI.opponentTrainingLoop,0.00000000001);
}
}
else{
while(!AI.finishedSession){
AI.train();
Game.gameWorld.handleInput(DELTA);
Game.gameWorld.update(DELTA);
Mouse.reset();
}
}
}
AITrainer.prototype.startSession = function(){
setTimeout(
()=>{
Game.gameWorld.stick.visible = false;
Canvas2D.clear();
Game.gameWorld.draw();
AI.init(Game.gameWorld, Game.policy);
AI.finishedSession = false;
AI.opponentTrainingLoop();
},
1000
);
}
const AI = new AITrainer();script/AI/Opponent.js
function Opponent(power, rotation){
this.power = power || (Math.random() * 75 + 1);
this.rotation = rotation || (Math.random()*6.283)-3.141;
this.evaluation = 0;
}script/game_objects/Ball.js
"use strict";
function Ball(initPos,color){
this.initPos = initPos;
this.position = initPos.copy();
this.origin = new Vector2(25,25);
this.velocity = Vector2.zero;
this.color = color;
this.moving = false;
this.visible = true;
this.inHole = false;
}
Object.defineProperty(Ball.prototype, "color",
{
get: function(){
if(this.sprite == sprites.redBall){
return Color.red;
}
else if(this.sprite == sprites.yellowBall){
return Color.yellow;
}
else if(this.sprite == sprites.blackBall){
return Color.black;
}
else{
return Color.white;
}
},
set: function (value) {
if (value === Color.red){
this.sprite = sprites.redBall;
}
else if(value == Color.yellow){
this.sprite = sprites.yellowBall;
}
else if(value == Color.black){
this.sprite = sprites.blackBall;
}
else{
this.sprite = sprites.ball;
}
}
});
Ball.prototype.shoot = function(power, angle){
if(power <= 0)
return;
this.moving = true;
this.velocity = calculateBallVelocity(power,angle);
}
var calculateBallVelocity = function(power, angle){
return new Vector2(100*Math.cos(angle)*power,100*Math.sin(angle)*power);
}
Ball.prototype.update = function(delta){
this.updatePosition(delta);
this.velocity.multiplyWith(0.98);
if(this.moving && Math.abs(this.velocity.x) < 1 && Math.abs(this.velocity.y) < 1){
this.stop();
}
}
Ball.prototype.updatePosition = function(delta){
if(!this.moving || this.inHole)
return;
var ball = this;
var newPos = this.position.add(this.velocity.multiply(delta));
if(Game.policy.isInsideHole(newPos)){
if(Game.sound && SOUND_ON){
var holeSound = sounds.hole.cloneNode(true);
holeSound.volume = 0.5;
holeSound.play();
}
this.position = newPos;
this.inHole = true;
setTimeout(function(){ball.visible=false;ball.velocity = Vector2.zero;}, 100);
Game.policy.handleBallInHole(this);
return;
}
var collision = this.handleCollision(newPos);
if(collision){
this.velocity.multiplyWith(0.95);
}else{
this.position = newPos;
}
}
Ball.prototype.handleCollision = function(newPos){
var collision = false;
if(Game.policy.isXOutsideLeftBorder(newPos, this.origin)){
this.velocity.x = -this.velocity.x;
this.position.x = Game.policy.leftBorderX + this.origin.x;
collision = true;
}
else if(Game.policy.isXOutsideRightBorder(newPos, this.origin)){
this.velocity.x = -this.velocity.x;
this.position.x = Game.policy.rightBorderX - this.origin.x;
collision = true;
}
if(Game.policy.isYOutsideTopBorder(newPos, this.origin)){
this.velocity.y = -this.velocity.y;
this.position.y = Game.policy.topBorderY + this.origin.y;
collision = true;
}
else if(Game.policy.isYOutsideBottomBorder(newPos, this.origin)){
this.velocity.y = -this.velocity.y;
this.position.y = Game.policy.bottomBorderY - this.origin.y;
collision = true;
}
return collision;
}
Ball.prototype.stop = function(){
this.moving = false;
this.velocity = Vector2.zero;
}
Ball.prototype.reset = function(){
this.inHole = false;
this.moving = false;
this.velocity = Vector2.zero;
this.position = this.initPos;
this.visible = true;
}
Ball.prototype.out = function(){
this.position = new Vector2(0, 900);
this.visible = false;
this.inHole = true;
}
Ball.prototype.draw = function () {
if(!this.visible)
return;
Canvas2D.drawImage(this.sprite, this.position, 0, 1, new Vector2(25,25));
};script/game_objects/Player.js
function Player(matchScore, totalScore){
this.color = undefined;
this.matchScore = matchScore;
this.totalScore = totalScore;
}script/game_objects/Score.js
"use strict";
function Score(position){
this.position = position;
this.origin = new Vector2(47,82);
this.value = 0;
}
Score.prototype.reset = function(){
this.position = position;
this.origin = new Vector2(30,0);
this.value = 0;
};
Score.prototype.draw = function () {
Canvas2D.drawText(
this.value,
this.position,
this.origin,
"#096834",
"top",
"Impact",
"200px"
);
};
Score.prototype.drawLines = function (color) {
for(let i=0; i<this.value; i++){
let pos = this.position.add(new Vector2(i*15,0));
Canvas2D.drawText(
"I",
pos,
this.origin,
color,
"top",
"Arial",
"20px"
);
}
};
Score.prototype.increment = function(){
this.value++;
};script/game_objects/Stick.js
"use strict";
function Stick(position){
this.position = position;
this.origin = new Vector2(970,11);
this.shotOrigin = new Vector2(950,11);
this.shooting = false;
this.visible = true;
this.rotation = 0;
this.power = 0;
this.trackMouse = true;
}
Stick.prototype.handleInput = function (delta) {
if(AI_ON && Game.policy.turn === AI_PLAYER_NUM)
return;
if(Game.policy.turnPlayed)
return;
if(Keyboard.down(Keys.W) && KEYBOARD_INPUT_ON){
if(this.power < 75){
this.origin.x+=2;
this.power+=1.2;
}
}
if(Keyboard.down(Keys.S) && KEYBOARD_INPUT_ON){
if(this.power>0){
this.origin.x-=2;
this.power-=1.2;
}
}
else if (this.power>0 && Mouse.left.down){
var strike = sounds.strike.cloneNode(true);
strike.volume = (this.power/(10))<1?(this.power/(10)):1;
strike.play();
Game.policy.turnPlayed = true;
this.shooting = true;
this.origin = this.shotOrigin.copy();
Game.gameWorld.whiteBall.shoot(this.power, this.rotation);
var stick = this;
setTimeout(function(){stick.visible = false;}, 500);
}
else if(this.trackMouse){
var opposite = Mouse.position.y - this.position.y;
var adjacent = Mouse.position.x - this.position.x;
this.rotation = Math.atan2(opposite, adjacent);
}
};
Stick.prototype.shoot = function(power, rotation){
this.power = power;
this.rotation = rotation;
if(Game.sound && SOUND_ON){
var strike = sounds.strike.cloneNode(true);
strike.volume = (this.power/(10))<1?(this.power/(10)):1;
strike.play();
}
Game.policy.turnPlayed = true;
this.shooting = true;
this.origin = this.shotOrigin.copy();
Game.gameWorld.whiteBall.shoot(this.power, this.rotation);
var stick = this;
setTimeout(function(){stick.visible = false;}, 500);
}
Stick.prototype.update = function(){
if(this.shooting && !Game.gameWorld.whiteBall.moving)
this.reset();
};
Stick.prototype.reset = function(){
this.position.x = Game.gameWorld.whiteBall.position.x;
this.position.y = Game.gameWorld.whiteBall.position.y;
this.origin = new Vector2(970,11);
this.shooting = false;
this.visible = true;
this.power = 0;
};
Stick.prototype.draw = function () {
if(!this.visible)
return;
Canvas2D.drawImage(sprites.stick, this.position,this.rotation,1, this.origin);
};script/geom/Vector2.js
"use strict";
function Vector2(x, y) {
this.x = typeof x !== 'undefined' ? x : 0;
this.y = typeof y !== 'undefined' ? y : 0;
}
Object.defineProperty(Vector2, "zero",
{
get: function () {
return new Vector2();
}
});
Object.defineProperty(Vector2.prototype, "isZero",
{
get: function () {
return this.x === 0 && this.y === 0;
}
});
Object.defineProperty(Vector2.prototype, "length",
{
get: function () {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
});
Vector2.prototype.addTo = function (v) {
if (v.constructor === Vector2) {
this.x += v.x;
this.y += v.y;
}
else if (v.constructor === Number) {
this.x += v;
this.y += v;
}
return this;
};
Vector2.prototype.add = function (v) {
var result = this.copy();
return result.addTo(v);
};
Vector2.prototype.subtractFrom = function (v) {
if (v.constructor === Vector2) {
this.x -= v.x;
this.y -= v.y;
}
else if (v.constructor === Number) {
this.x -= v;
this.y -= v;
}
return this;
};
Vector2.prototype.subtract = function (v) {
var result = this.copy();
return result.subtractFrom(v);
};
Vector2.prototype.divideBy = function (v) {
if (v.constructor === Vector2) {
this.x /= v.x;
this.y /= v.y;
}
else if (v.constructor === Number) {
this.x /= v;
this.y /= v;
}
return this;
};
Vector2.prototype.divide = function (v) {
var result = this.copy();
return result.divideBy(v);
};
Vector2.prototype.multiplyWith = function (v) {
if (v.constructor === Vector2) {
this.x *= v.x;
this.y *= v.y;
}
else if (v.constructor === Number) {
this.x *= v;
this.y *= v;
}
return this;
};
Vector2.prototype.multiply = function (v) {
var result = this.copy();
return result.multiplyWith(v);
};
Vector2.prototype.toString = function () {
return "(" + this.x + ", " + this.y + ")";
};
Vector2.prototype.normalize = function () {
var length = this.length;
if (length === 0)
return;
this.divideBy(length);
};
Vector2.prototype.copy = function () {
return new Vector2(this.x, this.y);
};
Vector2.prototype.equals = function (obj) {
return this.x === obj.x && this.y === obj.y;
};
Vector2.prototype.distanceFrom = function(obj){
return Math.sqrt((this.x-obj.x)*(this.x-obj.x) + (this.y-obj.y)*(this.y-obj.y));
}script/input/ButtonState.js
"use strict";
function ButtonState() {
this.down = false;
this.pressed = false;
}script/input/Keyboard.js
"use strict";
function handleKeyDown(evt) {
var code = evt.keyCode;
if (code < 0 || code > 255)
return;
if (!Keyboard._keyStates[code].down)
Keyboard._keyStates[code].pressed = true;
Keyboard._keyStates[code].down = true;
}
function handleKeyUp(evt) {
var code = evt.keyCode;
if (code < 0 || code > 255)
return;
Keyboard._keyStates[code].down = false;
}
function Keyboard_Singleton() {
this._keyStates = [];
for (var i = 0; i < 256; ++i)
this._keyStates.push(new ButtonState());
document.onkeydown = handleKeyDown;
document.onkeyup = handleKeyUp;
}
Keyboard_Singleton.prototype.reset = function () {
for (var i = 0; i < 256; ++i)
this._keyStates[i].pressed = false;
};
Keyboard_Singleton.prototype.pressed = function (key) {
return this._keyStates[key].pressed;
};
Keyboard_Singleton.prototype.down = function (key) {
return this._keyStates[key].down;
};
var Keyboard = new Keyboard_Singleton();script/input/Mouse.js
"use strict";
function handleMouseMove(evt) {
var canvasScale = Canvas2D.scale;
var canvasOffset = Canvas2D.offset;
var mx = (evt.pageX - canvasOffset.x) / canvasScale.x;
var my = (evt.pageY - canvasOffset.y) / canvasScale.y;
Mouse._position = new Vector2(mx, my);
}
function handleMouseDown(evt) {
handleMouseMove(evt);
if (evt.which === 1) {
if (!Mouse._left.down)
Mouse._left.pressed = true;
Mouse._left.down = true;
} else if (evt.which === 2) {
if (!Mouse._middle.down)
Mouse._middle.pressed = true;
Mouse._middle.down = true;
} else if (evt.which === 3) {
if (!Mouse._right.down)
Mouse._right.pressed = true;
Mouse._right.down = true;
}
}
function handleMouseUp(evt) {
handleMouseMove(evt);
if (evt.which === 1)
Mouse._left.down = false;
else if (evt.which === 2)
Mouse._middle.down = false;
else if (evt.which === 3)
Mouse._right.down = false;
}
function Mouse_Singleton() {
this._position = Vector2.zero;
this._left = new ButtonState();
this._middle = new ButtonState();
this._right = new ButtonState();
document.onmousemove = handleMouseMove;
document.onmousedown = handleMouseDown;
document.onmouseup = handleMouseUp;
}
Object.defineProperty(Mouse_Singleton.prototype, "left",
{
get: function () {
return this._left;
}
});
Object.defineProperty(Mouse_Singleton.prototype, "middle",
{
get: function () {
return this._middle;
}
});
Object.defineProperty(Mouse_Singleton.prototype, "right",
{
get: function () {
return this._right;
}
});
Object.defineProperty(Mouse_Singleton.prototype, "position",
{
get: function () {
return this._position;
}
});
Mouse_Singleton.prototype.reset = function () {
this._left.pressed = false;
this._middle.pressed = false;
this._right.pressed = false;
};
Mouse_Singleton.prototype.containsMouseDown = function (rect) {
return this._left.down && rect.contains(this._position);
};
Mouse_Singleton.prototype.containsMousePress = function (rect) {
return this._left.pressed && rect.contains(this._position);
};
var Mouse = new Mouse_Singleton();script/lib/LAB.min.js
/*! LAB.js (LABjs :: Loading And Blocking JavaScript)
v2.0.3 (c) Kyle Simpson
MIT License
*/
(function(o){var K=o.$LAB,y="UseLocalXHR",z="AlwaysPreserveOrder",u="AllowDuplicates",A="CacheBust",B="BasePath",C=/^[^?#]*\//.exec(location.href)[0],D=/^\w+\:\/\/\/?[^\/]+/.exec(C)[0],i=document.head||document.getElementsByTagName("head"),L=(o.opera&&Object.prototype.toString.call(o.opera)=="[object Opera]")||("MozAppearance"in document.documentElement.style),q=document.createElement("script"),E=typeof q.preload=="boolean",r=E||(q.readyState&&q.readyState=="uninitialized"),F=!r&&q.async===true,M=!r&&!F&&!L;function G(a){return Object.prototype.toString.call(a)=="[object Function]"}function H(a){return Object.prototype.toString.call(a)=="[object Array]"}function N(a,c){var b=/^\w+\:\/\//;if(/^\/\/\/?/.test(a)){a=location.protocol+a}else if(!b.test(a)&&a.charAt(0)!="/"){a=(c||"")+a}return b.test(a)?a:((a.charAt(0)=="/"?D:C)+a)}function s(a,c){for(var b in a){if(a.hasOwnProperty(b)){c[b]=a[b]}}return c}function O(a){var c=false;for(var b=0;b<a.scripts.length;b++){if(a.scripts[b].ready&&a.scripts[b].exec_trigger){c=true;a.scripts[b].exec_trigger();a.scripts[b].exec_trigger=null}}return c}function t(a,c,b,d){a.onload=a.onreadystatechange=function(){if((a.readyState&&a.readyState!="complete"&&a.readyState!="loaded")||c[b])return;a.onload=a.onreadystatechange=null;d()}}function I(a){a.ready=a.finished=true;for(var c=0;c<a.finished_listeners.length;c++){a.finished_listeners[c]()}a.ready_listeners=[];a.finished_listeners=[]}function P(d,f,e,g,h){setTimeout(function(){var a,c=f.real_src,b;if("item"in i){if(!i[0]){setTimeout(arguments.callee,25);return}i=i[0]}a=document.createElement("script");if(f.type)a.type=f.type;if(f.charset)a.charset=f.charset;if(h){if(r){e.elem=a;if(E){a.preload=true;a.onpreload=g}else{a.onreadystatechange=function(){if(a.readyState=="loaded")g()}}a.src=c}else if(h&&c.indexOf(D)==0&&d[y]){b=new XMLHttpRequest();b.onreadystatechange=function(){if(b.readyState==4){b.onreadystatechange=function(){};e.text=b.responseText+"\n//@ sourceURL="+c;g()}};b.open("GET",c);b.send()}else{a.type="text/cache-script";t(a,e,"ready",function(){i.removeChild(a);g()});a.src=c;i.insertBefore(a,i.firstChild)}}else if(F){a.async=false;t(a,e,"finished",g);a.src=c;i.insertBefore(a,i.firstChild)}else{t(a,e,"finished",g);a.src=c;i.insertBefore(a,i.firstChild)}},0)}function J(){var l={},Q=r||M,n=[],p={},m;l[y]=true;l[z]=false;l[u]=false;l[A]=false;l[B]="";function R(a,c,b){var d;function f(){if(d!=null){d=null;I(b)}}if(p[c.src].finished)return;if(!a[u])p[c.src].finished=true;d=b.elem||document.createElement("script");if(c.type)d.type=c.type;if(c.charset)d.charset=c.charset;t(d,b,"finished",f);if(b.elem){b.elem=null}else if(b.text){d.onload=d.onreadystatechange=null;d.text=b.text}else{d.src=c.real_src}i.insertBefore(d,i.firstChild);if(b.text){f()}}function S(c,b,d,f){var e,g,h=function(){b.ready_cb(b,function(){R(c,b,e)})},j=function(){b.finished_cb(b,d)};b.src=N(b.src,c[B]);b.real_src=b.src+(c[A]?((/\?.*$/.test(b.src)?"&_":"?_")+~~(Math.random()*1E9)+"="):"");if(!p[b.src])p[b.src]={items:[],finished:false};g=p[b.src].items;if(c[u]||g.length==0){e=g[g.length]={ready:false,finished:false,ready_listeners:[h],finished_listeners:[j]};P(c,b,e,((f)?function(){e.ready=true;for(var a=0;a<e.ready_listeners.length;a++){e.ready_listeners[a]()}e.ready_listeners=[]}:function(){I(e)}),f)}else{e=g[0];if(e.finished){j()}else{e.finished_listeners.push(j)}}}function v(){var e,g=s(l,{}),h=[],j=0,w=false,k;function T(a,c){a.ready=true;a.exec_trigger=c;x()}function U(a,c){a.ready=a.finished=true;a.exec_trigger=null;for(var b=0;b<c.scripts.length;b++){if(!c.scripts[b].finished)return}c.finished=true;x()}function x(){while(j<h.length){if(G(h[j])){try{h[j++]()}catch(err){}continue}else if(!h[j].finished){if(O(h[j]))continue;break}j++}if(j==h.length){w=false;k=false}}function V(){if(!k||!k.scripts){h.push(k={scripts:[],finished:true})}}e={script:function(){for(var f=0;f<arguments.length;f++){(function(a,c){var b;if(!H(a)){c=[a]}for(var d=0;d<c.length;d++){V();a=c[d];if(G(a))a=a();if(!a)continue;if(H(a)){b=[].slice.call(a);b.unshift(d,1);[].splice.apply(c,b);d--;continue}if(typeof a=="string")a={src:a};a=s(a,{ready:false,ready_cb:T,finished:false,finished_cb:U});k.finished=false;k.scripts.push(a);S(g,a,k,(Q&&w));w=true;if(g[z])e.wait()}})(arguments[f],arguments[f])}return e},wait:function(){if(arguments.length>0){for(var a=0;a<arguments.length;a++){h.push(arguments[a])}k=h[h.length-1]}else k=false;x();return e}};return{script:e.script,wait:e.wait,setOptions:function(a){s(a,g);return e}}}m={setGlobalDefaults:function(a){s(a,l);return m},setOptions:function(){return v().setOptions.apply(null,arguments)},script:function(){return v().script.apply(null,arguments)},wait:function(){return v().wait.apply(null,arguments)},queueScript:function(){n[n.length]={type:"script",args:[].slice.call(arguments)};return m},queueWait:function(){n[n.length]={type:"wait",args:[].slice.call(arguments)};return m},runQueue:function(){var a=m,c=n.length,b=c,d;for(;--b>=0;){d=n.shift();a=a[d.type].apply(null,d.args)}return a},noConflict:function(){o.$LAB=K;return m},sandbox:function(){return J()}};return m}o.$LAB=J();(function(a,c,b){if(document.readyState==null&&document[a]){document.readyState="loading";document[a](c,b=function(){document.removeEventListener(c,b,false);document.readyState="complete"},false)}})("addEventListener","DOMContentLoaded")})(this);script/menu/Button.js
function Button(sprite, position, callback, hoverSprite){
this.sprite = sprite;
this.hoverSprite = hoverSprite ? hoverSprite : sprite;
this.position = position;
this.callback = callback;
}
Button.prototype.draw = function(){
if(this.mouseInsideBorders()){
Canvas2D.drawImage(this.hoverSprite, this.position, 0, 1);
Canvas2D._canvas.style.cursor = "pointer";
}
else{
Canvas2D.drawImage(this.sprite, this.position, 0, 0.98);
}
}
Button.prototype.handleInput = function(){
if(Mouse.left.pressed && this.mouseInsideBorders()){
this.callback();
}
}
Button.prototype.mouseInsideBorders = function(){
mousePos = Mouse.position;
if(mousePos.x > this.position.x
&&
mousePos.x < this.position.x + this.sprite.width
&&
mousePos.y > this.position.y
&&
mousePos.y < this.position.y + this.sprite.height
){
return true;
}
return false;
}script/menu/Label.js
function Label(text, position, origin, color, textAlign, fontname, fontsize){
this.text = typeof text !== 'undefined' ? text : '';
this.position = typeof position !== 'undefined' ? position : Vector2.zero;
this.origin = typeof origin !== 'undefined' ? origin : Vector2.zero;
this.color = typeof color !== 'undefined' ? color : Color.black;
this.textAlign = typeof textAlign !== 'undefined' ? textAlign : "top";
this.fontname = typeof fontname !== 'undefined' ? fontname : "Courier New";
this.fontsize = typeof fontsize !== 'undefined' ? fontsize : "20px";
}
Label.prototype.draw = function(){
Canvas2D.drawText(
this.text,
this.position,
this.origin,
this.color,
this.textAlign,
this.fontname,
this.fontsize
);
}script/menu/MainMenu.js
function generateMainMenuLabels(headerText){
let labels = [
new Label(
headerText,
new Vector2(100,0),
Vector2.zero,
"white",
"left",
"Bookman",
"100px"
),
new Label(
"© 2018 Chen Shmilovich",
new Vector2(1250,700),
Vector2.zero,
"white",
"left",
"Bookman",
"20px"
)
];
return labels;
}
function generateMainMenuButtons(inGame){
let buttons = [];
let dev = 0;
if(inGame){
dev = 200;
buttons.push(
new Button
(
// CONTINUE BUTTON
sprites.continueButton,
new Vector2(200,200),
function(){
Game.mainMenu.active = false;
GAME_STOPPED = false;
setTimeout(Game.continueGame,200);
sounds.fadeOut(Game.mainMenu.sound);
},
sprites.continueButtonHover
)
)
}
let muteSprite = sprites.muteButton;
let muteSpriteHover = sprites.muteButtonHover;
if(Game.mainMenu.sound && Game.mainMenu.sound.volume === 0){
muteSprite = sprites.muteButtonPressed;
muteSpriteHover = sprites.muteButtonPressedHover;
}
let muteButton = new Button
(
// MUTE BUTTON
muteSprite,
new Vector2(1430,10),
function(){
if(Game.mainMenu.sound.volume == 0){
SOUND_ON = true;
Game.mainMenu.sound.volume = 0.8;
this.sprite = sprites.muteButton;
this.hoverSprite = sprites.muteButtonHover;
}
else{
SOUND_ON = false;
Game.mainMenu.sound.volume = 0.0;
this.sprite = sprites.muteButtonPressed;
this.hoverSprite = sprites.muteButtonPressedHover;
}
},
muteSpriteHover
);
let backButton = new Button
(
//BACK
sprites.backButton,
new Vector2(100,150),
function(){
Game.mainMenu.labels = generateMainMenuLabels("Classic 8-Ball");
Game.mainMenu.buttons = generateMainMenuButtons(inGame);
},
sprites.backButtonHover
);
buttons = buttons.concat([
new Button
(
// PLAYER vs PLAYER
sprites.twoPlayersButton,
new Vector2(200,dev+200),
function(){
AI_ON = false;
Game.mainMenu.active = false;
GAME_STOPPED = false;
setTimeout(Game.startNewGame,200);
sounds.fadeOut(Game.mainMenu.sound);
},
sprites.twoPlayersButtonHover
),
new Button
(
// PLAYER vs COMPUTER
sprites.onePlayersButton,
new Vector2(200,dev+400),
function(){
Game.mainMenu.labels = generateMainMenuLabels("Choose Difficulty");
Mouse.reset();
Game.mainMenu.buttons = [
new Button
(
//EASY
sprites.easyButton,
new Vector2(200,150),
function(){
AI_PLAYER_NUM = 1;
AI_ON = true;
TRAIN_ITER = 30;
Game.mainMenu.active = false;
GAME_STOPPED = false;
setTimeout(Game.startNewGame,200);
sounds.fadeOut(Game.mainMenu.sound);
},
sprites.easyButtonHover
),
new Button
(
//MEDIUM
sprites.mediumButton,
new Vector2(200,300),
function(){
AI_PLAYER_NUM = 1;
AI_ON = true;
TRAIN_ITER = 50;
Game.mainMenu.active = false;
GAME_STOPPED = false;
setTimeout(Game.startNewGame,200);
sounds.fadeOut(Game.mainMenu.sound);
},
sprites.mediumButtonHover
),
new Button
(
//HARD
sprites.hardButton,
new Vector2(200,450),
function(){
AI_PLAYER_NUM = 1;
AI_ON = true;
TRAIN_ITER = 100;
Game.mainMenu.active = false;
GAME_STOPPED = false;
setTimeout(Game.startNewGame,200);
sounds.fadeOut(Game.mainMenu.sound);
},
sprites.hardButtonHover
),
new Button
(
//INSANE
sprites.insaneButton,
new Vector2(200,600),
function(){
AI_PLAYER_NUM = 0;
AI_ON = true;
TRAIN_ITER = 700;
Game.mainMenu.active = false;
GAME_STOPPED = false;
setTimeout(Game.startNewGame,200);
sounds.fadeOut(Game.mainMenu.sound);
},
sprites.insaneButtonHover
),
muteButton,
backButton
];
},
sprites.onePlayersButtonHover
),
muteButton
]);
return buttons;
}script/menu/Menu.js
function Menu(){
}
Menu.prototype.init = function
(
backgroundSprite,
labels,
buttons,
sound
){
this.background = backgroundSprite;
this.labels = labels || [];
this.buttons = buttons || [];
this.sound = sound ? sound : undefined;
this.active = false;
}
Menu.prototype.load = function(){
this.sound.currentTime = 0;
this.active = true;
requestAnimationFrame(this.menuLoop.bind(this));
if(SOUND_ON){
this.sound.volume = 0.8;
}
this.sound.play();
}
Menu.prototype.draw = function(){
Canvas2D._canvas.style.cursor = "auto";
Canvas2D.drawImage(
this.background,
Vector2.zero,
0,
1,
Vector2.zero
);
for(let i = 0 ; i < this.labels.length ; i++){
this.labels[i].draw();
}
for(let i = 0 ; i < this.buttons.length ; i++){
this.buttons[i].draw();
}
}
Menu.prototype.handleInput = function(){
for(let i = 0 ; i < this.buttons.length ; i++){
this.buttons[i].handleInput();
}
}
Menu.prototype.menuLoop = function(){
if(this.active){
this.handleInput();
Canvas2D.clear();
this.draw();
Mouse.reset();
requestAnimationFrame(this.menuLoop.bind(this));
}
}
script/system/Color.js
"use strict";
var Color = {
aliceBlue: "#F0F8FF",
antiqueWhite: "#FAEBD7",
aqua: "#00FFFF",
aquamarine: "#7FFFD4",
azure: "#F0FFFF",
beige: "#F5F5DC",
bisque: "#FFE4C4",
black: "#000000",
blanchedAlmond: "#FFEBCD",
blue: "#0000FF",
blueViolet: "#8A2BE2",
brown: "#A52A2A",
burlyWood: "#DEB887",
cadetBlue: "#5F9EA0",
chartreuse: "#7FFF00",
chocolate: "#D2691E",
coral: "#FF7F50",
cornflowerBlue: "#6495ED",
cornsilk: "#FFF8DC",
crimson: "#DC143C",
cyan: "#00FFFF",
darkBlue: "#00008B",
darkCyan: "#008B8B",
darkGoldenrod: "#B8860B",
darkGray: "#A9A9A9",
darkGreen: "#006400",
darkKhaki: "#BDB76B",
darkMagenta: "#8B008B",
darkOliveGreen: "#556B2F",
darkOrange: "#FF8C00",
darkOrchid: "#9932CC",
darkRed: "#8B0000",
darkSalmon: "#E9967A",
darkSeaGreen: "#8FBC8B",
darkSlateBlue: "#483D8B",
darkSlateGray: "#2F4F4F",
darkTurquoise: "#00CED1",
darkViolet: "#9400D3",
deepPink: "#FF1493",
deepSkyBlue: "#00BFFF",
dimGray: "#696969",
dodgerBlue: "#1E90FF",
firebrick: "#B22222",
floralWhite: "#FFFAF0",
forestGreen: "#228B22",
fuchsia: "#FF00FF",
gainsboro: "#DCDCDC",
ghostWhite: "#F8F8FF",
gold: "#FFD700",
goldenrod: "#DAA520",
gray: "#808080",
green: "#008000",
greenYellow: "#ADFF2F",
honeydew: "#F0FFF0",
hotPink: "#FF69B4",
indianRed: "#CD5C5C",
indigo: "#4B0082",
ivory: "#FFFFF0",
khaki: "#F0E68C",
lavender: "#E6E6FA",
lavenderBlush: "#FFF0F5",
lawnGreen: "#7CFC00",
lemonChiffon: "#FFFACD",
lightBlue: "#ADD8E6",
lightCoral: "#F080FF",
lightCyan: "#E0FFFF",
lightGoldenrodYellow: "#FAFAD2",
lightGray: "#D3D3D3",
lightGreen: "#90EE90",
lightPink: "#FFB6C1",
lightSalmon: "#FFA07A",
lightSeaGreen: "#20B2AA",
lightSkyBlue: "#87CEFA",
lightSlateGray: "#778899",
lightSteelBlue: "#B0C4DE",
lightYellow: "#FFFFE0",
lime: "#00FF00",
limeGreen: "#32CD32",
linen: "#FAF0E6",
magenta: "#FF00FF",
maroon: "#800000",
mediumAquamarine: "#66CDAA",
mediumBlue: "#0000CD",
mediumOrchid: "#BA55D3",
mediumPurple: "#9370DB",
mediumSeaGreen: "#3CB371",
mediumSlateBlue: "#7B68EE",
mediumSpringGreen: "#00FA9A",
mediumTurquoise: "#48D1CC",
mediumVioletRed: "#C71585",
midnightBlue: "#191970",
mintCream: "#F5FFFA",
mistyRose: "#FFE4E1",
moccasin: "#FFE4B5",
navajoWhite: "#FFDEAD",
navy: "#000080",
oldLace: "#FDF5E6",
olive: "#808000",
oliveDrab: "#6B8E23",
orange: "#FFA500",
orangeRed: "#FF4500",
orchid: "#DA70D6",
paleGoldenrod: "#EEE8AA",
paleGreen: "#98FB98",
paleTurquoise: "#AFEEEE",
paleVioletRed: "#DB7093",
papayaWhip: "#FFEFD5",
peachPuff: "#FFDAB9",
peru: "#CD853F",
pink: "#FFC0CB",
plum: "#DDA0DD",
powderBlue: "#B0E0E6",
purple: "#800080",
red: "#FF0000",
rosyBrown: "#BC8F8F",
royalBlue: "#4169E1",
saddleBrown: "#8B4513",
salmon: "#FA8072",
sandyBrown: "#F4A460",
seaGreen: "#2E8B57",
seaShell: "#FFF5EE",
sienna: "#A0522D",
silver: "#C0C0C0",
skyBlue: "#87CEEB",
slateBlue: "#6A5ACD",
slateGray: "#708090",
snow: "#FFFAFA",
springGreen: "#00FF7F",
steelBlue: "#4682B4",
tan: "#D2B48C",
teal: "#008080",
thistle: "#D8BFD8",
tomato: "#FF6347",
turquoise: "#40E0D0",
violet: "#EE82EE",
wheat: "#F5DEB3",
white: "#FFFFFF",
whiteSmoke: "#F5F5F5",
yellow: "#FFFF00",
yellowGreen: "#9ACD32"
};script/system/Keys.js
"use strict";
var Keys = {
none: 0,
back: 8,
tab: 9,
enter: 13,
pause: 19,
escape: 27,
space: 32,
pageUp: 33,
pageDown: 34,
end: 35,
home: 36,
left: 37,
up: 38,
right: 39,
down: 40,
insert: 45,
del: 46,
d0: 48,
d1: 49,
d2: 50,
d3: 51,
d4: 52,
d5: 53,
d6: 54,
d7: 55,
d8: 56,
d9: 57,
A: 65, B: 66, C: 67, D: 68, E: 69, F: 70,
G: 71, H: 72, I: 73, J: 74, K: 75, L: 76,
M: 77, N: 78, O: 79, P: 80, Q: 81, R: 82,
S: 83, T: 84, U: 85, V: 86, W: 87, X: 88,
Y: 89, Z: 90,
multiply: 42,
add: 43,
subtract: 45,
decimal: 46,
divide: 47
};script/Assets.js
"use strict";
var sprites = {};
var sounds = {};
Game.loadAssets = function () {
var loadSprite = function (sprite) {
return Game.loadSprite("assets/sprites/" + sprite);
};
var loadSound = function (sound) {
return new Audio("assets/sounds/" + sound);
};
sprites.mainMenuBackground = loadSprite("main_menu_background.png");
sprites.background = loadSprite("spr_background4.png");
sprites.ball = loadSprite("spr_ball2.png");
sprites.redBall = loadSprite("spr_redBall2.png");
sprites.yellowBall = loadSprite("spr_yellowBall2.png");
sprites.blackBall = loadSprite("spr_blackBall2.png");
sprites.stick = loadSprite("spr_stick.png");
sprites.twoPlayersButton = loadSprite("2_players_button.png");
sprites.twoPlayersButtonHover = loadSprite("2_players_button_hover.png");
sprites.onePlayersButton = loadSprite("1_player_button.png");
sprites.onePlayersButtonHover = loadSprite("1_player_button_hover.png");
sprites.muteButton = loadSprite("mute_button.png");
sprites.muteButtonHover = loadSprite("mute_button_hover.png");
sprites.muteButtonPressed = loadSprite("mute_button_pressed.png");
sprites.muteButtonPressedHover = loadSprite("mute_button_pressed_hover.png");
sprites.easyButton = loadSprite("easy_button.png");
sprites.easyButtonHover = loadSprite("easy_button_hover.png");
sprites.mediumButton = loadSprite("medium_button.png");
sprites.mediumButtonHover = loadSprite("medium_button_hover.png");
sprites.hardButton = loadSprite("hard_button.png");
sprites.hardButtonHover = loadSprite("hard_button_hover.png");
sprites.backButton = loadSprite("back_button.png");
sprites.backButtonHover = loadSprite("back_button_hover.png");
sprites.continueButton = loadSprite("continue_button.png");
sprites.continueButtonHover = loadSprite("continue_button_hover.png");
sprites.insaneButton = loadSprite("insane_button.png");
sprites.insaneButtonHover = loadSprite("insane_button_hover.png");
sprites.aboutButton = loadSprite("about_button.png");
sprites.aboutButtonHover = loadSprite("about_button_hover.png");
sprites.controls = loadSprite("controls.png");
sounds.side = loadSound("Side.wav");
sounds.ballsCollide = loadSound("BallsCollide.wav");
sounds.strike = loadSound("Strike.wav");
sounds.hole = loadSound("Hole.wav");
// Bossa Antigua Kevin MacLeod (incompetech.com)
// Licensed under Creative Commons: By Attribution 3.0 License
// http://creativecommons.org/licenses/by/3.0/
sounds.jazzTune = loadSound("Bossa Antigua.mp3");
}
sounds.fadeOut = function(sound) {
var fadeAudio = setInterval(function () {
if(GAME_STOPPED)
return;
// Only fade if past the fade out point or not at zero already
if ((sound.volume >= 0.05)) {
sound.volume -= 0.05;
}
else{
sound.pause();
clearInterval(fadeAudio);
}
}, 400);
}script/Canvas2D.js
"use strict";
function Canvas2D_Singleton() {
this._canvas = null;
this._canvasContext = null;
this._canvasOffset = Vector2.zero;
}
Object.defineProperty(Canvas2D_Singleton.prototype, "offset",
{
get: function () {
return this._canvasOffset;
}
});
Object.defineProperty(Canvas2D_Singleton.prototype, "scale",
{
get: function () {
return new Vector2(this._canvas.width / Game.size.x,
this._canvas.height / Game.size.y);
}
});
Canvas2D_Singleton.prototype.initialize = function (divName, canvasName) {
this._canvas = document.getElementById(canvasName);
this._div = document.getElementById(divName);
if (this._canvas.getContext)
this._canvasContext = this._canvas.getContext('2d');
else {
alert('Your browser is not HTML5 compatible.!');
return;
}
window.onresize = Canvas2D_Singleton.prototype.resize;
this.resize();
};
Canvas2D_Singleton.prototype.clear = function () {
this._canvasContext.clearRect(0, 0, this._canvas.width, this._canvas.height);
};
Canvas2D_Singleton.prototype.resize = function () {
var gameCanvas = Canvas2D._canvas;
var gameArea = Canvas2D._div;
var widthToHeight = Game.size.x / Game.size.y;
var newWidth = window.innerWidth;
var newHeight = window.innerHeight;
var newWidthToHeight = newWidth / newHeight;
if (newWidthToHeight > widthToHeight) {
newWidth = newHeight * widthToHeight;
} else {
newHeight = newWidth / widthToHeight;
}
gameArea.style.width = newWidth + 'px';
gameArea.style.height = newHeight + 'px';
gameArea.style.marginTop = (window.innerHeight - newHeight) / 2 + 'px';
gameArea.style.marginLeft = (window.innerWidth - newWidth) / 2 + 'px';
gameArea.style.marginBottom = (window.innerHeight - newHeight) / 2 + 'px';
gameArea.style.marginRight = (window.innerWidth - newWidth) / 2 + 'px';
gameCanvas.width = newWidth;
gameCanvas.height = newHeight;
var offset = Vector2.zero;
if (gameCanvas.offsetParent) {
do {
offset.x += gameCanvas.offsetLeft;
offset.y += gameCanvas.offsetTop;
} while ((gameCanvas = gameCanvas.offsetParent));
}
Canvas2D._canvasOffset = offset;
};
Canvas2D_Singleton.prototype.drawImage = function (sprite, position, rotation, scale, origin) {
var canvasScale = this.scale;
position = typeof position !== 'undefined' ? position : Vector2.zero;
rotation = typeof rotation !== 'undefined' ? rotation : 0;
scale = typeof scale !== 'undefined' ? scale : 1;
origin = typeof origin !== 'undefined' ? origin : Vector2.zero;
this._canvasContext.save();
this._canvasContext.scale(canvasScale.x, canvasScale.y);
this._canvasContext.translate(position.x, position.y);
this._canvasContext.rotate(rotation);
this._canvasContext.drawImage(sprite, 0, 0,
sprite.width, sprite.height,
-origin.x * scale, -origin.y * scale,
sprite.width * scale, sprite.height * scale);
this._canvasContext.restore();
};
Canvas2D_Singleton.prototype.drawText = function (text, position, origin, color, textAlign, fontname, fontsize) {
var canvasScale = this.scale;
position = typeof position !== 'undefined' ? position : Vector2.zero;
origin = typeof origin !== 'undefined' ? origin : Vector2.zero;
color = typeof color !== 'undefined' ? color : Color.black;
textAlign = typeof textAlign !== 'undefined' ? textAlign : "top";
fontname = typeof fontname !== 'undefined' ? fontname : "sans-serif";
fontsize = typeof fontsize !== 'undefined' ? fontsize : "20px";
this._canvasContext.save();
this._canvasContext.scale(canvasScale.x, canvasScale.y);
this._canvasContext.translate(position.x - origin.x, position.y - origin.y);
this._canvasContext.textBaseline = 'top';
this._canvasContext.font = fontsize + " " + fontname;
this._canvasContext.fillStyle = color.toString();
this._canvasContext.textAlign = textAlign;
this._canvasContext.fillText(text, 0, 0);
this._canvasContext.restore();
};
var Canvas2D = new Canvas2D_Singleton();
script/Game.js
"use strict";
var requestAnimationFrame = (function () {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
function Game_Singleton() {
this.size = undefined;
this.spritesStillLoading = 0;
this.gameWorld = undefined;
this.sound = true;
this.mainMenu = new Menu();
}
Game_Singleton.prototype.start = function (divName, canvasName, x, y) {
this.size = new Vector2(x,y);
Canvas2D.initialize(divName, canvasName);
this.loadAssets();
this.assetLoadingLoop();
};
Game_Singleton.prototype.initialize = function () {
this.gameWorld = new GameWorld();
this.policy = new GamePolicy();
this.initMenus();
AI.init(this.gameWorld, this.policy);
};
Game_Singleton.prototype.initMenus = function(inGame){
let labels = generateMainMenuLabels("Classic 8-Ball");
let buttons = generateMainMenuButtons(inGame);
this.mainMenu.init
(
sprites.mainMenuBackground,
labels,
buttons,
sounds.jazzTune
);
}
Game_Singleton.prototype.loadSprite = function (imageName) {
console.log("Loading sprite: " + imageName);
var image = new Image();
image.src = imageName;
this.spritesStillLoading += 1;
image.onload = function () {
Game.spritesStillLoading -= 1;
};
return image;
};
Game_Singleton.prototype.assetLoadingLoop = function () {
if (!this.spritesStillLoading > 0)
requestAnimationFrame(Game.assetLoadingLoop);
else {
Game.initialize();
requestAnimationFrame(this.mainMenu.load.bind(this.mainMenu));
}
};
Game_Singleton.prototype.handleInput = function(){
if(Keyboard.down(Keys.escape)){
GAME_STOPPED = true;
Game.initMenus(true);
requestAnimationFrame(Game.mainMenu.load.bind(this.mainMenu));
}
}
Game_Singleton.prototype.startNewGame = function(){
Canvas2D._canvas.style.cursor = "auto";
Game.gameWorld = new GameWorld();
Game.policy = new GamePolicy();
Canvas2D.clear();
Canvas2D.drawImage(
sprites.controls,
new Vector2(Game.size.x/2,Game.size.y/2),
0,
1,
new Vector2(sprites.controls.width/2,sprites.controls.height/2)
);
setTimeout(()=>{
AI.init(Game.gameWorld, Game.policy);
if(AI_ON && AI_PLAYER_NUM == 0){
AI.startSession();
}
Game.mainLoop();
},5000);
}
Game_Singleton.prototype.continueGame = function(){
Canvas2D._canvas.style.cursor = "auto";
requestAnimationFrame(Game.mainLoop);
}
Game_Singleton.prototype.mainLoop = function () {
if(DISPLAY && !GAME_STOPPED){
Game.gameWorld.handleInput(DELTA);
Game.gameWorld.update(DELTA);
Canvas2D.clear();
Game.gameWorld.draw();
Mouse.reset();
Game.handleInput();
requestAnimationFrame(Game.mainLoop);
}
};
var Game = new Game_Singleton();
script/GamePolicy.js
function GamePolicy(){
this.turn = 0;
this.firstCollision = true;
let player1TotalScore = new Score(new Vector2(Game.size.x/2 - 75,Game.size.y/2 - 45));
let player2TotalScore = new Score(new Vector2(Game.size.x/2 + 75,Game.size.y/2 - 45));
let player1MatchScore = new Score(new Vector2(Game.size.x/2 - 280,108));
let player2MatchScore = new Score(new Vector2(Game.size.x/2 + 230,108));
this.players = [new Player(player1MatchScore,player1TotalScore), new Player(player2MatchScore,player2TotalScore)];
this.foul = false;
this.scored = false;
this.won = false;
this.turnPlayed = false;
this.validBallsInsertedOnTurn = 0;
this.leftBorderX = BORDER_SIZE;
this.rightBorderX = Game.size.x - BORDER_SIZE;
this.topBorderY = BORDER_SIZE;
this.bottomBorderY = Game.size.y - BORDER_SIZE;
this.topCenterHolePos = new Vector2(750,32);
this.bottomCenterHolePos = new Vector2(750,794);
this.topLeftHolePos = new Vector2(62,62);
this.topRightHolePos = new Vector2(1435,62);
this.bottomLeftHolePos = new Vector2(62,762)
this.bottomRightHolePos = new Vector2(1435,762);
}
GamePolicy.prototype.reset = function(){
this.turn = 0;
this.players[0].matchScore.value = 0;
this.players[0].color = undefined;
this.players[1].matchScore.value = 0;
this.players[1].color = undefined;
this.foul = false;
this.scored = false;
this.turnPlayed = false;
this.won = false;
this.firstCollision = true;
this.validBallsInsertedOnTurn = 0;
}
GamePolicy.prototype.drawScores = function(){
Canvas2D.drawText("PLAYER " + (this.turn+1), new Vector2(Game.size.x/2 + 40,200), new Vector2(150,0), "#096834", "top", "Impact", "70px");
this.players[0].totalScore.draw();
this.players[1].totalScore.draw();
this.players[0].matchScore.drawLines(this.players[0].color);
this.players[1].matchScore.drawLines(this.players[1].color);
}
GamePolicy.prototype.checkColisionValidity = function(ball1,ball2){
let currentPlayerColor = this.players[this.turn].color;
if(this.players[this.turn].matchScore.value == 7 &&
(ball1.color == Color.black || ball2.color == Color.black)){
this.firstCollision = false;
return;
}
if(!this.firstCollision)
return;
if(currentPlayerColor == undefined){
this.firstCollision = false;
return;
}
if(ball1.color == Color.white){
if(ball2.color != currentPlayerColor){
this.foul = true;
}
this.firstCollision = false;
}
if(ball2.color == Color.white){
if(ball1.color != currentPlayerColor){
this.foul = true;
}
this.firstCollision = false;
}
}
GamePolicy.prototype.handleBallInHole = function(ball){
setTimeout(function(){ball.out();}, 100);
let currentPlayer = this.players[this.turn];
let secondPlayer = this.players[(this.turn+1)%2];
if(currentPlayer.color == undefined){
if(ball.color === Color.red){
currentPlayer.color = Color.red;
secondPlayer.color = Color.yellow;
}
else if(ball.color === Color.yellow){
currentPlayer.color = Color.yellow;
secondPlayer.color = Color.red;
}
else if(ball.color === Color.black){
this.won = true;
this.foul = true;
}
else if(ball.color === Color.white){
this.foul = true;
}
}
if(currentPlayer.color === ball.color){
currentPlayer.matchScore.increment();
this.scored = true;
this.validBallsInsertedOnTurn++;
}
else if(ball.color === Color.white){
if(currentPlayer.color != undefined){
this.foul = true;
let ballsSet = Game.gameWorld.getBallsSetByColor(currentPlayer.color);
let allBallsInHole = true;
for (var i = 0 ; i < ballsSet.length; i++){
if(!ballsSet[i].inHole){
allBallsInHole = false;
}
}
if(allBallsInHole){
this.won = true;
}
}
}
else if(ball.color === Color.black){
if(currentPlayer.color != undefined){
let ballsSet = Game.gameWorld.getBallsSetByColor(currentPlayer.color);
for (var i = 0 ; i < ballsSet.length; i++){
if(!ballsSet[i].inHole){
this.foul = true;
}
}
this.won = true;
}
}
else{
secondPlayer.matchScore.increment();
this.foul = true;
}
}
GamePolicy.prototype.switchTurns = function(){
this.turn++;
this.turn%=2;
}
GamePolicy.prototype.updateTurnOutcome = function(){
if(!this.turnPlayed){
return;
}
if(this.firstCollision == true){
this.foul = true;
}
if(this.won){
if(!this.foul){
this.players[this.turn].totalScore.increment();
if(AI.finishedSession){
this.reset()
setTimeout(function(){Game.gameWorld.reset();
}, 1000);
}
}
else{
this.players[(this.turn+1)%2].totalScore.increment();
if(AI.finishedSession){
this.reset();
setTimeout(function(){Game.gameWorld.reset();
}, 1000);
}
}
return;
}
if(!this.scored || this.foul)
this.switchTurns();
this.scored = false;
this.turnPlayed = false;
this.firstCollision = true;
this.validBallsInsertedOnTurn = 0;
setTimeout(function(){Game.gameWorld.whiteBall.visible=true;}, 200);
if(AI_ON && this.turn === AI_PLAYER_NUM && AI.finishedSession){
AI.startSession();
}
}
GamePolicy.prototype.handleFoul = function(){
if(!Mouse.left.down){
Game.gameWorld.whiteBall.position = Mouse.position;
}
}
GamePolicy.prototype.isXOutsideLeftBorder = function(pos, origin){
return (pos.x - origin.x) < this.leftBorderX;
}
GamePolicy.prototype.isXOutsideRightBorder = function(pos, origin){
return (pos.x + origin.x) > this.rightBorderX;
}
GamePolicy.prototype.isYOutsideTopBorder = function(pos, origin){
return (pos.y - origin.y) < this.topBorderY;
}
GamePolicy.prototype.isYOutsideBottomBorder = function(pos , origin){
return (pos.y + origin.y) > this.bottomBorderY;
}
GamePolicy.prototype.isOutsideBorder = function(pos,origin){
return this.isXOutsideLeftBorder(pos,origin) || this.isXOutsideRightBorder(pos,origin) ||
this.isYOutsideTopBorder(pos, origin) || this.isYOutsideBottomBorder(pos , origin);
}
GamePolicy.prototype.isInsideTopLeftHole = function(pos){
return this.topLeftHolePos.distanceFrom(pos) < HOLE_RADIUS;
}
GamePolicy.prototype.isInsideTopRightHole = function(pos){
return this.topRightHolePos.distanceFrom(pos) < HOLE_RADIUS;
}
GamePolicy.prototype.isInsideBottomLeftHole = function(pos){
return this.bottomLeftHolePos.distanceFrom(pos) < HOLE_RADIUS;
}
GamePolicy.prototype.isInsideBottomRightHole = function(pos){
return this.bottomRightHolePos.distanceFrom(pos) < HOLE_RADIUS;
}
GamePolicy.prototype.isInsideTopCenterHole = function(pos){
return this.topCenterHolePos.distanceFrom(pos) < (HOLE_RADIUS + 6);
}
GamePolicy.prototype.isInsideBottomCenterHole = function(pos){
return this.bottomCenterHolePos.distanceFrom(pos) < (HOLE_RADIUS + 6);
}
GamePolicy.prototype.isInsideHole = function(pos){
return this.isInsideTopLeftHole(pos) || this.isInsideTopRightHole(pos) ||
this.isInsideBottomLeftHole(pos) || this.isInsideBottomRightHole(pos) ||
this.isInsideTopCenterHole(pos) || this.isInsideBottomCenterHole(pos);
}
GamePolicy.prototype.initiateState = function(policyState){
this.turn = policyState.turn;
this.firstCollision = policyState.firstCollision;
this.foul = policyState.foul;
this.scored = policyState.scored;
this.won = policyState.won;
this.turnPlayed = policyState.turnPlayed;
this.validBallsInsertedOnTurn = policyState.validBallsInsertedOnTurn;
this.players[0].totalScore.value = policyState.players[0].totalScore.value;
this.players[1].totalScore.value = policyState.players[1].totalScore.value;
this.players[0].matchScore.value = policyState.players[0].matchScore.value;
this.players[0].color = policyState.players[0].color;
this.players[1].matchScore.value = policyState.players[1].matchScore.value;
this.players[1].color = policyState.players[1].color;
}script/GameWorld.js
"use strict";
function GameWorld() {
this.whiteBallStartingPosition = new Vector2(413,413);
this.redBalls = [
new Ball(new Vector2(1056,433),Color.red),//3
new Ball(new Vector2(1090,374),Color.red),//4
new Ball(new Vector2(1126,393),Color.red),//8
new Ball(new Vector2(1126,472),Color.red),//10;
new Ball(new Vector2(1162,335),Color.red),//11
new Ball(new Vector2(1162,374),Color.red),//12
new Ball(new Vector2(1162,452),Color.red)//14
]
this.yellowBalls = [
new Ball(new Vector2(1022,413),Color.yellow),//1
new Ball(new Vector2(1056,393),Color.yellow),//2
new Ball(new Vector2(1090,452),Color.yellow),//6
new Ball(new Vector2(1126,354),Color.yellow),//7
new Ball(new Vector2(1126,433),Color.yellow),//9
new Ball(new Vector2(1162,413),Color.yellow),//13
new Ball(new Vector2(1162,491),Color.yellow)//15
];
this.whiteBall = new Ball(new Vector2(413,413),Color.white);
this.blackBall = new Ball(new Vector2(1090,413),Color.black);
this.balls = [
this.yellowBalls[0],
this.yellowBalls[1],
this.redBalls[0],
this.redBalls[1],
this.blackBall,
this.yellowBalls[2],
this.yellowBalls[3],
this.redBalls[2],
this.yellowBalls[4],
this.redBalls[3],
this.redBalls[4],
this.redBalls[5],
this.yellowBalls[5],
this.redBalls[6],
this.yellowBalls[6],
this.whiteBall]
this.stick = new Stick({ x : 413, y : 413 });
this.gameOver = false;
}
GameWorld.prototype.getBallsSetByColor = function(color){
if(color === Color.red){
return this.redBalls;
}
if(color === Color.yellow){
return this.yellowBalls;
}
if(color === Color.white){
return this.whiteBall;
}
if(color === Color.black){
return this.blackBall;
}
}
GameWorld.prototype.handleInput = function (delta) {
this.stick.handleInput(delta);
};
GameWorld.prototype.update = function (delta) {
this.stick.update(delta);
for (var i = 0 ; i < this.balls.length; i++){
for(var j = i + 1 ; j < this.balls.length ; j++){
this.handleCollision(this.balls[i], this.balls[j], delta);
}
}
for (var i = 0 ; i < this.balls.length; i++) {
this.balls[i].update(delta);
}
if(!this.ballsMoving() && AI.finishedSession){
Game.policy.updateTurnOutcome();
if(Game.policy.foul){
this.ballInHand();
}
}
};
GameWorld.prototype.ballInHand = function(){
if(AI_ON && Game.policy.turn === AI_PLAYER_NUM){
return;
}
KEYBOARD_INPUT_ON = false;
this.stick.visible = false;
if(!Mouse.left.down){
this.whiteBall.position = Mouse.position;
}
else{
let ballsOverlap = this.whiteBallOverlapsBalls();
if(!Game.policy.isOutsideBorder(Mouse.position,this.whiteBall.origin) &&
!Game.policy.isInsideHole(Mouse.position) &&
!ballsOverlap){
KEYBOARD_INPUT_ON = true;
Keyboard.reset();
Mouse.reset();
this.whiteBall.position = Mouse.position;
this.whiteBall.inHole = false;
Game.policy.foul = false;
this.stick.position = this.whiteBall.position;
this.stick.visible = true;
}
}
}
GameWorld.prototype.whiteBallOverlapsBalls = function(){
let ballsOverlap = false;
for (var i = 0 ; i < this.balls.length; i++) {
if(this.whiteBall !== this.balls[i]){
if(this.whiteBall.position.distanceFrom(this.balls[i].position)<BALL_SIZE){
ballsOverlap = true;
}
}
}
return ballsOverlap;
}
GameWorld.prototype.ballsMoving = function(){
var ballsMoving = false;
for (var i = 0 ; i < this.balls.length; i++) {
if(this.balls[i].moving){
ballsMoving = true;
}
}
return ballsMoving;
}
GameWorld.prototype.handleCollision = function(ball1, ball2, delta){
if(ball1.inHole || ball2.inHole)
return;
if(!ball1.moving && !ball2.moving)
return;
var ball1NewPos = ball1.position.add(ball1.velocity.multiply(delta));
var ball2NewPos = ball2.position.add(ball2.velocity.multiply(delta));
var dist = ball1NewPos.distanceFrom(ball2NewPos);
if(dist<BALL_SIZE){
Game.policy.checkColisionValidity(ball1, ball2);
var power = (Math.abs(ball1.velocity.x) + Math.abs(ball1.velocity.y)) +
(Math.abs(ball2.velocity.x) + Math.abs(ball2.velocity.y));
power = power * 0.00482;
if(Game.sound && SOUND_ON){
var ballsCollide = sounds.ballsCollide.cloneNode(true);
ballsCollide.volume = (power/(20))<1?(power/(20)):1;
ballsCollide.play();
}
var opposite = ball1.position.y - ball2.position.y;
var adjacent = ball1.position.x - ball2.position.x;
var rotation = Math.atan2(opposite, adjacent);
ball1.moving = true;
ball2.moving = true;
var velocity2 = new Vector2(90*Math.cos(rotation + Math.PI)*power,90*Math.sin(rotation + Math.PI)*power);
ball2.velocity = ball2.velocity.addTo(velocity2);
ball2.velocity.multiplyWith(0.97);
var velocity1 = new Vector2(90*Math.cos(rotation)*power,90*Math.sin(rotation)*power);
ball1.velocity = ball1.velocity.addTo(velocity1);
ball1.velocity.multiplyWith(0.97);
}
}
GameWorld.prototype.draw = function () {
Canvas2D.drawImage(sprites.background);
Game.policy.drawScores();
for (var i = 0; i < this.balls.length; i++) {
this.balls[i].draw();
}
this.stick.draw();
};
GameWorld.prototype.reset = function () {
this.gameOver = false;
for (var i = 0; i < this.balls.length; i++) {
this.balls[i].reset();
}
this.stick.reset();
if(AI_ON && AI_PLAYER_NUM === 0){
AI.startSession();
}
};
GameWorld.prototype.initiateState = function(balls){
for (var i = 0; i < this.balls.length; i++) {
this.balls[i].position.x = balls[i].position.x;
this.balls[i].position.y = balls[i].position.y;
this.balls[i].visible = balls[i].visible;
this.balls[i].inHole = balls[i].inHole;
}
this.stick.position = this.whiteBall.position;
}
script/Global.js
const LOG = false; const BALL_SIZE = 38; const BORDER_SIZE = 57; const HOLE_RADIUS = 46; const DELTA = 1/100; let DISPLAY = true; let SOUND_ON = true; let GAME_STOPPED = true; let KEYBOARD_INPUT_ON = true; let TRAIN_ITER = 100; let AI_ON = true; let AI_PLAYER_NUM = 1; let DISPLAY_TRAINING = false;
How to Play?
- Aim by moving the mouse.
- Left click: shoot.
- ‘W’ : Increase shot power.
- ‘S’ : Decrease shot power.
- ‘Esc’ : Return to main menu.