UNPKG

@ayshrj/ludo.js

Version:

A TypeScript-based headless Ludo game engine for simulating game logic, AI moves, and game state management.

2 lines (1 loc) 7.48 kB
import{EventEmitter as A}from"events";function b(p,C){return Math.floor(Math.random()*(C-p+1))+p}function T(){return{red:[-1,-1,-1,-1],green:[-1,-1,-1,-1],yellow:[-1,-1,-1,-1],blue:[-1,-1,-1,-1]}}var g=class extends A{constructor(i=4){super();this.numberOfPlayers=i;this.ranking=[];this.currentDiceRoll=null;this.lastDiceRoll=null;this.validTokenIndices=[];this.currentConsecutiveSixes=0;this.TRACK_LENGTH=57;this.safeZones=[0,8,13,21,26,34,39,47];this.currentBoardStatus="";this.gameState="playerHasToRollADice";this.players=[];i===2?this.players=["blue","green"]:i===3?this.players=["blue","red","green"]:this.players=["blue","red","green","yellow"],this.reset()}emitStateChange(){this.emit("stateChange",this.getCurrentState())}reset(){this.board=Array.from({length:15},()=>Array.from({length:15},()=>null)),this.tokenPositions=T(),this.ranking=[],this.currentDiceRoll=null,this.lastDiceRoll=null,this.validTokenIndices=[],this.currentConsecutiveSixes=0,this.currentBoardStatus="",this.gameState="playerHasToRollADice";let i=b(0,this.players.length-1);this.currentPiece=this.players[i];let n={red:[[1,1],[1,4],[4,1],[4,4]],green:[[1,10],[1,13],[4,10],[4,13]],yellow:[[10,10],[10,13],[13,10],[13,13]],blue:[[10,1],[10,4],[13,1],[13,4]]};for(let t of this.players)n[t].forEach(([l,s])=>{this.board[l][s]={isHome:t}});let e=[];for(let t=1;t<=5;t++)e.push([6,t]);for(let t=5;t>=0;t--)e.push([t,6]);for(let t=7;t<=8;t++)e.push([0,t]);for(let t=1;t<=5;t++)e.push([t,8]);for(let t=9;t<=14;t++)e.push([6,t]);for(let t=7;t<=8;t++)e.push([t,14]);for(let t=13;t>=9;t--)e.push([8,t]);for(let t=9;t<=14;t++)e.push([t,8]);for(let t=7;t>=6;t--)e.push([14,t]);for(let t=13;t>=9;t--)e.push([t,6]);for(let t=5;t>=0;t--)e.push([8,t]);for(let t=0;t<=6;t++)e.push([7,t]);let a={red:e,green:e.map(t=>this.rotateCoord(t,90)),yellow:e.map(t=>this.rotateCoord(t,180)),blue:e.map(t=>this.rotateCoord(t,270))};this.colorPaths=a;for(let t of this.players)a[t].forEach(([s,r],o)=>{this.board[s][r]||(this.board[s][r]={}),t==="red"&&(this.board[s][r].redTrack=o),t==="green"&&(this.board[s][r].greenTrack=o),t==="blue"&&(this.board[s][r].blueTrack=o),t==="yellow"&&(this.board[s][r].yellowTrack=o),this.safeZones.includes(o)&&(this.board[s][r].isSafeZone=!0),o===0&&(this.board[s][r].isStartingPosition=t,this.board[s][r].isSafeZone=!0),o>=51&&o<=55&&(this.board[s][r].isOnPathToFinalPosition=t),o===56&&(this.board[s][r].isFinalPosition=t)});this.emitStateChange()}rotateCoord([i,n],e){return e===90?[7+(n-7),7-(i-7)]:e===180?[14-i,14-n]:[7-(n-7),7+(i-7)]}rollDiceForCurrentPiece(){if(this.gameState!=="playerHasToRollADice")return this.currentBoardStatus=`Invalid action. Current state: ${this.gameState}.`,this.emitStateChange(),-1;if(this.currentDiceRoll!==null)return this.currentBoardStatus="Already rolled. You must move a token or wait/pass.",this.emitStateChange(),this.currentDiceRoll;let i=b(1,6);if(this.currentDiceRoll=i,this.lastDiceRoll=i,i===6){if(this.currentConsecutiveSixes++,this.currentConsecutiveSixes===3)return this.currentBoardStatus=`Three consecutive sixes. Turn skipped for ${this.currentPiece}.`,this.resetTurnState(!1),this.nextTurn(),this.emitStateChange(),i}else this.currentConsecutiveSixes=0;return this.validTokenIndices=this.getValidMoves(this.currentPiece,i),this.validTokenIndices.length===0?(this.currentBoardStatus=`No valid moves for ${this.currentPiece} (rolled ${i}). Passing turn.`,this.resetTurnState(!1),this.nextTurn(),this.emitStateChange()):(this.gameState="playerHasToSelectAPosition",this.emitStateChange()),i}selectToken(i){if(this.gameState!=="playerHasToSelectAPosition"){this.currentBoardStatus=`Invalid action. State: ${this.gameState}`,this.emitStateChange();return}if(this.currentDiceRoll===null){this.currentBoardStatus="You must roll before selecting a token.",this.emitStateChange();return}if(!this.validTokenIndices.includes(i)){this.currentBoardStatus="That token is not a valid choice.",this.emitStateChange();return}let n=this.currentDiceRoll,e=this.tokenPositions[this.currentPiece][i],a;if(e===-1){if(n!==6){this.currentBoardStatus="Cannot leave home without rolling a 6.",this.emitStateChange();return}a=0}else{if(e+n>this.TRACK_LENGTH-1){this.currentBoardStatus="Move would go beyond final square. Invalid.",this.emitStateChange();return}a=e+n}this.tokenPositions[this.currentPiece][i]=a;let t=0;if(a!==56&&!this.safeZones.includes(a)&&(t=this.handleCollisions(a,this.currentPiece)),a===56&&this.tokenPositions[this.currentPiece].every(s=>s===56)&&!this.ranking.includes(this.currentPiece)&&this.ranking.push(this.currentPiece),this.resetTurnState(!1),t>0){this.currentBoardStatus=`${this.currentPiece} captured ${t} token(s). Roll again!`,this.gameState="playerHasToRollADice",this.emitStateChange();return}if(n===6){this.currentBoardStatus=`${this.currentPiece} rolled a 6. Roll again!`,this.gameState="playerHasToRollADice",this.emitStateChange();return}this.nextTurn(),this.emitStateChange()}getValidMoves(i,n){let e=this.tokenPositions[i],a=[];for(let t=0;t<4;t++){let l=e[t];l!==56&&(l===-1?n===6&&a.push(t):l+n<=56&&a.push(t))}return a}handleCollisions(i,n){let e=0,[a,t]=this.colorPaths[n][i];for(let l of this.players)if(l!==n)for(let s=0;s<4;s++){let r=this.tokenPositions[l][s];if(r<0||r===56)continue;let[o,d]=this.colorPaths[l][r];o===a&&d===t&&(this.tokenPositions[l][s]=-1,e++)}return e}nextTurn(){if(this.ranking.length>=this.players.length){this.gameState="gameFinished",this.currentBoardStatus="Game Over! All players finished.";return}let i=this.players.indexOf(this.currentPiece);this.currentPiece=this.players[(i+1)%this.players.length],this.currentConsecutiveSixes=0,this.gameState="playerHasToRollADice",this.currentBoardStatus=`Now it's ${this.currentPiece}'s turn to roll.`}resetTurnState(i=!0){this.currentDiceRoll=null,this.validTokenIndices=[],i&&(this.lastDiceRoll=null)}getCurrentState(){return{turn:this.currentPiece,tokenPositions:this.tokenPositions,ranking:this.ranking,boardStatus:this.currentBoardStatus,diceRoll:this.currentDiceRoll,lastDiceRoll:this.lastDiceRoll,gameState:this.gameState,players:this.players}}bestMove(){if(this.currentDiceRoll===null)return console.warn("bestMove called but no dice roll available"),-1;let i=this.currentDiceRoll,n=this.getValidMoves(this.currentPiece,i);if(n.length===0)return-1;let e={CAPTURE_BONUS:50,LEAVE_HOME_BONUS:35,LAND_SAFE_ZONE_BONUS:25,APPROACH_FINAL_BONUS:15,REACH_FINAL_BONUS:100,DISTANCE_ADVANCE_FACTOR:.5,RISK_PENALTY_NEAR_OPPONENT:40},a=Number.NEGATIVE_INFINITY,t=n[0];for(let l of n){let s=this.tokenPositions[this.currentPiece][l],r=s===-1?0:s+i,o=0;if(s===-1&&i===6&&(o+=e.LEAVE_HOME_BONUS),this.safeZones.includes(r)&&(o+=e.LAND_SAFE_ZONE_BONUS),r!==56&&!this.safeZones.includes(r)){let[d,P]=this.colorPaths[this.currentPiece][r],c=0;for(let h of this.players)if(h!==this.currentPiece)for(let u=0;u<4;u++){let f=this.tokenPositions[h][u];if(f<0||f===56)continue;let[m,S]=this.colorPaths[h][f];m===d&&S===P&&c++}c>0&&(o+=c*e.CAPTURE_BONUS)}if(r>=51&&r<56&&(o+=e.APPROACH_FINAL_BONUS),r===56&&(o+=e.REACH_FINAL_BONUS),o+=r*e.DISTANCE_ADVANCE_FACTOR,r<56){let[d,P]=this.colorPaths[this.currentPiece][r],c=0;for(let h of this.players)if(h!==this.currentPiece)for(let u=0;u<4;u++){let f=this.tokenPositions[h][u];if(!(f<0||f===56))for(let m=1;m<=6;m++){let S=f+m;if(S<=56){let[k,R]=this.colorPaths[h][S];if(k===d&&R===P){c++;break}}}}c>0&&(o-=c*e.RISK_PENALTY_NEAR_OPPONENT)}o>a&&(a=o,t=l)}return t}};export{g as Ludo,T as initializeTokenPosition};