UNPKG

morris

Version:

Nine Men's Morris implementation in Node.js

456 lines (411 loc) 18 kB
(function() { const helper = require("./helper"); var Board = require("morrisboard"); var Exception = require("./Exception"); class Morris { constructor(options) { var self = this; options = options.fillDefaults({ board: { rows: 3, points: [false, true, false], default: [] }, pieces: 9, rules: true, lastChangeset: null, teams: [] }); // Initialize morris board for game controller this.board = new Board(options.board.rows, options.board.points); // Try to fill the board with default teams if they are given this.board.map = this.board.map.map(function(point, i) { // If the current point has a related default team in default-object of options if (options.board.default[i]) { // set point's team to the default one point.team = options.board.default[i]; } // Return point because we are working with map method return point; }); // Just wether rule validating is active (Boolean) this.rules = options.rules; // By default, 'teams' is just an array conatining team names // Fill each one up to a team object this.teams = this.teams.map(function(teamName) { // Try to get the current team object from option's teams objects // Option's team objects are used to fill defaults var defaultTeam = options.teams.objectFromKey(teamName, "name"); // Team object that will repleace the team name var team = { name: teamName, // Fill up the given amount of pieces with own 'fill' prototype function of Array pieces: new Array(options.pieces).fill(function(index) { // Try to use properties from this piece within the defaultTeam given from options // But if this does not exist, use an empty piece instead return { point: (defaultTeam && "pieces" in defaultTeam && index in defaultTeam.pieces) ? defaultTeam.pieces[index].point : null, removed: (defaultTeam && "pieces" in defaultTeam && index in defaultTeam.pieces) ? defaultTeam.pieces[index].removed : false }; }), // Returns all active pieces within this team // "Active" means pieces, that are just not removed yet get activePieces() { // 'removed' property of a piece is always 'false' if the piece is not removed return this.pieces.filter(piece => !piece.removed); }, // Returns all pieces within this team that are on the board and moveable get moveablePieces() { // Filter for all pieces whose point property's type is number which excludes the 'null' value return this.activePieces.filter(piece => typeof piece.point === "number"); } }; return team; }); // Try to set an existing lastChangeset to the instance if one is given in options this.__lastChangeset = options.lastChangeset || null } // Calculates the next team's name that is used to move get nextTeam() { // Check wether there exist a last changeset if (this.__lastChangeset) { // Get the team name of the last change normally directly from the piece // Sometimes the chnageset contains a seperate 'team' property that is used if the piece that was used to interact with is not a part of the team that interacted // This case is when a "removement" happened (A piece will always be removed by the contrary team) var teamName = this.__lastChangeset.team || this.getTeam(this.__lastChangeset.piece); var teamIndex = this.teams.indexOfKey(teamName, "name"); // Try to just use the "next" team in 'teams' array but if the current one is the last one, jump over to the first one return (teamIndex + 1) in this.teams ? this.teams[teamIndex + 1].name : this.teams[0].name; } // No last changeset, return first team's name because this one is always starting the match return this.teams[0].name; } // Calculates the next action that should be performed get nextAction() { // Check wether there exist a last changeset if (this.__lastChangeset) { // Check wether the target point of the last changeset (last movement) is involved in at least one mill // That means, that this mill was created with this movement (last changeset) if (this.__lastChangeset.targetPoint.mills.length > 0) { // Next action should be removed // Please keep in mind that the "nextTeam" now is the contrary team of the team that created the mill. This may looks not correct but it is absolutely correct because we will remove the piece from this contrary team return "remove"; } } // The last changeset's target point seems not to be a part of a created mill which means that we will get the next action just by checking the current game's phase (0-2) var phaseActions = [ "set", "move", "move" ]; // Return the action that is related to current pahse of the game return phaseActions[this.phase]; } // Returns current pahse of the game get phase() { // Loop trough teams and check wether they have at least one unused piece (Phase 1) for (var team of this.teams) { // Returns the index of first piece object that's 'point' property is null of current team // If this index is not -1, such a piece object exists and phause 1 is still active because there is a team that is allowed to 'set()' if (team.pieces.indexOfKey(null, "point") > -1) { // Return 0 to represent phase 1 return 0; } } // No piece found that's "point" property is null // Phase 1 is completed, looking for phase 2 or 3 ... // Loop trough teams to check wether they have three or less active (unremoved) pieces on board for (var team of this.teams) { // Filter the pieces array for a false in "removed" property // If the returning array's length is three or less, phase 2 is completed and phase 3 is the current one if (team.activePieces.length <= 3) { // Return 2 to represent phase 3 return 2; } } // There exist no unused pieces but no team has three or less active pieces // Return 1 to represent phase 2 return 1; } // Returns wether the game is game over get gameOver() { // Loop trough all teams to check wether they have more than 0 active pieces for (var team of this.teams) { // Check the current team for having no active pieces if (team.activePieces.length <= 0) { // Return true because obviously the game is over return true; } } // Every team has at least one active piece // Game is still running, return false return false; } // Returns wether the game is a draw get draw() { // Game has to not to be over and there should be no possible move return (!this.gameOver && this.getMoves().length <= 0); } // Get piece by position object getPiece(pos) { var index = this.board.getPointIndex(pos.row, pos.position); for (var team of this.teams) { var relatedPiece = team.pieces.objectFromKey(index, "point"); if (relatedPiece) { return relatedPiece; } } } // Method to set a piece to a point and validate this movement with the rules set(options, sandbox = false) { // Get the point object by using its position given from options var targetPoint = this.board.getPoint(options.row, options.position); // Prototype the changeset var changeset = { success: false, action: "set", piece: null, targetPoint: targetPoint }; // Errors will be thrown by adding them to the changeset // Anonymous function to use the return break (() => { // If the target point is invalid, throw an error if (!targetPoint) { changeset.error = new Exception("Point is invalid or does not exist", 3, "data"); return; } // Get team object from options "team" prooperty var team = this.teams.objectFromKey(options.team, "name"); // If this team is inavlid, throw an error if (!team) { changeset.error = new Exception("Team is invalid", 0, "data"); return; } // If the next team that is allowed to interact is not the team that is tried to set and rule validation is allowed if (this.rules && this.nextTeam != options.team) { changeset.error = new Exception("Team is not allowed to set", 2, "rule"); return; } // Get first piece that isn't setted yet var piece = team.pieces.objectFromKey(null, "point"); // If there exist a piece that should be edited if (piece) { changeset.piece = piece; // Piece is valid // If target point has no piece on it or if it has one, rule validation has to be disabled if (!targetPoint.team || !this.rules) { // Movement is okay // But change only something really if sandbox is d if (!sandbox) { // Set the piece on the board and return its index var settedIndex = this.board.set(options); // Set the related piece's point property to this index to relate the piece to the point on the board piece.point = settedIndex; } } else { changeset.error = new Exception("Point (target) is already used by piece.", 4, "rule", { targetPoint: targetPoint }); } } else { changeset.error = new Exception("Team has no unused pieces anymore", 1, "data"); } })(); // If no error occured if (!changeset.error) { // Set success of the changeset to true changeset.success = true; // If sandbox is not used, the successfull movement will be saved in the last changeset property if (!sandbox) { this.__lastChangeset = changeset; } } // Return changeset return changeset; } move(from, to, sandbox = false) { // Get the indexes of the points we are working with var fromIndex = this.board.getPointIndex(from.row, from.position); var toIndex = this.board.getPointIndex(to.row, to.position); // Get the point objects var startPoint = this.board.map[fromIndex]; var targetPoint = this.board.map[toIndex]; // Protoytpe changeset var changeset = { success: false, action: "move", piece: null, startPoint: startPoint, targetPoint: targetPoint }; // Errors will be thrown by adding them to the changeset // Anonymous function to use the return break (() => { // If start or target point is not valid, throw an error if (!startPoint || !targetPoint) { changeset.error = new Exception("Cannot move piece. Start point or target point is invalid or does not exist", 3, "data"); return; } // If the start point's team is false which means that the point has no piece on it // And therefore, you can not move any piece from it to another point if (!startPoint.team) { changeset.error = new Exception("Cannot move piece from point. No piece on this point", 4, "data"); return; } // Get the piece standing on the start point var piece = this.getPiece(from); // Refer this piece to the changeset changeset.piece = piece; // Validate movement with rules if (this.validateMovement(startPoint, targetPoint) || !this.rules) { // If validating was successfully or rules are disabled, go on if (!sandbox) { this.board.move(fromIndex, toIndex); piece.point = toIndex; } } else { changeset.error = new Exception("Invalid movement. Cannot move piece over such a distance or this team is not allowed to move", 5, "rule"); } })(); if (!changeset.error) { changeset.success = true; if (!sandbox) { this.__lastChangeset = changeset; } } return changeset; } remove(point, sandbox = false) { var targetPoint = this.board.getPoint(point.row, point.position); var startIndex = this.board.map.indexOf(targetPoint); var piece = this.teams.objectFromKey(targetPoint.team, "name").pieces.objectFromKey(startIndex, "point"); var changeset = { success: false, action: "remove", piece: piece, // Define team explictily because the piece who was used to interact with is defintly not a piece of the team that is doing the removement team: this.getTeam(this.__lastChangeset.piece), targetPoint: targetPoint }; if (targetPoint && typeof piece.point === "number") { if ((targetPoint.team === this.nextTeam && this.nextAction === "remove" && targetPoint.mills.length <= 0) || !this.rules) { if (!sandbox) { this.board.remove(startIndex); piece.removed = true; piece.point = undefined; } } else { changeset.error = new Exception("Not allowed to remove pieces of this team", 7, "rule"); } } else { changeset.error = new Exception("Piece or point is invalid or does not exist", 3, "data"); } if (!changeset.error) { changeset.success = true; if (!sandbox) { this.__lastChangeset = changeset; } } return changeset; } // Method to validate a movement in general by using all the aspects of Nine Men's Morris rules validateMovement(from, to) { // Get reference from point to related team var teamRef = this.teams.objectFromKey(from.team, "name"); //console.log(teamRef.name, Board.isSurrounding(from, to), this.board.map.indexOf(from.surroundings.right)); // Check wether the related team has 3 or less active (unremoved) pieces (Jumping is allowed) or if not, the target point borders on the point directly return ((teamRef.activePieces.length <= 3 || Board.isSurrounding(from, to)) && this.nextTeam === from.team && !to.team); } getTeam(piece) { for (var team of this.teams) { if (team.pieces.includes(piece)) { return team.name; } } /*for (var team of this.teams) { if (team.pieces.objectFromKey(piece.point, "point")) { return team.name; } }*/ return null; } getMoves() { var self = this; var movements = []; var actions = { set() { // Returns all empty points on the board which have no piece on it (team: false) var emptyPoints = self.board.map.filter(point => !point.team); // Loop trough all possible target points emptyPoints.forEach(function(point) { // Assign setting options from point's position and the next team var pointInfo = Object.assign({ team: self.nextTeam }, point.position); // Set this point for the next team in sandbox mode (true) var testSet = self.set(pointInfo, true); // Sandbox setting was successfull, add it to array if (testSet.success) { // Push an movement describing object to the array 'movements' movements.push({ action: "set", team: pointInfo.team, targetPoint: point }); } }); }, move() { var usedPoints = self.board.map.filter(point => point.team); // Loop trough all used points on the board and check wether the pieces on them can be moved to a different point usedPoints.forEach(startPoint => { // Loop trough the whole board now and check wether 'startPoint's piece can be moved to the current target point self.board.map.forEach(targetPoint => { // Validate a theoretically movement between these two points if (self.validateMovement(startPoint, targetPoint)) { // Such a movement seems to be valid // Push a movement describing object to the array 'movements' movements.push({ action: "move", startPoint: startPoint, targetPoint: targetPoint }); } }); }); }, remove() { var usedPoints = self.board.map.filter(point => point.team); // Loop trough all used points on the board and check wether the pieces on them can be moved to a different point usedPoints.forEach(targetPoint => { // Remove piece in sandbox mode and check the success var removement = self.remove(targetPoint.position, true); // Wether the removement is allowed if (removement.success) { // Push a movement describing object to the array 'movements' movements.push({ action: "remove", targetPoint: targetPoint }); } }); } }; actions[this.nextAction](); return movements; } } Morris.prototype.teams = ["white", "black"]; if ("process" in this) { module.exports = Morris; } else { window.MorrisGame = Morris; } })();