UNPKG

node-haxball

Version:

The most powerful and lightweight API that allows you to develop your original Haxball(www.haxball.com) host, client, and standalone applications both on node.js and browser environments and also includes every possible hack and functionality that you can

1,207 lines (1,120 loc) 44.4 kB
module.exports = function(API){ const { OperationType, VariableType, ConnectionState, AllowFlags, Direction, CollisionFlags, CameraFollow, BackgroundType, GamePlayState, BanEntryType, Callback, Utils, Room, Replay, Query, Library, RoomConfig, Plugin, Renderer, Errors, Language, EventFactory, Impl } = API; Object.setPrototypeOf(this, Plugin.prototype); Plugin.call(this, "CMD_sokoban", true, { // "CMD_sokoban" is plugin's name, "true" means "activated just after initialization". Every plugin should have a unique name. version: "0.1", author: "abc & AlliBalliBaba", description: `This plugin sets up a sokoban game.`, allowFlags: AllowFlags.CreateRoom // We allow this plugin to be activated on CreateRoom only. }); //var downloadLink = "https://cdn.jsdelivr.net/gh/begoon/sokoban-maps/maps/sokoban-maps-60-plain.txt"; //var maps = null; var that = this, boxSize = 30; var tileProperties = [ null, // empty { color: "000000", radius: boxSize/2 }, // wall { color: "a1662f", radius: boxSize/2 }, // box { color: "355e3b", radius: boxSize/4 }, // target { color: "cc0808", radius: boxSize/2 }, // box+target null // player ]; /* // example map: var mapProps = { w: 7, h: 7, tiles: [ [0, 1, 1, 1, 1, 1, 0], [1, 1, 0, 0, 0, 1, 0], [1, 0, 3, 3, 0, 1, 1], [1, 0, 5, 4, 0, 0, 1], [1, 0, 2, 4, 2, 0, 1], [1, 0, 0, 0, 0, 1, 1], [1, 1, 1, 1, 1, 1, 0] ] }; */ var gameState = null, mapProps = null, worker, workerPromise, workerPromiseResolve, permissionCtx, permissionIds; function updatePlayerDiscs(){ that.room.state.players.forEach((player)=>{ if (player.disc!=null){ that.room.setPlayerDiscProperties(player.id, { xspeed: 0, yspeed: 0, cGroup: 1<<CollisionFlags.c0 }); } }); } function updateStadium(){ if (workerPromise) return workerPromise; workerPromise = new Promise((resolve, reject)=>{ workerPromiseResolve = resolve; worker.postMessage("1"); }); return workerPromise.then((_mapProps)=>{ mapProps = _mapProps; var offsetX = (1-mapProps.w)*boxSize/2, offsetY = (1-mapProps.h)*boxSize/2; var stadiumJson = { "name" : "Sokoban", "width" : 420, "height" : 200, "cameraWidth" : 0, "cameraHeight" : 0, "maxViewWidth" : 0, "cameraFollow" : "player", "spawnDistance" : 170, "redSpawnPoints" : [], "blueSpawnPoints" : [], "canBeStored" : true, "kickOffReset" : "partial", "bg" : { "color" : "777700" }, "vertexes" : mapProps.vertices.map(({x,y})=>({"x": x*boxSize+offsetX, "y": y*boxSize+offsetY})), "segments" : mapProps.segments, "goals" : [], "discs" : [], "planes" : [], "joints" : [], "playerPhysics" : { "radius" : boxSize/2, "cMask" : ["none"], "cGroup" : ["c0"], "gravity" : [0, 0], "acceleration" : 0, "damping" : 0, "kickingAcceleration" : 0, "kickingDamping" : 0, "kickStrength" : 0, "kickback" : 0 }, "ballPhysics" : { "radius" : 0, "cMask" : ["none"], "cGroup" : ["c0"] } }; gameState = []; for (var i=0;i<mapProps.h;i++){ var a = mapProps.tiles[i]; var row = []; gameState.push(row); for (var j=0;j<mapProps.w;j++){ var val = a[j], p = [j*boxSize+offsetX, i*boxSize+offsetY], ids = []; if (val==5){ stadiumJson.redSpawnPoints.push(p); stadiumJson.blueSpawnPoints.push(p); val = 0; } else if (val==6){ stadiumJson.redSpawnPoints.push(p); stadiumJson.blueSpawnPoints.push(p); val = 3; } var props = tileProperties[val]; if (props){ stadiumJson.discs.push({ "pos": p, "color": props.color, "radius": props.radius, "cMask" : ["none"], "cGroup" : ["c0"] }); ids.push(stadiumJson.discs.length); if (val==4){ stadiumJson.discs.push({ "pos": p, "color": props.color, "radius": props.radius, "cMask" : ["none"], "cGroup" : ["c0"] }); ids.push(stadiumJson.discs.length); } } row.push({ tile: val, discIds: ids }); } } var stadium; try{ stadium = Utils.parseStadium(JSON.stringify(stadiumJson)); }catch(e){} if (!stadium) return false; that.room.setCurrentStadium(stadium); return true; }); } function nextStadium(){ that.room.stopGame(); updateStadium().then(()=>{ that.room.startGame(); }); //while (!updateStadium(Math.floor(maps.length*Math.random()))); //that.room.startGame(); } function resetState(){ gameState = []; var k = 0; for (var i=0;i<mapProps.h;i++){ var a = mapProps.tiles[i]; var row = []; gameState.push(row); for (var j=0;j<mapProps.w;j++){ var val = a[j], ids = []; if (val==5) val = 0; var props = tileProperties[val]; if (props){ ids.push(++k); if (val==4) ids.push(++k); } row.push({ tile: val, discIds: ids }); } } } function checkEndGame(player){ if (gameState.findIndex((row)=>(row.findIndex((cell)=>(cell.tile==2 || cell.tile==3))>=0))<0){ Utils.runAfterGameTick(()=>{ that.room.sendAnnouncement("["+player.id+"]"+player.name+" has won.", null, 0xff0000); nextStadium(); }); } } function optimizeForHaxball(level){ var arr = [], arr2 = []; var x1=level.w+2, y1=level.h+2, x2=-1, y2=-1; for (var i=-1;i<=level.h;i++){ var oRow = level.arr[i] || [], row = [], row2 = []; for (var j=-1;j<=level.w;j++){ var c = oRow[j]; row.push((c==null || c==1)?1:0); row2.push(0); } arr.push(row); arr2.push(row2); } function checkCall(x, y, dx, dy){ var nx = x+dx, ny = y+dy; if (nx<0 || nx>level.w+1 || ny<0 || ny>level.h+1 || arr2[ny][nx]) return; if (arr[ny][nx]==0){ if (x1>x) x1=x; if (x2<x) x2=x; if (y1>y) y1=y; if (y2<y) y2=y; arr2[y][x]=1; } floodfill(nx,ny); } function floodfill(x,y){ if (arr[y][x]==0 || arr2[y][x]==2) return; arr2[y][x]=2; checkCall(x,y,-1,0); checkCall(x,y,1,0); checkCall(x,y,0,-1); checkCall(x,y,0,1); } floodfill(0,0); var arr3 = []; for (var i=y1;i<=y2;i++){ var row = []; for (var j=x1;j<=x2;j++){ var val; switch(arr2[i][j]){ case 0: val = level.arr[i-1]?.[j-1]; break; case 1: val = 8; break; case 2:{ if ((arr2[i+1]?.[j+1]==0)||(arr2[i+1]?.[j-1]==0)||(arr2[i-1]?.[j+1]==0)||(arr2[i-1]?.[j-1]==0)) val = 8; else val = 0; break; } } row.push(val); } arr3.push(row); } var vertices = [], segments = []; var w = x2-x1+1, h = y2-y1+1; for (var i=0;i<h;i++) for (var j=0;j<w;j++){ if (arr3[i][j]!=8) continue; var b1 = arr3[i][j-1]==8; var b2 = arr3[i][j+1]==8; var b3 = arr3[i-1]?.[j]==8; var b4 = arr3[i+1]?.[j]==8; var t = b1+b2+b3+b4; if ((t==2) && ((b1&&b2)||(b3&&b4))) continue; vertices.push({x:j, y:i, b1:b2, b2:b4, id:vertices.length}); } vertices.forEach((v)=>{ var {x,y,b1,b2,id} = v; delete v.b1; delete v.b2; delete v.id; if (b1) segments.push({ "v0": id, "v1": vertices.filter((z)=>(z.y==y && z.x>x)).sort((v1,v2)=>(v1.x-v2.x))[0].id }); if (b2) segments.push({ "v0": id, "v1": vertices.filter((z)=>(z.x==x && z.y>y)).sort((v1,v2)=>(v1.y-v2.y))[0].id }); }); return { w: w, h: h, tiles: arr3, vertices: vertices, segments: segments }; } this.initialize = function(){ permissionCtx = that.room.librariesMap.permissions?.createContext("sokoban"); if (permissionCtx) permissionIds = { skip: permissionCtx.addPermission("skip") }; that.room.librariesMap.commands?.add({ name: "skip", parameters: [], minParameterCount: 0, helpText: "Skips the current map.", callback: ({}, byId) => { if (byId!=0 && !permissionCtx?.checkPlayerPermission(byId, permissionIds.skip)){ that.room.librariesMap.commands?.announcePermissionDenied(byId); return; } nextStadium(); } }); worker = new Worker(URL.createObjectURL(new Blob([` // Code adapted from : https://github.com/AlliBalliBaba/Sokoban-Level-Generator //create a 2D Array function Array2d(xSize, ySize, content) { var NodeGrid = new Array(xSize); for (var i = 0; i < xSize; i++) { NodeGrid[i] = new Array(ySize); for (var j = 0; j < ySize; j++) NodeGrid[i][j] = content; } return NodeGrid; } function perm(aList, bList, permutation, permutations) { for (var j = 0; j < bList.length; j++) { var newPer = permutation.slice(); newPer.push([aList[0], bList[j]]); if (aList.length > 1) { var newList = bList.slice(); newList.splice(j, 1); perm(aList.slice(1, aList.length), newList, newPer, permutations) } else permutations.push(newPer); } } //find all permutations between the elements of 2 lists of equal size function findPermutations(aList, bList) { var permutations = []; perm(aList, bList, [], permutations); return permutations; } //add to sorted array via binary search function addToSortedArray(array, element, compareFunction) { var index = binarySearch(array, element, compareFunction); if (index < 0) index = -index - 1; array.splice(index, 0, element); } //binary search in sorted array function binarySearch(array, element, compareFunction) { var m = 0; var n = array.length - 1; while (m <= n) { var k = (n + m) >> 1; var cmp = compareFunction(element, array[k]); if (cmp > 0) m = k + 1; else if (cmp < 0) n = k - 1; else return k; } return -m - 1; } function fComparer(element1, element2) { return element1.f - element2.f; } //check if a point is in the boundaries of a 2D-array function checkBoundaries(arr2D, x, y) { return (x >= 0 && x < arr2D.length && y >= 0 && y < arr2D[0].length); } //return a random integer between min (included) and max (excluded) function randomInt(min, max) { return Math.floor(Math.random() * (max - min)) + min; } class Level { constructor(xSize, ySize, NumBoxes) { this.xSize = xSize; this.ySize = ySize; this.buttons = []; this.boxes = []; this.ghostboxes = []; this.solveCounter = NumBoxes; this.savedPositions = []; this.trash = false; //initialize nodes for pathfinding this.nodes = Array2d(xSize, ySize, null); for (var i = 0; i < this.nodes.length; i++) for (var j = 0; j < this.nodes[0].length; j++) this.nodes[i][j] = new Node(i, j); //randomly place boxes and buttons this.defineAllowedSpots(); this.placeObjects(NumBoxes); } placeObjects(numBoxes) { //place buttons for (var i = 0; i < numBoxes; i++) { var pos = this.randomSpot(); if (pos != null) this.buttons.push(new Button(pos[0], pos[1])) } //place boxes for (var i = 0; i < numBoxes; i++) { var pos = this.randomSpot(); if (pos != null) { this.boxes.push(new Box(pos[0], pos[1], this.buttons[i])) this.nodes[pos[0]][pos[1]].hasBox = true; } } //place player var pos = this.randomSpot(); if (pos == null) pos = [this.buttons[0].x, this.buttons[0].y]; this.setPlayerPos(pos[0], pos[1]); this.playerstartX = this.playerX; this.playerstartY = this.playerY; } //write all nodes, where placement is allowed into a list defineAllowedSpots() { this.allowedSpots = []; for (var i = 2; i < this.nodes.length - 2; i++) for (var j = 2; j < this.nodes[0].length - 2; j++) this.allowedSpots.push(this.nodes[i][j]); } //return a random unoccupied spot and remove the wall randomSpot() { if (this.allowedSpots.length == 0) return null; var rand = randomInt(0, this.allowedSpots.length); var x = this.allowedSpots[rand].x; var y = this.allowedSpots[rand].y; this.allowedSpots.splice(rand, 1); this.nodes[x][y].wall = false; if (this.blockaded(x, y)) return this.randomSpot(); else return [x, y]; } //randomly remove walls from the level rip(amount) { for (var i = 0; i < amount; i++) if (this.allowedSpots.length != 0) this.randomSpot(); } setPlayerPos(X, Y) { this.playerX = X; this.playerY = Y; } //check if a spot is blockaded by boxes blockaded(X, Y) { return (this.nodes[X + 1][Y].hasBox && ((this.nodes[X + 1][Y + 1].hasBox && this.nodes[X][Y + 1].hasBox) || (this.nodes[X + 1][Y - 1].hasBox && this.nodes[X][Y - 1].hasBox))) || (this.nodes[X - 1][Y].hasBox && ((this.nodes[X - 1][Y - 1].hasBox && this.nodes[X][Y - 1].hasBox) || (this.nodes[X - 1][Y + 1].hasBox && this.nodes[X][Y + 1].hasBox))); } } class Box { constructor(X, Y, button) { this.x = X; this.y = Y; this.px = this.x; this.py = this.y; this.placed = false; this.solveButton = button; } setPosition(X, Y) { this.x = X; this.y = Y; } placeExactly(X, Y) { this.x = X; this.y = Y; this.px = this.x; this.py = this.y; } } class Button { constructor(X, Y) { this.x = X; this.y = Y; } } // the idea behind this pathfinding algorithm is to traverse the least amount of walls // additionally the player will always take the longest available free path when choosing a box const wallCost = 100; //the cost for traversing a wall var pathCost = 1; //the cost for traversing an unoccupied node var playerPathCost = -1; //the player cost for traversing an unoccupied node const boxCost = 10000; //the cost for traversing an occupied node class Node { //initialize each node as an unoccupied wall constructor(X, Y) { this.x = X; this.y = Y; this.f = 0; //path-cost-estimation this.cost = 0; //path-cost this.closed = false; //variable for pathfinding this.checked = false; //variable for pathfinding this.wall = true; //check if node has a wall this.occupied = false; //check if node is occupied(for pathfinding) this.parent = null; //node parent for pathfinding this.hasBox = false; //check if node has a box this.used = false; //variable for optimizing } } class Pathfinder { constructor(Level, startX, startY, endX, endY) { this.level = Level; this.nodes = this.level.nodes; this.startX = startX; this.startY = startY; this.endX = endX; this.endY = endY; this.open = []; this.closed = []; } //return path and cost, cost of player-path and box-path can differ returnPath(isBox) { this.open.push(this.nodes[this.startX][this.startY]); while (this.open.length != 0) { var thisNode = this.open.shift(); if (thisNode.x == this.endX && thisNode.y == this.endY) { this.open.push(thisNode); return this.sumPath(thisNode); } else { thisNode.closed = true; this.closed.push(thisNode); this.checkNeighbor(thisNode.x + 1, thisNode.y, thisNode, isBox); this.checkNeighbor(thisNode.x - 1, thisNode.y, thisNode, isBox); this.checkNeighbor(thisNode.x, thisNode.y + 1, thisNode, isBox); this.checkNeighbor(thisNode.x, thisNode.y - 1, thisNode, isBox); } } console.log("no path found"); return this.sumPath(thisNode); } //recreate the path starting from the last node sumPath(endNode) { var path = []; var cost = endNode.cost; while (endNode.parent != null) { path.unshift(endNode); endNode = endNode.parent; } this.resetNodes(); return [path, cost] } //check a neighboring node checkNeighbor(x, y, parent, isBox) { if (checkBoundaries(this.nodes, x, y)) { var thisNode = this.nodes[x][y]; if (!thisNode.closed && !thisNode.checked) { thisNode.cost = this.calculateCost(thisNode, parent, isBox); thisNode.f = thisNode.cost + Math.abs(x - this.endX) + Math.abs(y - this.endY); thisNode.parent = parent; thisNode.checked = true; addToSortedArray(this.open, thisNode, fComparer); } else if (!thisNode.closed) { var cost = this.calculateCost(thisNode, parent, isBox); if (cost < thisNode.cost && thisNode.parent.parent != null) { thisNode.cost = cost; thisNode.f = thisNode.cost + Math.abs(x - this.endX) + Math.abs(y - this.endY); thisNode.parent = parent; } } } } //calculate the cost for a node calculateCost(node, parent, isBox) { var tempCost = 0; if (node.occupied) tempCost = parent.cost + boxCost; else { if (isBox) tempCost = (node.wall ? parent.cost + wallCost : parent.cost + pathCost); else tempCost = (node.wall ? parent.cost + wallCost : parent.cost + playerPathCost); } // if the path is calculated for a box, the player path also has to be included // the player has to walk around the box when changing directions // there are always 2 ways to walk around the box for each of the 8 situations: if (isBox && parent.parent != null) { var cost1 = 0; var cost2 = 0; if (node.x - 1 == parent.x && node.x - 2 != parent.parent.x) { //case 1: node is right of parent if (node.y - 1 == parent.parent.y) { //case 1.1: node is up right of parent.parent cost1 = this.nodeCost(node.x - 2, node.y) + this.nodeCost(node.x - 2, node.y - 1); cost2 = this.nodeCost(node.x, node.y - 1) + this.nodeCost(node.x, node.y + 1) + this.nodeCost(node.x - 1, node.y + 1) + this.nodeCost(node.x - 2, node.y + 1) + this.nodeCost(node.x - 2, node.y); } else { //case 1.2: node is down right of parent.parent cost1 = this.nodeCost(node.x - 2, node.y) + this.nodeCost(node.x - 2, node.y + 1); cost2 = this.nodeCost(node.x, node.y - 1) + this.nodeCost(node.x, node.y + 1) + this.nodeCost(node.x - 1, node.y - 1) + this.nodeCost(node.x - 2, node.y - 1) + this.nodeCost(node.x - 2, node.y); } } else if (node.x + 1 == parent.x && node.x + 2 != parent.parent.x) { //case 2: node is left of parent if (node.y - 1 == parent.parent.y) { //case 2.1: node is up left of parent.parent cost1 = this.nodeCost(node.x + 2, node.y) + this.nodeCost(node.x + 2, node.y - 1); cost2 = this.nodeCost(node.x, node.y - 1) + this.nodeCost(node.x, node.y + 1) + this.nodeCost(node.x + 1, node.y + 1) + this.nodeCost(node.x + 2, node.y + 1) + this.nodeCost(node.x + 2, node.y); } else { //case 2.2: node is down left of parent.parent cost1 = this.nodeCost(node.x + 2, node.y) + this.nodeCost(node.x + 2, node.y + 1); cost2 = this.nodeCost(node.x, node.y - 1) + this.nodeCost(node.x, node.y + 1) + this.nodeCost(node.x + 1, node.y - 1) + this.nodeCost(node.x + 2, node.y - 1) + this.nodeCost(node.x + 2, node.y); } } else if (node.y - 1 == parent.y && node.y - 2 != parent.parent.y) { //case 3: node is above parent if (node.x - 1 == parent.parent.x) { //case 3.1: node is right up of parent.parent var cost1 = this.nodeCost(node.x, node.y - 2) + this.nodeCost(node.x - 1, node.y - 2); var cost2 = this.nodeCost(node.x - 1, node.y) + this.nodeCost(node.x + 1, node.y) + this.nodeCost(node.x + 1, node.y - 1) + this.nodeCost(node.x + 1, node.y - 2) + this.nodeCost(node.x, node.y - 2); } else { //case 3.2: node is left up of parent.parent var cost1 = this.nodeCost(node.x, node.y - 2) + this.nodeCost(node.x + 1, node.y - 2); var cost2 = this.nodeCost(node.x - 1, node.y) + this.nodeCost(node.x + 1, node.y) + this.nodeCost(node.x - 1, node.y - 1) + this.nodeCost(node.x - 1, node.y - 2) + this.nodeCost(node.x, node.y - 2); } } else if (node.y + 1 == parent.y && node.y + 2 != parent.parent.y) { //case 4: node is under parent if (node.x - 1 == parent.parent.x) { //case 4.1: node is right down of parent.parent var cost1 = this.nodeCost(node.x, node.y + 2) + this.nodeCost(node.x - 1, node.y + 2); var cost2 = this.nodeCost(node.x - 1, node.y) + this.nodeCost(node.x + 1, node.y) + this.nodeCost(node.x + 1, node.y + 1) + this.nodeCost(node.x + 1, node.y + 2) + this.nodeCost(node.x, node.y + 2); } else { //case 4.2: node is left down of parent.parent var cost1 = this.nodeCost(node.x, node.y + 2) + this.nodeCost(node.x + 1, node.y + 2); var cost2 = this.nodeCost(node.x - 1, node.y) + this.nodeCost(node.x + 1, node.y) + this.nodeCost(node.x - 1, node.y + 1) + this.nodeCost(node.x - 1, node.y + 2) + this.nodeCost(node.x, node.y + 2); } } tempCost += (cost1 < cost2 ? cost1 : cost2); } else if (isBox) { if (node.x - 1 == parent.x) tempCost += this.nodeCost(node.x - 2, node.y); else if (node.x + 1 == parent.x) tempCost += this.nodeCost(node.x + 2, node.y); else if (node.y - 1 == parent.y) tempCost += this.nodeCost(node.x, node.y - 2); else if (node.y + 1 == parent.y) tempCost += this.nodeCost(node.x, node.y + 2); } //for optimizing prefer used nodes if (node.used) tempCost -= 5; return tempCost; } //calculate the cost of a position nodeCost(x, y) { if (!checkBoundaries(this.nodes, x, y)) return boxCost; var node = this.nodes[x][y]; if (node.occupied) return boxCost; else return (node.wall ? wallCost : playerPathCost); } //reset the level's nodes for further pathfinding resetNodes() { this.open.forEach(function(node) { node.checked = false; node.closed = false; node.parent = null; node.cost = 0; }); this.closed.forEach(function(node) { node.checked = false; node.closed = false; node.parent = null; node.cost = 0; }); } } //generates the paths between boxes and buttons in a level by removing walls function generatePaths(lvl) { var steps = 0; //create ghostBoxes for solving var ghostBoxes = copyBoxes(lvl, false); //push the ghostBoxes towards the buttons while (lvl.solveCounter > 0) { //calculate the paths from all boxes to their buttons var boxPaths = CalcualteBoxPaths(lvl, ghostBoxes); //calculate the player paths to all boxes and choose the lowest cost path var playerPaths = CalcualtePlayerPaths(lvl, ghostBoxes, boxPaths); var bestPath = playerPaths[1]; var playerPath = playerPaths[0][bestPath][0]; // CAUTION: Sometimes playerPaths[0][bestPath] is null, so this line will throw error. var boxPath = boxPaths[bestPath][0]; //remove all walls on the player's path for (var i = 0; i < playerPath.length; i++) { playerPath[i].wall = false; if (playerPath[i].occupied) lvl.trash = true; } //push the box into the solving direction var thisbox = ghostBoxes[bestPath]; var currentNode = boxPath[0]; var diffX = currentNode.x - thisbox.x; var diffY = currentNode.y - thisbox.y; var stop = 0; //if the box-path is longer than 1, push until there is a turn for (var i = 1; i < boxPath.length; i++) { var nextNode = boxPath[i]; if (diffX == nextNode.x - currentNode.x && diffY == nextNode.y - currentNode.y) currentNode = nextNode; else { stop = i - 1; break; } } //remove all walls on the box's path for (var i = 0; i <= stop; i++) boxPath[i].wall = false; //set new player and box positions lvl.nodes[thisbox.x][thisbox.y].occupied = false; thisbox.setPosition(boxPath[stop].x, boxPath[stop].y) lvl.nodes[thisbox.x][thisbox.y].occupied = true; lvl.setPlayerPos(thisbox.x - diffX, thisbox.y - diffY) //check if the moved box is on the button if (thisbox.x == thisbox.solveButton.x && thisbox.y == thisbox.solveButton.y) { thisbox.placed = true; lvl.solveCounter--; ghostBoxes.splice(bestPath, 1); } steps++; if (steps > 4000) { lvl.trash = true; break; } } //reset player position lvl.setPlayerPos(lvl.playerstartX, lvl.playerstartY); } function copyBoxes(lvl, used) { var newBoxes = []; for (var i = 0; i < lvl.boxes.length; i++) { newBoxes.push(new Box(lvl.boxes[i].x, lvl.boxes[i].y, lvl.boxes[i].solveButton)); lvl.nodes[lvl.boxes[i].x][lvl.boxes[i].y].occupied = true; lvl.nodes[lvl.boxes[i].x][lvl.boxes[i].y].used = used; } return newBoxes; } //calculate all boxpaths and return them in an array function CalcualteBoxPaths(lvl, ghostBoxes) { var boxPaths = []; for (var i = 0; i < ghostBoxes.length; i++) { var thisbox = ghostBoxes[i]; lvl.nodes[thisbox.x][thisbox.y].occupied = false; var solver = new Pathfinder(lvl, thisbox.x, thisbox.y, thisbox.solveButton.x, thisbox.solveButton.y) boxPaths.push(solver.returnPath(true)); lvl.nodes[thisbox.x][thisbox.y].occupied = true; } return boxPaths; } //return all player paths and the index of the lowest cost one function CalcualtePlayerPaths(lvl, ghostBoxes, boxPaths) { var playerPaths = []; var bestPath = -1; var lowestCost = 100000000; for (var i = 0; i < ghostBoxes.length; i++) { var newX = ghostBoxes[i].x; var newY = ghostBoxes[i].y; if (boxPaths[i][0][0].x == ghostBoxes[i].x + 1) newX -= 1; else if (boxPaths[i][0][0].x == ghostBoxes[i].x - 1) newX += 1; else if (boxPaths[i][0][0].y == ghostBoxes[i].y + 1) newY -= 1; else newY += 1; var solver = new Pathfinder(lvl, lvl.playerX, lvl.playerY, newX, newY); playerPaths.push(solver.returnPath(false)); if (playerPaths[i][1] < lowestCost) { lowestCost = playerPaths[i][1]; bestPath = i; } } return ([playerPaths, bestPath]); } // optimize the level by solving it in different ways and removing unnecessary free spots // repeatably optimizing yields different results, since there is a randomness to the algorithm const optPathCost = 4; //optimizer box path cost const optPlayerCost = 4; //optimizer player path cost function optimizeLvl(lvl, iterations) { var maxUnnecessary = []; var minDestroyWall = []; var bestPath = 0; var steps = 0; lvl.playerX = lvl.playerstartX; lvl.playerY = lvl.playerstartY; //make the playerpathcost positive var tempPlayerCost = playerPathCost; playerPathCost = optPlayerCost; //free the button positions for (var j = 0; j < lvl.buttons.length; j++) { lvl.nodes[lvl.buttons[j].x][lvl.buttons[j].y].occupied = false; lvl.nodes[lvl.buttons[j].x][lvl.buttons[j].y].used = true; } //solve the level randomly and check if nodes weren't visited for (var n = 0; n < iterations; n++) { //create ghostBoxes for solving var ghostBoxes = copyBoxes(lvl, true); //solve the level var solveCounter = ghostBoxes.length; var destroyWall = []; var trash = false; while (solveCounter > 0) { //randomly set pathcost for the boxes to negative in order to simulate nondirectional pushing of the boxes var tempCost = pathCost; pathCost = randomInt(-2, optPathCost); //calculate the paths from all boxes to their buttons var boxPaths = CalcualteBoxPaths(lvl, ghostBoxes); pathCost = tempCost; //calculate the player paths to all boxes and choose a RANDOM free path var playerPaths = CalcualtePlayerPaths(lvl, ghostBoxes, boxPaths)[0]; var bestPath = randomInt(0, playerPaths.length); var playerPath = playerPaths[bestPath][0]; var boxPath = boxPaths[bestPath][0]; // mark all nodes, that the player visited for (var i = 0; i < playerPath.length; i++) { playerPath[i].used = true; if (playerPath[i].wall) destroyWall.push(playerPath[i]); } //push the box into the solving direction until there is a turn var thisBox = ghostBoxes[bestPath]; var currentNode = boxPath[0]; var diffX = currentNode.x - thisBox.x; var diffY = currentNode.y - thisBox.y; //if the path is longer than 1, check for a turn var stop = 0; for (var i = 1; i < boxPath.length; i++) { var nextNode = boxPath[i]; if (true == false && diffX == nextNode.x - currentNode.x && diffY == nextNode.y - currentNode.y) currentNode = nextNode; else { stop = i - 1; break; } } //mark nodes on the box's path for (var i = 0; i <= stop; i++) { boxPath[i].used = true; if (boxPath[i].wall) destroyWall.push(boxPath[i]); } //set and mark new player and box positions lvl.nodes[thisBox.x][thisBox.y].occupied = false; thisBox.setPosition(boxPath[stop].x, boxPath[stop].y); lvl.nodes[thisBox.x][thisBox.y].occupied = true; lvl.setPlayerPos(thisBox.x - diffX, thisBox.y - diffY); //check if the moved box is on the button if (thisBox.x == thisBox.solveButton.x && thisBox.y == thisBox.solveButton.y) { thisBox.placed = true; solveCounter--; ghostBoxes.splice(bestPath, 1); } steps++; if (steps > 10000) { trash = true; break; } } //reset player position lvl.setPlayerPos(lvl.playerstartX, lvl.playerstartY) lvl.nodes[lvl.playerX][lvl.playerY].used = true; //check if a node is unnecessary; var unnecessary = []; for (var i = 0; i < lvl.nodes.length; i++) for (var j = 0; j < lvl.nodes[0].length; j++) { if (!lvl.nodes[i][j].wall && !lvl.nodes[i][j].used) unnecessary.push(lvl.nodes[i][j]); lvl.nodes[i][j].used = false; lvl.nodes[i][j].occupied = false; } if (!trash && unnecessary.length - destroyWall.length > maxUnnecessary.length - minDestroyWall.length) { maxUnnecessary = unnecessary; minDestroyWall = destroyWall; per = n; } } //remove the unnecessary free spaces for (var i = 0; i < maxUnnecessary.length; i++) maxUnnecessary[i].wall = true; for (var i = 0; i < minDestroyWall.length; i++) minDestroyWall[i].wall = false; playerPathCost = tempPlayerCost; console.log(String(maxUnnecessary.length) + " walls added, " + String(minDestroyWall.length) + " walls removed") } // optimize the level for all possible button-to-box combinations // this algorithm scales with n!, it should only be used with a low number of boxes function optimizeLvl2(lvl, iterations) { var boxPermutations = findPermutations(lvl.boxes, lvl.buttons); var tempBoxes = copyBoxes(lvl, false); for (var i = 0; i < boxPermutations.length; i++) { var thisPermutation = boxPermutations[i]; for (var j = 0; j < thisPermutation.length; j++) lvl.boxes[j] = new Box(thisPermutation[j][0].x, thisPermutation[j][0].y, thisPermutation[j][1]) optimizeLvl(lvl, iterations); } lvl.boxes = tempBoxes; } function lowCost(value) { return value[1] < 50; } //generate random level function generateLevel(w, h, numBoxes, optimizeIterations) { if (!w) w = randomInt(7, 16); if (w<7) w = 7; else if (w>15) w = 15; if (!h) h = randomInt(7, 16); if (h<7) h = 7; else if (h>15) h = 15; var minBoxes = Math.floor((w+h-6)/4); var maxBoxes = Math.floor(((w-4)*(h-4)-2)/2); if (!numBoxes) numBoxes = randomInt(minBoxes, maxBoxes); if (numBoxes<minBoxes) numBoxes = minBoxes; if (numBoxes>maxBoxes) numBoxes = maxBoxes; if (optimizeIterations==null) optimizeIterations = randomInt(-1000, 50000/(w*h)); var currentLvl; do{ currentLvl = new Level(w, h, numBoxes); //randomly remove walls from the level currentLvl.rip(randomInt(-2, 5)); //generate level generatePaths(currentLvl); } while(currentLvl.trash); if (numBoxes<5) optimizeLvl2(currentLvl, optimizeIterations); else optimizeLvl(currentLvl, optimizeIterations); var arr = []; for (var i=0;i<h;i++) arr[i] = []; currentLvl.nodes.forEach((col)=>{ col.forEach((cell)=>{ arr[cell.y][cell.x] = cell.wall ? 1 : (cell.hasBox ? 2 : 0); }); }); currentLvl.buttons.forEach((button)=>{ arr[button.y][button.x] = (arr[button.y][button.x]==0) ? 3 : 4; }); arr[currentLvl.playerstartY][currentLvl.playerstartX] = (arr[currentLvl.playerstartY][currentLvl.playerstartX]==0) ? 5 : 6; return { w: w, h: h, arr: arr }; } this.onmessage = function(msg){ if (msg.data=="1") postMessage(generateLevel()); } `], { type: "text/javascript" }))); worker.onmessage = (msg)=>{ if (!workerPromise || !workerPromiseResolve) return; workerPromiseResolve(optimizeForHaxball(msg.data)); workerPromise = null; workerPromiseResolve = null; }; setTimeout(nextStadium, 1000); /* fetch(downloadLink).then((x)=>{ x.text().then((t)=>{ var conversion = {" ": 0, "X": 1, "*": 2, ".": 3, "&": 4, "@": 5}; maps = t.split("*************************************\n").filter((y)=>y.length>0).map((y)=>{ var arr = y.split("\n").filter((z, i)=>((i==2 || i==3 || i>=6) && (z?.length>0))); var w = parseInt(arr[0].substring(arr[0].indexOf(": ")+2)); var h = parseInt(arr[1].substring(arr[1].indexOf(": ")+2)); var a = []; for (var i=0;i<h;i++){ var row = arr[i+2], row2 = Array(w).fill(0); for (var j=0;j<row.length;j++) row2[j] = conversion[row.charAt(j)]; a.push(row2); } return { w: w, h: h, tiles: a }; }); nextStadium(); }); }); */ }; this.finalize = function(){ worker?.terminate(); worker = null; workerPromise = null; workerPromiseResolve = null; that.room.librariesMap.commands?.remove("skip"); that.room.librariesMap.permissions?.removeContext(permissionCtx); permissionCtx = null; permissionIds = null; }; this.onGameStart = function(){ resetState(); setTimeout(updatePlayerDiscs, 100); }; this.onPlayerTeamChange = function(id, teamId, byId){ updatePlayerDiscs(); }; this.onAutoTeams = function(pid1, tid1, pid2, tid2, byId){ updatePlayerDiscs(); }; this.onOperationReceived = function(type, msg, globalFrameNo, clientFrameNo, customData){ if (type==OperationType.StartGame) return (mapProps!=null && workerPromise==null); if (type!=OperationType.SendInput || !gameState) return true; var p = that.room.players.find((x)=>x.id==msg.byId), d = p?.disc; if (!d) return true; var ix = d.pos.x/boxSize+(mapProps.w-1)/2, iy = d.pos.y/boxSize+(mapProps.h-1)/2; switch(msg.input){ case 1://up case 2://down case 4://left case 8://right var {dirX, dirY} = Utils.reverseKeyState(msg.input); var tt = gameState[iy+dirY][ix+dirX]; if (tt){ var {tile, discIds} = tt; if (tile==1 || tile==8) // wall return false; if (tile==2 || tile==4){ var tt2 = gameState[iy+2*dirY][ix+2*dirX]; if (!tt2 || tt2.tile==1 || tt2.tile==8 || tt2.tile==2 || tt2.tile==4) return false; if (tile==2 && tt2.tile==0){ tt.tile = 0; tt2.tile = 2; var discId = discIds.pop(); tt2.discIds.push(discId); that.room.setDiscProperties(discId, { x: (ix+2*dirX+0.5-mapProps.w/2)*boxSize, y: (iy+2*dirY+0.5-mapProps.h/2)*boxSize }); } else if (tile==2 && tt2.tile==3){ tt.tile = 0; tt2.tile = 4; var discId = discIds.pop(); tt2.discIds.push(discId); that.room.setDiscProperties(discId, { x: NaN, y: NaN }); var {color, radius} = tileProperties[tt2.tile]; that.room.setDiscProperties(tt2.discIds[0], { color: parseInt(color, 16), radius: radius }); checkEndGame(p); } else if (tile==4 && tt2.tile==0){ tt.tile = 3; tt2.tile = 2; var discId = discIds.pop(); tt2.discIds.push(discId); var {color, radius} = tileProperties[tt2.tile]; that.room.setDiscProperties(discId, { x: (ix+2*dirX+0.5-mapProps.w/2)*boxSize, y: (iy+2*dirY+0.5-mapProps.h/2)*boxSize, color: parseInt(color, 16), radius: radius }); var {color, radius} = tileProperties[tt.tile]; that.room.setDiscProperties(discIds[0], { x: (ix+dirX+0.5-mapProps.w/2)*boxSize, y: (iy+dirY+0.5-mapProps.h/2)*boxSize, color: parseInt(color, 16), radius: radius }); } else if (tile==4 && tt2.tile==3){ tt.tile = 3; tt2.tile = 4; var discId = discIds.pop(); tt2.discIds.push(discId); var {color, radius} = tileProperties[tt2.tile]; that.room.setDiscProperties(discId, { x: (ix+2*dirX+0.5-mapProps.w/2)*boxSize, y: (iy+2*dirY+0.5-mapProps.h/2)*boxSize, color: parseInt(color, 16), radius: radius }); var {color, radius} = tileProperties[tt.tile]; that.room.setDiscProperties(discIds[0], { x: (ix+dirX+0.5-mapProps.w/2)*boxSize, y: (iy+dirY+0.5-mapProps.h/2)*boxSize, color: parseInt(color, 16), radius: radius }); } } } that.room.setPlayerDiscProperties(p.id, { x: d.pos.x+dirX*boxSize, y: d.pos.y+dirY*boxSize }); break; case 16://kick break; } return false; }; };