// Cloned by junhao zhao on 1 Dec 2022 from World "Pacman" by Enhanced
// Please leave this clone trail here.
// Cloned by Enhanced on 21 Jun 2018 from World "Pacman In Space" by Simon Lowry
// Please leave this clone trail here.
/////////////////////////////////////////////////////////////////////////////////////////////////
/*
--------------------------------
New Features by Junhao Zhao :
--------------------------------
New rule:
1. multi-player game base on websocket
2. Two player control the pacmans try to eat bean as much as posiable
3. Two player will not effect each other
4. Small bean is 10 marks and big bean is 50 marks, there is a fake bean is 10 marks
5. The player have more marks win.
Change from prev:
1. Add websocket
2. Remove AI enemy
3. Add 2rd contralable pacman
4. gave a mark count for player two
5. give a decision function for find winner
6. It can move at an oblique angle, so the map has been modified to increase competitiveness
--------------------------------
New Features by Nathan Bonnard :
--------------------------------
1. Camera is fixed, you don't have to drag the mouse to play.
2. The movement of the player is now smooth and regular.
We now use a variable velocityPlayer that is added everyTime to the postion of the player,
you can't move faster by keep pushing down buttons.
3. new texture/colors, the previous one were not easy to visualize with a top view
-------------------------
Features by Simon Lowry :
-------------------------
Above Game Screen Section:
--------------------------
Contains Pacmans current Lives and total points.
A message is printed out here when a pacman loses all lives to
Teleport:
----------
If you go off the grid at any of the two "exit points" at the top of the screen you appear
at the bottom of the screen and vice versa for the bottom exit points.
Ghost Killer
-----------
When pacman moves onto a location contain a special dot ( a special dot is the white dots )
he is able to kill the ghosts for 5 seconds and gains a 100 point bonus for each ghost he kills
during that time. Music plays for that 5 seconds allowing you to know when you can kill the ghosts
and when it returns to the original way of ghosts killing pacman.
Sound:
-----
I used the theme tune from the tv show 'Stranger Things' to add an eerie affect to the
game, since it's pacman in space.
Used the traditional pacman sounds for chomping pellets, death of pacman, death of ghosts, and
When you win the game the sound played is an homage to a game from the 90's, final fantasy.
Level:
-----
# represents wired blocks which make up the maze itself
. represents regular pellets
- represents special dots/pellets which
/ represents our agent
I used an array of the above items to initialize the level itself.
= I wanted to design the level as an actual maze so it wouldn't be straight forward to
collect all the pellets. It added some difficulty for the human player.
This is especially the case in a couple of areas where there's only one way to go in and out,
making it easier for our enemy agents to close in on the human player and block off any possible exit.
= With three enemy agents on the screen they close in on you very fast. This also adds to the level of difficulty for
the human player.
Aditional Features that could be added (not added due to time constraints):
---------------------------------------------------------------------------
- each enemy has a set path which they traverse but on each step, they check to see if the
agent is within X steps of them, i.e. within a certain range of them.
if the agent is within their range try to kill him. If the agent goes outside the range of sight, return
to set path.
- animations:
- chomping pacman
- animation for pacman dieing
- Change colour of ghosts during ghost killer mode
- Additional Levels
- possible level random selection
- incremental levels, each new level would become more difficult
Limitations
------------
- I wanted to have a singular method for drawing an enemy. I couldn't get it working in Three.js
so I used separate methods for drawing each enemy. This is inefficient code re-use and something
I will figure out eventually.
Note:
-----
Pacman and agent referenced interchangeably, they are the same thing. Same for pellet and dot.
My apologies for that.
*/
////////////////////////////////////////////////////////////////////////////////////////////////
AB.drawRunControls = false;
// World must define these:
const CLOCKTICK = 100; // speed of run - move things every n milliseconds, lower faster
const MAXSTEPS = 10000000; // length of a run before final score
const SCREENSHOT_STEP = 50;
// -- constants for normal or special pellets to be added to screen
const NORMAL = true;
const SPECIAL = false;
//---- global constants: -------------------------------------------------------
const gridsize = 20; // number of squares along side of world
// density of maze - number of internal boxes
// (bug) use trunc or can get a non-integer
const squaresize = 100; // size of square in pixels
const MAXPOS = gridsize * squaresize; // length of one side in pixels
const sphereRadius = 50;
const sphereHeight = 10;
const sphereWidth = 10;
const pelletRadius = 10;
const SKYCOLOR = 0x000000; // a number, not a string
const BLANKCOLOR = SKYCOLOR ; // make objects this color until texture arrives (from asynchronous file read)
const show3d = true; // Switch between 3d and 2d view (both using Three.js)
// ********** originally = 0.8 ***************//
const startRadiusConst = MAXPOS * 0.8 ; // distance from centre to start the camera at
// it's an illusion, if you zoom out you see theat the skybox is just a container and outside of that is nothing
const skyboxConst = MAXPOS * 3 ; // where to put skybox
const maxRadiusConst = MAXPOS * 8 ; // maximum distance from camera we will render things
//--- Mind can pick one of these actions -----------------
const ACTION_LEFT = 0;
const ACTION_RIGHT = 1;
const ACTION_UP = 2;
const ACTION_DOWN = 3;
const ACTION_STAYSTILL = 4;
// in initial view, (smaller-larger) on i axis is aligned with (left-right)
// in initial view, (smaller-larger) on j axis is aligned with (away from you - towards you)
// contents of a grid square
const GRID_BLANK = 0;
const GRID_WALL = 1;
const GRID_MAZE = 2;
// keyword self used for a private function to call a public function self.functionName()
// --- some useful random functions -------------------------------------------
//---- start of World class -------------------------------------------------------
function World() {
var LEVEL = [
'# # # # # . # # # # # # # # # # # . # #', // 1
'# . . . # . # . . . # . . . . . . . . #', // 2
'# . # # # . # # . # # . . . # # . . . #', // 3
'# - . . . . . . ? . . . # . . # . . - #', // 4
'# . # # . . # # # # # . # # # # . # . #', // 5
'# . # # . . # . . # # . . . . . . # . #', // 6
'# . # # . . # . . # # . # # # # . # . #', // 7
'# . . # . . # . . # # . . # . . . # . #', // 8
'# . . . . . . . . . # . # # . . . . . #', // 9
'# . # # # . . . . . # . . # . . # . . #', // 10
'# . # . # . . . . # # . . . . . # # # #', // 11
'# . . . . . . . - . . . . . . . . . . #', // 12
'# # . # # # # . . . # # . # # . . . . #', // 13
'# . . . . . . . . # # . . . . . # . . #', // 14
'# . # . . # . . . . . . . . . . . . . #', // 15
'# - . . . # . . . # . . # # # # . . - #', // 16
'# . # . . # # # . # . . # . . # . . . #', // 17
'# # # . . . # . . # . . # . # # . . . #', // 18
'# / . . # . . . . . . . . . . . . . # #', // 19
'# # # # # . # # # # # # # # # # # . # #' // 20
];
var numPellets = 0; // counts the number of pellet objects on the screen, used for removal of pellets
var BOXHEIGHT; // 3d or 2d box height
var GRID = new Array(gridsize); // can query GRID about whether squares are occupied, will in fact be initialised as a 2D array
var theagent, theenemy;
// enemy and agent position on squares
var ei, ej, ai, aj, e2i, e2j, e3i, e3j;
var step;
var playerVelocity = new THREE.Vector3(0,0,0); // vector3 that is add to the position of the Player at each frame
var eplayerVelocity = new THREE.Vector3(0,0,0);
var lives; // number of lives pacman has left
var myScore; // current score
var self = this; // needed for private fn to call public fn - see below
// regular "function" syntax means private functions:
function initGrid()
{
for (var i = 0; i < gridsize ; i++)
{
GRID[i] = new Array(gridsize); // each element is an array
for (var j = 0; j < gridsize ; j++)
{
GRID[i][j] = GRID_BLANK ;
}
}
}
function occupied ( i, j ) // is this square occupied
{
if ( GRID[i][j] == GRID_WALL ) return true; // fixed objects, walls and stones
if ( GRID[i][j] == GRID_MAZE ) return true;
return false;
}
function occupiedByPellet(i, j) {
if (GRID[i][j] != GRID_MAZE && GRID[i][j] != GRID_BLANK && !isSpecialPellet(i, j) ) {
// increase score
myScore += 10;
// remove the pellet from screen
threeworld.scene.remove(GRID[i][j]);
GRID[i][j] = GRID_BLANK;
// pacman chomps pellet sound
playPelletSound();
numPellets--; // decrease pellet counter
} else if (isSpecialPellet(i, j)) { // start of ghost killer mode if true
myScore += 50;
playIntermissionSound();
threeworld.scene.remove(GRID[i][j]);
GRID[i][j] = GRID_BLANK;
numPellets--;
}
}
function eoccupiedByPellet(i, j) {
if (GRID[i][j] != GRID_MAZE && GRID[i][j] != GRID_BLANK && !isSpecialPellet(i, j) ) {
// increase score
yourScore += 10;
// remove the pellet from screen
threeworld.scene.remove(GRID[i][j]);
GRID[i][j] = GRID_BLANK;
// pacman chomps pellet sound
playPelletSound();
numPellets--; // decrease pellet counter
} else if (isSpecialPellet(i, j)) { // start of ghost killer mode if true
yourScore += 50;
playIntermissionSound();
threeworld.scene.remove(GRID[i][j]);
GRID[i][j] = GRID_BLANK;
numPellets--;
}
}
// returns true if the positioned pacman moved onto
// contains a special pellet
function isSpecialPellet(i, j) {
if (GRID[i][j] == GRID_BLANK) {
return false;
} else if ((i == 1 && (j == 15 || j == 3)) || // positions of special dots
(i == 18 && (j == 3 || j == 15)) ) {
return true;
}
return false;
}
// logically, coordinates are: y=0, x and z all positive (no negative)
// logically my dimensions are all positive 0 to MAXPOS
// to centre everything on origin, subtract (MAXPOS/2) from all dimensions
function translate ( x )
{
return ( x - ( MAXPOS/2 ) );
}
//--- skybox ----------------------------------------------------------------------------------------------
function initSkybox()
{
// x,y,z positive and negative faces have to be in certain order in the array
// mountain skybox, credit:
// http://stemkoski.github.io/Three.js/Skybox.html
var materialArray = [
( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/starter/sky_pos_z.jpg" ), side: THREE.BackSide } ) ),
( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/starter/sky_neg_z.jpg" ), side: THREE.BackSide } ) ),
( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/starter/sky_pos_y.jpg" ), side: THREE.BackSide } ) ),
( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/starter/sky_neg_y.jpg" ), side: THREE.BackSide } ) ),
( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/starter/sky_pos_x.jpg" ), side: THREE.BackSide } ) ),
( new THREE.MeshBasicMaterial ( { map: THREE.ImageUtils.loadTexture( "/uploads/starter/sky_neg_x.jpg" ), side: THREE.BackSide } ) ),
];
var skyGeometry = new THREE.CubeGeometry ( skyboxConst, skyboxConst, skyboxConst );
var skyMaterial = new THREE.MeshFaceMaterial ( materialArray );
var theskybox = new THREE.Mesh ( skyGeometry, skyMaterial );
threeworld.scene.add( theskybox ); // We are inside a giant cube
}
// This does the file read the old way using loadTexture.
// (todo) Change to asynchronous TextureLoader. A bit complex:
// Make blank skybox. Start 6 asynch file loads to call 6 return functions.
// Each return function checks if all 6 loaded yet. Once all 6 loaded, paint the skybox.
function loadTextures()
{
theagent.material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
theenemy.material = new THREE.MeshBasicMaterial( { color: 0xff0000 } );
}
// --- add fixed objects ----------------------------------------
// creates a wireframe block
function createBlock() {
var shape = new THREE.CubeGeometry( squaresize, BOXHEIGHT, squaresize );
var material = new THREE.MeshBasicMaterial( {color: 0x00008B, wireframe:true } );
var cube = new THREE.Mesh( shape, material );
return cube;
}
// creates a pellet for pacman to chomp
function createPellet(whichPellet) {
var shape;
var material;
if (whichPellet == NORMAL) {
shape = new THREE.SphereGeometry( pelletRadius, sphereHeight, sphereWidth);
material = new THREE.MeshBasicMaterial({color: "yellow"});
} else if (whichPellet == SPECIAL) {
shape = new THREE.SphereGeometry( pelletRadius + 10, sphereHeight + 10, sphereWidth + 10);
material = new THREE.MeshBasicMaterial({color: "white"});
}
var pellet = new THREE.Mesh(shape, material);
return pellet;
}
// --- enemy functions -----------------------------------
function drawEnemy() // given ei, ej, draw it
{
theenemy.position.set(translate ( ei * squaresize ), 0, translate ( ej * squaresize ) );
threeworld.scene.add(theenemy);
threeworld.follow.copy ( theenemy.position );
}
function initThreeEnemy()
{
var shape = new THREE.SphereGeometry( sphereRadius, sphereHeight, sphereWidth );
theenemy = new THREE.Mesh( shape );
theenemy.material.color.setHex( BLANKCOLOR );
drawEnemy();
}
function socketSender(action, player) // function to send out the move to do, then do the move locally
{
data = [action, player];
AB.socketOut(data);
if (player == 1){
moveLogicalAgent(action);
}
else{
emoveLogicalAgent(action);
}
}
// --- agent functions -----------------------------------
function drawAgent() // given ai, aj, draw it
{
theagent.position.set(translate ( ai * squaresize ), 0, translate ( aj * squaresize ) );
threeworld.scene.add(theagent);
threeworld.follow.copy ( theagent.position ); // follow vector = agent position (for camera following agent)
}
function initThreeAgent()
{
var shape = new THREE.SphereGeometry( sphereRadius, sphereHeight, sphereWidth );
theagent = new THREE.Mesh( shape );
theagent.material.color.setHex( BLANKCOLOR );
drawAgent();
}
function moveLogicalAgent( a ) // this is called by the infrastructure that gets action a from the Mind
{
if ( a == ACTION_LEFT ) playerVelocity.x = -1;
else if ( a == ACTION_RIGHT ) playerVelocity.x = 1;
else if ( a == ACTION_UP ) playerVelocity.y = 1;
else if ( a == ACTION_DOWN ) playerVelocity.y = -1;
else{ playerVelocity.x = 0;playerVelocity.y = 0;}
}
function emoveLogicalAgent( a ) // this is called by the infrastructure that gets action a from the Mind
{
if ( a == ACTION_LEFT ) eplayerVelocity.x = -1;
else if ( a == ACTION_RIGHT ) eplayerVelocity.x = 1;
else if ( a == ACTION_UP ) eplayerVelocity.y = 1;
else if ( a == ACTION_DOWN ) eplayerVelocity.y = -1;
else{ eplayerVelocity.x = 0;eplayerVelocity.y = 0;}
}
// When key is Up
function keyHandler(e)
// user control
// Note that this.takeAction(a) is constantly running at same time, redrawing the screen.
{
if (e.keyCode == 37) socketSender(ACTION_LEFT, 1);
if (e.keyCode == 38) socketSender(ACTION_DOWN, 1);
if (e.keyCode == 39) socketSender(ACTION_RIGHT, 1);
if (e.keyCode == 40) socketSender(ACTION_UP, 1);
if (e.keyCode == 65) socketSender(ACTION_LEFT, 2);
if (e.keyCode == 87) socketSender(ACTION_DOWN, 2);
if (e.keyCode == 68) socketSender(ACTION_RIGHT, 2);
if (e.keyCode == 83) socketSender(ACTION_UP, 2);
}
//////////////////// TO BE DELETED //////////////////// ////////////////////
//////////////////// //////////////////// //////////////////// ////////////////////
function updateStatusBefore(a)
// this is called before anyone has moved on this step, agent has just proposed an action
// update status to show old state and proposed move
{
var x = self.getState();
$("#user_span3").html( status );
}
function updateStatusAfter() // agent and enemy have moved, can calculate score
{
var status = "P1 Points: " + myScore + " P2 Points: " + yourScore;
$("#user_span7").html( status );
}
//--- public functions / interface / API ----------------------------------------------------------
this.endCondition; // If set to true, run will end.
this.newRun = function()
{
// (subtle bug) must reset variables like these inside newRun (in case do multiple runs)
this.endCondition = false;
step = 0;
myScore = 0;
yourScore = 0;
// for all runs:
initGrid();
setStartingPositions(); // sets starting positions of human and agents
// for graphical runs only:
if ( true )
{
if ( show3d )
{
BOXHEIGHT = squaresize;
threeworld.init3d ( startRadiusConst, maxRadiusConst, SKYCOLOR );
}
else
{
BOXHEIGHT = 1;
threeworld.init2d ( startRadiusConst, maxRadiusConst, SKYCOLOR );
}
initSkybox();
initMusic();
// Set up objects first:
initThreeAgent();
setUpLevel();
initThreeEnemy();
// Then paint them with textures - asynchronous load of textures from files.
// The texture file loads return at some unknown future time in some unknown order.
// Because of the unknown order, it is probably best to make objects first and later paint them, rather than have the objects made when the file reads return.
// It is safe to paint objects in random order, but might not be safe to create objects in random order.
loadTextures();
document.onkeydown = keyHandler;
}
};
AB.socketStart(), AB.socketIn = function(data) // incoming data on socket, i.e. clicks of other player
{
if (data[1] == 1){
moveLogicalAgent(data[0]);
}
else {
emoveLogicalAgent(data[0]);
}
};
function setUpLevel() {
for (var row = 0; row < gridsize ; row++) {
var y = row;
for (var column = 0; column < LEVEL[row].length; column += 2) {
var cell = LEVEL[row][column];
var x = Math.floor(column / 2);
switch ( cell ) {
case "#": { // adds a wireframe block to the screen
var object = createBlock();
GRID[x][row] = GRID_MAZE;
object.position.set(translate ( x * squaresize ), 0, translate ( row * squaresize ) );
threeworld.scene.add(object);
break;
} case ".": { // adds a pellet to the screen
var pellet = createPellet(true);
GRID[x][row]= pellet;
numPellets++;
pellet.position.set(translate ( x * squaresize ), 0, translate ( row * squaresize ) );
threeworld.scene.add(pellet);
break;
} case "-": { // adds a special dot to the screen
var specialDot = createPellet(false);
GRID[x][row]= specialDot;
numPellets++;
specialDot.position.set(translate ( x * squaresize ), 0, translate ( row * squaresize ) );
threeworld.scene.add(specialDot);
break;
}
}
}
}
threeworld.camera.position.set(0,1600,0);
threeworld.camera.rotation.set(0,0,0);
}
///// ///// ///// ///// POSITION BASED METHODS ///// ///// ///// ///// /////
// resets all enemies and human agent to original positions
function restartPositions() {
setStartingPositions();
drawAgent();
drawEnemy();
}
// sets starting positions
function setStartingPositions() {
ai = 1; aj = 18; // agent starting position
ei = 18; ej = 2; // enemy1 starting position
}
function resetGhostPosition() {
if (ai == ei && aj == ej) {
ei = 18; ej = 2;
drawEnemy();
}
}
////////// ///// ///// ///// ///// END POSITION METHODS ///// ///// ///// ///// /////
this.getState = function()
{
var x = [ ai, aj, ei, ej ];
return ( x );
};
this.nextStep = function()
{
if ( ! occupied(ai + playerVelocity.x, aj + playerVelocity.y) )
{
if (ai + playerVelocity.x == 5 && aj + playerVelocity.y == 20) {
ai = 5; aj = 0;
} else if (ai + playerVelocity.x == 5 && aj + playerVelocity.y == -1) {
ai = 5; aj = 19;
} else if (ai + playerVelocity.x == 17 && aj + playerVelocity.y == 20) {
ai = 17; aj = 0;
} else if (ai + playerVelocity.x == 17 && aj + playerVelocity.y == -1) {
ai = 17; aj = 19;
}
else
{
ai += playerVelocity.x;
aj += playerVelocity.y;
}
}
if ( ! occupied(ei + eplayerVelocity.x, ej + eplayerVelocity.y) )
{
if (ei + eplayerVelocity.x == 5 && ej + eplayerVelocity.y == 20) {
ei = 5; ej = 0;
} else if (ei + eplayerVelocity.x == 5 && ej + eplayerVelocity.y == -1) {
ei = 5; ej = 19;
} else if (ei + eplayerVelocity.x == 17 && ej + eplayerVelocity.y == 20) {
ei = 17; ej = 0;
} else if (ei + eplayerVelocity.x == 17 && ej + eplayerVelocity.y == -1) {
ei = 17; ej = 19;
}
else
{
ei += eplayerVelocity.x;
ej += eplayerVelocity.y;
}
}
var a = 4;
if ( true )
updateStatusBefore(a); // show status line before moves
moveLogicalAgent(a);
emoveLogicalAgent(a);
// slow the enemy down to every nth step
// death of player
// occupied by a pellet
occupiedByPellet(ai, aj);
eoccupiedByPellet(ei, ej);
if ( true )
{
drawAgent();
drawEnemy();
updateStatusAfter(); // show status line after moves
}
if (numPellets == 0) {
this.endCondition = true;
playWinGameSound();
}
};
this.endRun = function()
{
if ( true )
{
// player loses condition
if ( this.endCondition && myScore > yourScore ){
$("#user_span6").html( "<font color=blue> <B> Player one won the game, congrats!! Final score: </B>" + myScore + "<BR> </font> " );
} else if (this.endCondition && myScore < yourScore) {
$("#user_span6").html( "<font color=blue> <B> Player two won the game, congrats!! Final score: </B>" + yourScore + "<BR> </font> " );
} else if (this.endCondition && myScore == yourScore){ // player has won the game
$("#user_span6").html( "<font color=blue> <B> Draw, Final score: </B>" + yourScore + "<BR> </font> " );
}
}
};
}
//---- end of World class -------------------------------------------------------
// --- music and sound effects ----------------------------------------
function initMusic()
{
// put music element in one of the spans
var x = "<audio id=theaudio src=/uploads/zhaoj23/bmg_001.mp3 autoplay loop> </audio>" ;
$("#user_span2").html( x );
/* var x = "<audio id=theaudio src=/uploads/starter/Defense.Line.mp3 autoplay loop> </audio>" ;
$("#user_span1").html( x ); */
}
function musicPlay()
{
// jQuery does not seem to parse pause() etc. so find the element the old way:
document.getElementById('theaudio').play();
}
function musicPause()
{
document.getElementById('theaudio').pause();
}
function soundAlarm()
{
var x = "<audio src=/uploads/starter/air.horn.mp3 autoplay > </audio>";
$("#user_span2").html( x );
}
/*************************** MY SOUND SECTION *************************/
function playPelletSound() {
var x = "<audio id=theaudio src=/uploads/simonlowry/pacman_chomp.mp3 autoplay > </audio>" ;
$("#user_span1").html( x );
}
function playDeathSound() {
var x = "<audio id=theaudio src=/uploads/simonlowry/pacman_death.mp3 autoplay > </audio>" ;
$("#user_span1").html( x );
}
function playWinGameSound(){
var x = "<audio id=theaudio src=/uploads/simonlowry/finalfantasyviicd1-nobuouematsu-11-fanfare.mp3 autoplay > </audio>" ;
$("#user_span2").html( x );
}
function playGhostKilledSound() {
var x = "<audio id=theaudio src=/uploads/simonlowry/pacman_eatghost.mp3 autoplay > </audio>" ;
$("#user_span4").html( x );
}
function playIntermissionSound() {
var x = "<audio id=theaudio src=/uploads/simonlowry/pacman_intermission.mp3 autoplay > </audio>" ;
$("#user_span5").html( x );
}
function playExtraPacmanSound() {
var x = "<audio id=theaudio src=/uploads/simonlowry/pacman_extrapac.mp3 autoplay > </audio>" ;
$("#user_span1").html( x );
}
/*************************** END SOUND SECTION *************************/
// start sockets