diff --git a/.gitignore b/.gitignore index c1f2cba..bdb7604 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ nbproject /app/server/db npm-debug.log* *.*~* +package-lock.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c9dcf18 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Run Server", + "program": "${workspaceFolder}/app/server/server.js", + "request": "launch", + "skipFiles": [ + "/**" + ], + "type": "node" + }, + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${file}" + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 52223bd..765d986 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,25 @@ # Using alpine image, because it is super slim FROM alpine +ARG USER=default +ENV HOME /home/$USER + # Install only bash and nodejs, then remove cached package data -RUN apk add --update bash && apk add --update nodejs nodejs-npm && rm -rf /var/cache/apk/* +RUN apk add --update bash && apk add --update nodejs npm git && rm -rf /var/cache/apk/* # Create app directory. This is where source code will be copied to RUN mkdir -p /usr/src/app WORKDIR /usr/src/app +# Create a user for running npm install +# add new user +RUN adduser -D $USER \ + && mkdir -p /etc/sudoers.d \ + && echo "$USER ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$USER \ + && chmod 0440 /etc/sudoers.d/$USER +# USER $USER +# WORKDIR $HOME + # Copy source from host to directory in container COPY . /usr/src/app diff --git a/app/browser/index.html b/app/browser/index.html index 9c455bd..24453dd 100644 --- a/app/browser/index.html +++ b/app/browser/index.html @@ -4,12 +4,12 @@ - - + + - + - + - + @@ -69,25 +69,27 @@ -
-
-

Backgammon.js
Extensible multiplayer game

+
+
+

Upgammon
+ From Bangkok to Chang Mai to now +

-
-
+ +
+
@@ -96,7 +98,7 @@

Backgammon.js
Extensible multiplayer game - + - + @@ -125,12 +127,12 @@

Backgammon.js
Extensible multiplayer game

- + - + - +
diff --git a/app/browser/js/SimpleBoardUI.js b/app/browser/js/SimpleBoardUI.js index d660d59..31af827 100644 --- a/app/browser/js/SimpleBoardUI.js +++ b/app/browser/js/SimpleBoardUI.js @@ -18,7 +18,7 @@ function SimpleBoardUI(client) { * @type {Client} */ this.client = client; - + /** * @type {Match} */ @@ -33,7 +33,8 @@ function SimpleBoardUI(client) { this.container = $('#' + this.client.config.containerID); this.container.append($('#tmpl-board').html()); this.container.append($('
')); - + this.displayPieceId = true; + this.board = $('#board'); this.fields = []; for (var i = 0; i < 4; i++) { @@ -58,30 +59,30 @@ function SimpleBoardUI(client) { n += n / Math.pow(2, 53); // added 1360765523: 17.56.toFixedDown(2) === "17.56" return n.toFixed(digits); }; - + this.notifyOhSnap = function (message, params) { if (!params.duration) { params.duration = 1500; } ohSnap(message, params); }; - + this.notifyInfo = function (message, timeout) { this.notifyOhSnap(message, {color: 'blue', duration: timeout}); }; - + this.notifyPositive = function (message, timeout) { this.notifyOhSnap(message, {color: 'green', duration: timeout}); }; - + this.notifyNegative = function (message, timeout) { this.notifyOhSnap(message, {color: 'red', duration: timeout}); }; - + this.notifySuccess = function (message, timeout) { this.notifyOhSnap(message, {color: 'green', duration: timeout}); }; - + this.notifyError = function (message, timeout) { this.notifyOhSnap(message, {color: 'red', duration: timeout}); }; @@ -89,7 +90,7 @@ function SimpleBoardUI(client) { this.getPointElem = function (pos) { return $('#point' + pos); }; - + this.getPieceElem = function (piece) { return $('#piece' + piece.id); }; @@ -109,7 +110,7 @@ function SimpleBoardUI(client) { } return null; }; - + this.getBarElem = function (type) { var barID = (type === this.client.player.currentPieceType) ? 'top-bar' : 'bottom-bar'; var bar = $('#' + barID); @@ -119,10 +120,10 @@ function SimpleBoardUI(client) { this.getBarTopPieceElem = function (type) { var barElem = this.getBarElem(type); var pieceElem = barElem.find('div.piece').last(); - + return pieceElem; }; - + this.getBarTopPiece = function (type) { var pieceElem = this.getBarTopPieceElem(type); if (pieceElem) { @@ -134,14 +135,15 @@ function SimpleBoardUI(client) { this.getPieceByID = function (id) { return $('#piece' + id); }; - + /** * Handles clicking on a point (position) + * Add [SHIFT] to move UP */ this.handlePointClick = function (e) { var self = e.data; var game = self.match.currentGame; - + console.log('mousedown click', game); if (!model.Game.hasMoreMoves(game)) { return; @@ -160,13 +162,18 @@ function SimpleBoardUI(client) { var position = $(e.currentTarget).data('position'); var piece = self.getTopPiece(position); if (piece) { - self.client.reqMove(piece, steps); + // If shift key was pressed, move UP by height=steps + if (e.shiftKey === true) { + self.client.reqUp(piece, steps); + } else { + self.client.reqMove(piece, steps); + } } e.preventDefault(); }; - + /** - * Handles clicking on a point (position) + * Handles clicking on bar */ this.handleBarClick = function (e) { var self = e.data; @@ -189,17 +196,17 @@ function SimpleBoardUI(client) { var pieceElem = $(e.currentTarget).find('div.piece').last(); var piece = pieceElem.data('piece'); if (piece) { - self.client.reqMove(piece, steps); + self.client.reqMove(piece, steps); } e.preventDefault(); }; - + /** * Assign actions to DOM elements */ this.assignActions = function () { var self = this; - + // Game actions $('#btn-roll').unbind('click'); $('#btn-roll').click(function (e) { @@ -212,24 +219,24 @@ function SimpleBoardUI(client) { $('#btn-confirm').click(function (e) { self.client.reqConfirmMoves(); }); - + $('#btn-undo').unbind('click'); $('#btn-undo').click(function (e) { self.client.reqUndoMoves(); }); - + $('#menu-undo').unbind('click'); $('#menu-undo').click(function (e) { $('.navbar').collapse('hide'); self.client.reqUndoMoves(); }); - + $('#menu-resign').unbind('click'); $('#menu-resign').click(function (e) { // Ask player if they want to resign from current game only // or abandon the whole match $('.navbar').collapse('hide'); - + BootstrapDialog.show({ title: 'Resign from game or match?', type: BootstrapDialog.TYPE_DEFAULT, @@ -263,34 +270,34 @@ function SimpleBoardUI(client) { ] }); }); - + if ((!this.match) || (!this.match.currentGame) || (!this.client.player)) { return; } - + // Actions for points for (var pos = 0; pos < 24; pos++) { var pointElem = this.getPointElem(pos); $(document).on('contextmenu', pointElem, function(e){ // Block browser menu - return false; + // return false; }); pointElem.unbind('mousedown'); pointElem.mousedown(self, self.handlePointClick); } - + // Actions for bar for (var pieceType = 0; pieceType <= model.PieceType.BLACK; pieceType++) { console.log('pieceType', pieceType); var barElem = this.getBarElem(pieceType); console.log(barElem); - + $(document).on('contextmenu', barElem, function(e){ // Block browser menu - return false; + // return false; }); - + barElem.unbind('mousedown'); barElem.mousedown(self, self.handleBarClick); } @@ -310,9 +317,9 @@ function SimpleBoardUI(client) { - Field 1 - bottom left - Field 2 - top right - Field 3 - bottom right - + Fields are arrange on the board in the following way: - + +12-13-14-15-16-17------18-19-20-21-22-23-+ | | | | | Field 0 | | Field 2 | @@ -326,38 +333,38 @@ function SimpleBoardUI(client) { | Field 1 | | Field 3 | | | | | +11-10--9--8--7--6-------5--4--3--2--1--0-+ -1 - + */ - + var pieceType = this.client.player.currentPieceType; var i; var k; var typeClass; - + for (i = 12; i < 18; i++) { typeClass = i % 2 === 0 ? 'even' : 'odd'; - + k = (pieceType === model.PieceType.BLACK) ? i - 12 : i; this.createPoint(this.fields[0], k, typeClass); } for (i = 11; i >= 6; i--) { typeClass = i % 2 === 0 ? 'even' : 'odd'; - + k = (pieceType === model.PieceType.BLACK) ? i + 12 : i; this.createPoint(this.fields[1], k, typeClass); } for (i = 18; i < 24; i++) { typeClass = i % 2 === 0 ? 'even' : 'odd'; - + k = (pieceType === model.PieceType.BLACK) ? i - 12 : i; this.createPoint(this.fields[2], k, typeClass); } for (i = 5; i >= 0; i--) { typeClass = i % 2 === 0 ? 'even' : 'odd'; - + k = (pieceType === model.PieceType.BLACK) ? i + 12 : i; this.createPoint(this.fields[3], k, typeClass); } @@ -366,12 +373,20 @@ function SimpleBoardUI(client) { this.createPiece = function (parentElem, piece, count) { var pieceTypeClass = piece.type === model.PieceType.WHITE ? 'white' : 'black'; - var pieceElem = $('
 
'); + var pieceElem = $( + '
' + + (this.displayPieceId ? piece.id : " ") + + "
" + ); pieceElem.data('piece', piece); parentElem.append(pieceElem); }; - + /** * Compact pieces in all positions */ @@ -382,7 +397,7 @@ function SimpleBoardUI(client) { this.compactElement(this.getBarElem(model.PieceType.WHITE), this.client.player.currentPieceType === model.PieceType.WHITE ? 'top' : 'bottom'); this.compactElement(this.getBarElem(model.PieceType.BLACK), this.client.player.currentPieceType === model.PieceType.BLACK ? 'top' : 'bottom'); }; - + /** * Compact pieces in specific DOM element to make them fit vertically. * @param {number} pos - Position of point @@ -406,19 +421,27 @@ function SimpleBoardUI(client) { // margin in percent = 100 - ((8 / 88) * 100) ratio = 100 - (((overflow / (itemCount - 1)) / itemHeight) * 100); } - + if (ratio > 100) { ratio = 100; } if (ratio <= 0) { ratio = 1; } - + var self = this; element.children().each(function(i) { var marginPercent = ratio * i; var negAlignment = (alignment === 'top') ? 'bottom' : 'top'; - + + // push up last piece if height override is set + if (i === itemCount - 1) { + const height = $(this).data('height'); + if (height) { + marginPercent = ratio * (i + height); + } + // $(this).removeData('height'); + } $(this).css(alignment, "0"); $(this).css("margin-" + alignment, self.toFixedDown(marginPercent, 2) + "%"); @@ -435,14 +458,14 @@ function SimpleBoardUI(client) { this.compactPosition = function (pos) { var pointElement = this.getPointElem(pos); var alignment; - + if (this.client.player.currentPieceType === model.PieceType.BLACK) { alignment = ((pos >= 0) && (pos <= 11)) ? 'top' : 'bottom'; } else { alignment = ((pos >= 12) && (pos <= 23)) ? 'top' : 'bottom'; } - + this.compactElement(pointElement, alignment); }; @@ -450,7 +473,7 @@ function SimpleBoardUI(client) { var game = this.match.currentGame; var i, pos; var b; - + for (pos = 0; pos < game.state.points.length; pos++) { var point = game.state.points[pos]; for (i = 0; i < point.length; i++) { @@ -459,8 +482,8 @@ function SimpleBoardUI(client) { } this.compactPosition(pos); } - - + + for (b = 0; b < game.state.bar.length; b++) { var bar = game.state.bar[b]; for (i = 0; i < bar.length; i++) { @@ -479,13 +502,13 @@ function SimpleBoardUI(client) { this.removePieces = function () { var game = this.match.currentGame; - + for (var pos = 0; pos < game.state.points.length; pos++) { var point = game.state.points[pos]; var pointElem = this.getPointElem(pos); pointElem.empty(); } - + this.getBarElem(model.PieceType.BLACK).empty(); this.getBarElem(model.PieceType.WHITE).empty(); }; @@ -504,24 +527,24 @@ function SimpleBoardUI(client) { this.createPoints(); this.createPieces(); - + this.randomizeDiceRotation(); - + this.assignActions(); this.updateControls(); this.updateScoreboard(); - + this.compactAllPositions(); }; - + this.handleTurnStart = function () { this.randomizeDiceRotation(); }; - + this.handleEventUndoMoves = function () { this.notifyInfo('Player undid last move.'); }; - + this.handleEventGameRestart = function () { var yourscore = this.match.score[this.client.player.currentPieceType]; var oppscore = this.match.score[this.client.otherPlayer.currentPieceType]; @@ -541,7 +564,7 @@ function SimpleBoardUI(client) { this.randomizeDiceRotation = function () { this.rotationAngle = []; for (var i = 0; i < 10; i++) { - this.rotationAngle[i] = Math.random() * 30 - 15; + this.rotationAngle[i] = Math.random() * 30 - 15; } }; @@ -555,7 +578,7 @@ function SimpleBoardUI(client) { $('#menu-undo').hide(); return; } - + var game = this.match.currentGame; $('#btn-roll').toggle( @@ -565,7 +588,7 @@ function SimpleBoardUI(client) { (!model.Game.diceWasRolled(game)) && (!game.turnConfirmed) ); - + var canConfirmMove = game.hasStarted && (!game.isOver) && @@ -580,10 +603,10 @@ function SimpleBoardUI(client) { model.Game.isPlayerTurn(game, this.client.player) && model.Game.diceWasRolled(game) && (!game.turnConfirmed); - + $('#btn-confirm').toggle(canConfirmMove); $('#btn-undo').toggle(canConfirmMove); - + $('#menu-resign').toggle(game.hasStarted && (!game.isOver)); $('#menu-undo').toggle(canUndoMove); @@ -602,12 +625,12 @@ function SimpleBoardUI(client) { console.log('Game:', game); console.log('Player:', this.client.player); }; - + this.updateScoreboard = function () { if ((!this.match) || (!this.match.currentGame)) { return; } - + var isInMatch = (this.match.currentGame); var matchText = (isInMatch) ? 'Match "' + this.rule.title + '", ' + this.match.length + '/' + this.match.length @@ -619,7 +642,7 @@ function SimpleBoardUI(client) { 'Match has not been started'; $('#match-state').text(matchText); $('#match-state').attr('title', matchTextTitle); - + var yourscore = this.match.score[this.client.player.currentPieceType]; $('#yourscore').text(yourscore); @@ -631,42 +654,42 @@ function SimpleBoardUI(client) { $('#oppscore').text(''); } }; - + this.showGameEndMessage = function (winner, resigned) { $('#game-result-overlay').show(); - + var result = winner.id === this.client.player.id; var message; var matchState; - + if (resigned) { message = (result) ? 'Other player resigned!' : 'You resigned.'; } else { message = (result) ? 'You WON!' : 'You lost.'; } - + matchState = 'Match standing '; if (this.match.isOver) { message += message = ' Match is over.'; matchState = 'Match result '; } - + var color = (result) ? 'green' : 'red'; - + $('.game-result').css('color', color); $('.game-result .message').html(message); $('.game-result .state').html(matchState); - + var yourscore = this.match.score[this.client.player.currentPieceType]; var oppscore = this.match.score[this.client.otherPlayer.currentPieceType]; $('.game-result .yourscore').text(yourscore); $('.game-result .oppscore').text(oppscore); - + $('.game-result .text').each(function () { fitText($(this)); }); - + if (resigned) { this.notifyInfo('Other player resigned from game'); } @@ -682,24 +705,24 @@ function SimpleBoardUI(client) { this.updateDie = function (dice, index, type) { var color = (type === model.PieceType.BLACK) ? 'black' : 'white'; var id = '#die' + index; - + // Set text $(id).text(dice.values[index]); - + // Change image $(id).removeClass('digit-1-white digit-2-white digit-3-white digit-4-white digit-5-white digit-6-white digit-1-black digit-2-black digit-3-black digit-4-black digit-5-black digit-6-black played'); $(id).addClass('digit-' + dice.values[index] + '-' + color); if (dice.movesLeft.length === 0) { $(id).addClass('played'); } - + var angle = this.rotationAngle[index]; $(id).css('transform', 'rotate(' + angle + 'deg)'); }; /** * Recreate DOM elements representing dice and render them in dice container. - * Player's dice are shown in right pane. Other player's dice are shown in + * Player's dice are shown in right pane. Other player's dice are shown in * left pane. * @param {Dice} dice - Dice to render * @param {number} index - Index of dice value in array @@ -719,12 +742,12 @@ function SimpleBoardUI(client) { else { diceElem = $('#dice-left'); } - + for (var i = 0; i < dice.values.length; i++) { diceElem.append(''); this.updateDie(dice, i, type); } - + var self = this; $('.dice .die').unbind('click'); $('.dice .die').click(function (e) { @@ -753,12 +776,15 @@ function SimpleBoardUI(client) { else if (action.type === model.MoveActionType.BEAR) { this.playBearAction(action); } + else if (action.type === model.MoveActionType.UP) { + this.playUpAction(action); + } // TODO: Make sure actions are played back slow enough for player to see // all of them comfortly } }; - + this.playMoveAction = function (action) { if (!action.piece) { throw new Error('No piece!'); @@ -774,7 +800,7 @@ function SimpleBoardUI(client) { this.compactPosition(srcPointElem.data('position')); this.compactPosition(dstPointElem.data('position')); }; - + this.playRecoverAction = function (action) { if (!action.piece) { throw new Error('No piece!'); @@ -790,7 +816,7 @@ function SimpleBoardUI(client) { this.compactElement(srcPointElem, action.piece.type === this.client.player.currentPieceType ? 'top' : 'bottom'); this.compactPosition(dstPointElem.data('position')); }; - + this.playHitAction = function (action) { if (!action.piece) { throw new Error('No piece!'); @@ -806,7 +832,7 @@ function SimpleBoardUI(client) { this.compactPosition(srcPointElem.data('position')); this.compactElement(dstPointElem, action.piece.type === this.client.player.currentPieceType ? 'top' : 'bottom'); }; - + this.playBearAction = function (action) { if (!action.piece) { throw new Error('No piece!'); @@ -819,7 +845,19 @@ function SimpleBoardUI(client) { this.compactPosition(srcPointElem.data('position')); }; - + + this.playUpAction = function (action) { + if (!action.piece) { + throw new Error('No piece!'); + } + + var pieceElem = this.getPieceElem(action.piece); + var srcPointElem = pieceElem.parent(); + pieceElem.data('height', action.to); + + this.compactPosition(srcPointElem.data('position')); + }; + /** * Compact pieces after UI was resized */ diff --git a/app/browser/js/config.js b/app/browser/js/config.js index 5086903..44ed08a 100644 --- a/app/browser/js/config.js +++ b/app/browser/js/config.js @@ -3,9 +3,9 @@ var config = { 'boardUI': '../app/browser/js/SimpleBoardUI.js', 'defaultRule': 'RuleBgCasual', 'selectableRules': [ - 'RuleBgCasual', - 'RuleBgGulbara', - 'RuleBgTapa' + 'RuleUsUpgammon', + 'RuleBgCasual' + ] }; diff --git a/app/browser/js/main.js b/app/browser/js/main.js index 842ac6a..6d9f16e 100644 --- a/app/browser/js/main.js +++ b/app/browser/js/main.js @@ -18,6 +18,7 @@ require('../../../lib/rules/rule.js'); require('../../../lib/rules/RuleBgCasual.js'); require('../../../lib/rules/RuleBgGulbara.js'); require('../../../lib/rules/RuleBgTapa.js'); +require('../../../lib/rules/RuleUsUpgammon.js'); function App() { this._config = {}; diff --git a/app/browser/package.json b/app/browser/package.json index b96300a..c764b89 100644 --- a/app/browser/package.json +++ b/app/browser/package.json @@ -24,7 +24,7 @@ "build": "npm run build:js", "watch:js": "watchify ./js/main.js --require socket.io-client -o ./js/bundle.js -v", "watch": "npm run watch:js", - "postinstall": "HOME=$OPENSHIFT_REPO_DIR bower install || bower install" + "postinstall": "HOME=$OPENSHIFT_REPO_DIR bower install --allow-root || bower install --allow-root" }, "browser": { "jquery-fittext": "./bower_components/fittext/fittext.js" diff --git a/app/server/config.js b/app/server/config.js index 6ba1feb..1c8fcca 100644 --- a/app/server/config.js +++ b/app/server/config.js @@ -1,9 +1,8 @@ var config = { 'rulePath': '../../lib/rules/', 'enabledRules': [ - 'RuleBgCasual', - 'RuleBgGulbara', - 'RuleBgTapa' + 'RuleUsUpgammon', + 'RuleBgCasual' ] }; diff --git a/app/server/server.js b/app/server/server.js index c7ef000..ac173f2 100644 --- a/app/server/server.js +++ b/app/server/server.js @@ -30,7 +30,7 @@ function Server() { * @type {Player[]} */ this.players = []; - + /** * List of all matches * @type {Match[]} @@ -48,7 +48,7 @@ function Server() { * @type {{rulePath: string, enabledRules: string[]}} */ this.config = require('./config'); - + /** * Load enabled rules */ @@ -58,14 +58,14 @@ function Server() { require(this.config.rulePath + ruleName + '.js'); } }; - + /** * Save server state to database, in order to be able to resume active games later */ this.snapshotServer = function () { if (db) { console.log("Saving server state..."); - + var players = db.collection('players'); players.remove(); players.insert(this.players); @@ -77,7 +77,7 @@ function Server() { console.log("State saved."); } }; - + /** * Load saved server state from database */ @@ -130,16 +130,16 @@ function Server() { console.log("State restored."); } }; - + /** * Run server instance */ this.run = function () { /** Reference to server instance */ var self = this; - + this.loadRules(); - + this.restoreServer(); expressServer.use(express.static(path.join(__dirname, '../browser'))); @@ -157,7 +157,7 @@ function Server() { console.log(e); } }); - + // Subscribe for client requests: var m = comm.Message; var messages = [ @@ -168,6 +168,7 @@ function Server() { m.JOIN_MATCH, m.ROLL_DICE, m.MOVE_PIECE, + m.UP_PIECE, m.CONFIRM_MOVES, m.UNDO_MOVES, m.RESIGN_GAME, @@ -199,7 +200,7 @@ function Server() { console.log('listening on *:' + port); }); }; - + /** * Get match object associated with a socket * @param {Socket} socket - Client's socket @@ -235,7 +236,7 @@ function Server() { this.getSocketRule = function (socket) { return socket.rule; }; - + /** * Associate match object with socket * @param {Socket} socket - Client's socket @@ -279,7 +280,8 @@ function Server() { * @param {Object} params - Object map with message parameters */ this.sendMessage = function (socket, msg, params) { - console.log('Sending message ' + msg + ' to client ' + socket.id); + const timestamp = new Date().toTimeString(); + console.log(timestamp + ' - Sending message ' + msg + ' to client ' + socket.id); socket.emit(msg, params); }; @@ -330,13 +332,13 @@ function Server() { */ this.handleDisconnect = function (socket) { console.log('Client disconnected'); - + // DONE: remove this client from the waiting queue var player = this.getSocketPlayer(socket); if (!player) { return; } - + this.queueManager.removeFromAll(player); }; @@ -352,7 +354,7 @@ function Server() { var reply = { 'result': false }; - + // Return client's sequence number back. Client uses this number // to find the right callback that should be executed on message reply. if (params.clientMsgSeq) { @@ -380,6 +382,9 @@ function Server() { else if (msg === comm.Message.MOVE_PIECE) { reply.result = this.handleMovePiece(socket, params, reply); } + else if (msg === comm.Message.UP_PIECE) { + reply.result = this.handleUpPiece(socket, params, reply); + } else if (msg === comm.Message.CONFIRM_MOVES) { reply.result = this.handleConfirmMoves(socket, params, reply); } @@ -408,7 +413,7 @@ function Server() { // First send reply this.sendMessage(socket, msg, reply); - + // After that execute provided sendAfter callback. The callback // allows any additional events to be sent after the reply // has been sent. @@ -416,11 +421,11 @@ function Server() { { // Execute provided callback reply.sendAfter(); - + // Remove it from reply, it does not need to be sent to client delete reply.sendAfter; } - + this.snapshotServer(); }; @@ -434,7 +439,7 @@ function Server() { */ this.handleCreateGuest = function (socket, params, reply) { console.log('Creating guest player'); - + var player = null; if (!this.getSocketPlayer(socket) && params && params.playerID) { player = this.getPlayerByID(params.playerID); @@ -452,11 +457,11 @@ function Server() { console.log('Player ID found in cookie: ' + playerID); console.log(player); } - + if (player) { // Player already exists, but has been disconnected var match = this.getMatchByID(player.currentMatch); - + // If there is a pending match, use existing player, // else create a new player object if (match && !match.isOver) @@ -477,16 +482,16 @@ function Server() { } ); }; - + reply.player = player; reply.reconnected = true; - + console.log('Player: ', player); - return true; + return true; } } - + // New player will be created player = model.Player.createNew(); player.name = 'Player ' + player.id; @@ -524,7 +529,7 @@ function Server() { return true; }; - + /** * Handle client's request to play a random match. * If there is another player waiting in queue, start a match @@ -539,7 +544,7 @@ function Server() { */ this.handlePlayRandom = function (socket, params, reply) { console.log('Play random match'); - + var player = this.getSocketPlayer(socket); if (!player) { reply.errorMessage = 'Player not found!'; @@ -552,36 +557,36 @@ function Server() { } var popResult = this.queueManager.popFromRandom(params.ruleName); - + otherPlayer = popResult.player; // TODO: Make sure otherPlayer has not disconnected while waiting. // If that is the case, pop another player from the queue. - + if (otherPlayer) { if (params.ruleName === '.*') { params.ruleName = popResult.ruleName; } - + if (params.ruleName === '.*') { params.ruleName = model.Utils.getRandomElement(this.config.enabledRules); } - + // Start a new match with this other player var rule = model.Utils.loadRule(params.ruleName); var match = model.Match.createNew(rule); - + otherPlayer.currentMatch = match.id; otherPlayer.currentPieceType = model.PieceType.WHITE; model.Match.addHostPlayer(match, otherPlayer); - + player.currentMatch = match.id; player.currentPieceType = model.PieceType.BLACK; model.Match.addGuestPlayer(match, player); - + this.matches.push(match); - + var game = model.Match.createNewGame(match, rule); game.hasStarted = true; game.turnPlayer = otherPlayer; @@ -590,11 +595,11 @@ function Server() { // Assign match and rule objects to sockets of both players this.setSocketMatch(socket, match); this.setSocketRule(socket, rule); - + var otherSocket = this.clients[otherPlayer.socketID]; this.setSocketMatch(otherSocket, match); this.setSocketRule(otherSocket, rule); - + // Remove players from waiting queue this.queueManager.remove(player); this.queueManager.remove(otherPlayer); @@ -603,7 +608,7 @@ function Server() { reply.host = otherPlayer; reply.guest = player; reply.ruleName = params.ruleName; - + var self = this; reply.sendAfter = function () { self.sendMatchMessage( @@ -614,13 +619,13 @@ function Server() { } ); }; - + return true; } else { // Put player in queue, and wait for another player this.queueManager.addToRandom(player, params.ruleName); - + reply.isWaiting = true; return true; } @@ -657,7 +662,7 @@ function Server() { player.currentMatch = match.id; player.currentPieceType = model.PieceType.WHITE; this.matches.push(match); - + var game = model.Match.createNewGame(match, rule); this.setSocketMatch(socket, match); @@ -686,7 +691,7 @@ function Server() { reply.errorMessage = 'Match with ID ' + params.matchID + ' not found!'; return false; } - + var match = this.getMatchByID(params.matchID); if (!match) { reply.errorMessage = 'Match with ID ' + params.matchID + ' not found!'; @@ -751,9 +756,9 @@ function Server() { var match = this.getSocketMatch(socket); var player = this.getSocketPlayer(socket); var rule = this.getSocketRule(socket); - + var game = match.currentGame; - + if (!game) { reply.errorMessage = 'Match with ID ' + match.id + ' has no current game!'; return false; @@ -811,14 +816,14 @@ function Server() { var match = this.getSocketMatch(socket); var player = this.getSocketPlayer(socket); var rule = this.getSocketRule(socket); - + console.log('Piece:', params.piece); - + if (!params.piece) { reply.errorMessage = 'No piece selected!'; return false; } - + if (!match.currentGame) { reply.errorMessage = 'Match created, but current game is null!'; return false; @@ -830,7 +835,7 @@ function Server() { } // First, check status of the game: if game was started, if it is player's turn, etc. - if (!rule.validateMove(match.currentGame, player, params.piece, params.steps)) { + if (!rule.validateMove(match.currentGame, player, params.piece, params.steps, model.MoveActionType.MOVE)) { reply.errorMessage = 'Requested move is not valid!'; return false; } @@ -844,9 +849,9 @@ function Server() { try { rule.applyMoveActions(match.currentGame.state, actionList); rule.markAsPlayed(match.currentGame, params.steps); - + match.currentGame.moveSequence++; - + reply.piece = params.piece; reply.type = params.type; reply.steps = params.steps; @@ -864,7 +869,7 @@ function Server() { } ); - return true; + return true; } catch (e) { reply.piece = params.piece; @@ -875,11 +880,99 @@ function Server() { if (process.env.DEBUG) { throw e; } - + return false; } }; +/** + * Handle client's request to UP a piece. + * @param {Socket} socket - Client socket + * @param {Object} params - Request parameters + * @param {number} params.piece - Piece to move + * @param {number} params.height - Number of steps to move in height + * @param {PieceType} params.type - Type of piece + * @param {Object} reply - Object to be send as reply + * @returns {boolean} - Returns true if message have been processed + * successfully and a reply should be sent. + */ +this.handleUpPiece = function (socket, params, reply) { + console.log('Moving UP a piece', params); + + var match = this.getSocketMatch(socket); + var player = this.getSocketPlayer(socket); + var rule = this.getSocketRule(socket); + + console.log('Piece:', params.piece); + + if (!params.piece) { + reply.errorMessage = 'No piece selected!'; + return false; + } + + if (!match.currentGame) { + reply.errorMessage = 'Match created, but current game is null!'; + return false; + } + + if (params.moveSequence < match.currentGame.moveSequence) { + reply.errorMessage = 'This move has already been played!'; + return false; + } + + // First, check status of the game: if game was started, if it is player's turn, etc. + if (!rule.validateUpMove(match.currentGame, player, params.piece, params.height)) { + reply.errorMessage = 'Requested UP move is not valid!'; + return false; + } + + var actionList = rule.getMoveActions(match.currentGame.state, params.piece, params.height, model.MoveActionType.UP); + if (actionList.length === 0) { + reply.errorMessage = 'Requested move is not allowed!'; + return false; + } + + try { + rule.applyMoveActions(match.currentGame.state, actionList); + rule.markAsPlayed(match.currentGame, params.height); + + match.currentGame.moveSequence++; + + reply.piece = params.piece; + reply.type = params.type; + reply.steps = params.steps; + reply.height = params.height; + reply.moveActionList = actionList; + + this.sendMatchMessage( + match, + comm.Message.EVENT_PIECE_UP, + { + 'match': match, + 'piece': params.piece, + 'type': params.type, + 'steps': params.steps, + 'height': params.steps, + 'moveActionList': actionList + } + ); + + return true; + } + catch (e) { + reply.piece = params.piece; + reply.type = params.type; + reply.steps = params.steps; + reply.moveActionList = []; + + if (process.env.DEBUG) { + throw e; + } + + return false; + } +}; + /** * Handle client's request to confirm moves made in current turn * @param {Socket} socket - Client socket @@ -890,7 +983,7 @@ function Server() { */ this.handleConfirmMoves = function (socket, params, reply) { console.log('Confirming piece movement', params); - + var self = this; var match = this.getSocketMatch(socket); @@ -901,9 +994,9 @@ function Server() { reply.errorMessage = 'Confirming moves is not allowed!'; return false; } - + var otherPlayer = (model.Match.isHost(match, player)) ? match.guest : match.host; - + console.log('CONFIRM MOVES'); // Check if player has won if (rule.hasWon(match.currentGame.state, player)) { @@ -961,7 +1054,7 @@ function Server() { return true; }; - + /** * Handle client's request to resign from current game (game only, not whole match) * @param {Socket} socket - Client socket @@ -977,12 +1070,12 @@ function Server() { var player = this.getSocketPlayer(socket); var rule = this.getSocketRule(socket); var otherPlayer = (model.Match.isHost(match, player)) ? match.guest : match.host; - + this.endGame(socket, otherPlayer, true, reply); - + return true; }; - + /** * Handle client's request to resign from whole match * @param {Socket} socket - Client socket @@ -998,9 +1091,9 @@ function Server() { var player = this.getSocketPlayer(socket); var rule = this.getSocketRule(socket); var otherPlayer = (model.Match.isHost(match, player)) ? match.guest : match.host; - + var self = this; - + reply.sendAfter = function () { self.sendMatchMessage( match, @@ -1012,10 +1105,10 @@ function Server() { } ); }; - + return true; }; - + /** * End game * @param {Socket} socket - Client socket @@ -1031,7 +1124,7 @@ function Server() { var player = this.getSocketPlayer(socket); var rule = this.getSocketRule(socket); var otherPlayer = (model.Match.isHost(match, player)) ? match.guest : match.host; - + // 1. Update score var score = rule.getGameScore(match.currentGame.state, winner); match.score[winner.currentPieceType] += score; diff --git a/docs/rules.md b/docs/rules.md index 5e55d91..bfd9c05 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -58,7 +58,7 @@ Want a rule where every second dice is 6:6, well, nothing prevents you from crea `require('../../../lib/rules/RuleCCYourRule.js');` - - Open file and add a new require after others: + - Open file `lib/client.js` and add a new require after others: `require('./rules/RuleBgCasual.js');` diff --git a/lib/client.js b/lib/client.js index 51209a5..16340be 100644 --- a/lib/client.js +++ b/lib/client.js @@ -9,6 +9,7 @@ require('./rules/rule.js'); require('./rules/RuleBgCasual.js'); require('./rules/RuleBgGulbara.js'); require('./rules/RuleBgTapa.js'); +require('./rules/RuleUsUpgammon.js'); /** * Backgammon client @@ -26,26 +27,26 @@ function Client(config) { * @type {Socket} */ this._socket = null; - + /** * Counter used to generate unique sequence number for messages in client's session * @type {number} */ this._clientMsgSeq = 0; - + /** * Map of callback functions to be executed after server replies to a message * @type {Object} */ this._callbackList = {}; - + /** * Dictionary of arrays, containing subscriptions for reception of messages by id/type. * The key of the dictionary is the message ID. * The value of the dictionary is an array with callback functions to execute when message is received. * @type {{Array}} */ - this._msgSubscriptions = {}; + this._msgSubscriptions = {}; /** * Client's player object @@ -58,7 +59,7 @@ function Client(config) { * @type {Player} */ this.otherPlayer = null; - + /** * Current match * @type {Match} @@ -117,7 +118,7 @@ function Client(config) { self.handleConnect(); self.updateUI(); }); - + // Subscribe for other messages: var m = comm.Message; var messages = [ @@ -128,17 +129,19 @@ function Client(config) { m.JOIN_MATCH, m.ROLL_DICE, m.MOVE_PIECE, + m.UP_PIECE, m.EVENT_PLAYER_JOINED, m.EVENT_TURN_START, m.EVENT_DICE_ROLL, m.EVENT_PIECE_MOVE, + m.EVENT_PIECE_UP, m.EVENT_MATCH_START, m.EVENT_GAME_OVER, m.EVENT_MATCH_OVER, m.EVENT_GAME_RESTART, m.EVENT_UNDO_MOVES ]; - + var createHandler = function(msg){ return function(params) { self.handleMessage(msg, params); @@ -152,7 +155,7 @@ function Client(config) { } }; - + /** * Message callback * @@ -161,7 +164,7 @@ function Client(config) { * @param {number} clientMsgSeq - An integer. * @param {Object} reply - Object containing reply data. * @param {boolean} reply.result - Result of command execution - */ + */ /** * Send message to server. @@ -172,12 +175,12 @@ function Client(config) { this.sendMessage = function (msg, params, callback) { params = params || {}; params.clientMsgSeq = ++this._clientMsgSeq; - + // Store reference to callback. It will be executed when server replies to this message this._callbackList[params.clientMsgSeq] = callback; - + console.log('Sending message ' + msg + ' with ID ' + params.clientMsgSeq); - + this._socket.emit(msg, params); }; @@ -237,12 +240,18 @@ function Client(config) { else if (msg == comm.Message.MOVE_PIECE) { this.handleMovePiece(params); } + else if (msg == comm.Message.UP_PIECE) { + this.handleUpPiece(params); + } else if (msg == comm.Message.EVENT_PLAYER_JOINED) { this.handleEventPlayerJoined(params); } else if (msg == comm.Message.EVENT_PIECE_MOVE) { this.handleEventPieceMove(params); } + else if (msg == comm.Message.EVENT_PIECE_UP) { + this.handleEventPieceUp(params); + } else if (msg == comm.Message.EVENT_TURN_START) { this.handleEventTurnStart(params); } @@ -268,16 +277,16 @@ function Client(config) { console.log('Unknown message!'); return; } - + if (params.clientMsgSeq) { var callback = this._callbackList[params.clientMsgSeq]; if (callback) { callback(msg, params.clientMsgSeq, params); - + delete this._callbackList[params.clientMsgSeq]; } } - + this._notify(msg, params); this.updateUI(); @@ -293,7 +302,7 @@ function Client(config) { // TODO: update UI console.log('Created guest player (ID): ' + this.player.id); - + // Store player ID as cookie. It will be used to retrieve the player // object later, if page is reloaded. document.cookie = 'player_id=; expires=Thu, 01 Jan 1970 00:00:00 GMT'; @@ -308,7 +317,7 @@ function Client(config) { // TODO: update UI console.log('List of matches (IDs): ' + params.list.length); }; - + /** * Handle reply - Start random match * @param {Object} params - Message parameters @@ -361,7 +370,7 @@ function Client(config) { }; /** - * Handle reply - Piece moved + * Handle reply - Piece move (user-initiated) * @param {Object} params - Message parameters */ this.handleMovePiece = function (params) { @@ -371,6 +380,17 @@ function Client(config) { } }; + /** + * Handle reply - Piece UP (user-initiated) + * @param {Object} params - Message parameters + */ + this.handleUpPiece = function (params) { + console.log('Piece UP move'); + if (!params.result) { + this.boardUI.notifyError(params.errorMessage); + } + }; + /** * Handle event - Another player joined match * @param {Object} params - Message parameters @@ -387,7 +407,7 @@ function Client(config) { */ this.handleEventTurnStart = function (params) { console.log('Turn start'); - + this.boardUI.handleTurnStart(); }; @@ -411,15 +431,28 @@ function Client(config) { console.log('Piece moved'); this.boardUI.playActions(params.moveActionList); }; - + /** - * Handle event - Piece moved + * Handle event - Piece UP moved + * @param {Object} params - Message parameters + * @param {number} params.position - Position of piece being moved + * @param {PieceType} params.type - Type of piece being moved + * @param {number} params.steps - Number steps the piece is moved with (height) + * @param {MoveAction[]} params.moveActionList - List of actions that have to be played in UI + */ + this.handleEventPieceUp = function (params) { + console.log('Piece UP moved'); + this.boardUI.playActions(params.moveActionList); + }; + + /** + * Handle event - Start of a match * @param {Object} params - Message parameters * @param {number} params.match - Match that has been started */ this.handleEventMatchStart = function (params) { console.log('Match started'); - + if (model.Match.isHost(params.match, this.player)) { this.updatePlayer(params.match.host); this.updateOtherPlayer(params.match.guest); @@ -428,12 +461,12 @@ function Client(config) { this.updatePlayer(params.match.guest); this.updateOtherPlayer(params.match.host); } - + this.updateMatch(params.match); this.updateRule(this.loadRule(params.match.ruleName)); this.resetBoard(this.match, this.rule); }; - + /** * Handle event - Game over. Current game is over. Prepare for next game of match, if any. * @param {Object} params - Message parameters @@ -443,7 +476,7 @@ function Client(config) { console.log('Game is over. Winner:', params.winner); this.boardUI.showGameEndMessage(params.winner, params.resigned); }; - + /** * Handle event - Match is over. Offer rematch or starting a new game. * @param {Object} params - Message parameters @@ -453,7 +486,7 @@ function Client(config) { console.log('Match is over. Winner:', params.winner); this.boardUI.showGameEndMessage(params.winner, params.resigned); }; - + /** * Handle event - Game restart. Current game in match is over. Match is not finished, so start next game. * @param {Object} params - Message parameters @@ -549,7 +582,7 @@ function Client(config) { this.boardUI.updateControls(); this.boardUI.updateScoreboard(); }; - + /** * Subscribe for notification on message reception * @param {number} msgID - The type of message to subscribe for @@ -560,7 +593,7 @@ function Client(config) { this._msgSubscriptions[msgID].push(callback); console.log(this._msgSubscriptions); }; - + /** * Subscribe for notification on message reception * @param {number} msg - The ID of the message received @@ -575,7 +608,7 @@ function Client(config) { } } }; - + /** * Request playing a match with random player - from waiting queue. * @param {string} ruleName - Name of rule to use (eg. RuleBgCasual) @@ -648,7 +681,7 @@ function Client(config) { this.reqUndoMoves = function (callback) { this.sendMessage(comm.Message.UNDO_MOVES, undefined, callback); }; - + /** * Resign from current game only * @param {messageCallback} [callback] - Callback function to be called when server sends a reply @@ -656,7 +689,7 @@ function Client(config) { this.reqResignGame = function (callback) { this.sendMessage(comm.Message.RESIGN_GAME, undefined, callback); }; - + /** * Resign from whole match * @param {messageCallback} [callback] - Callback function to be called when server sends a reply @@ -683,7 +716,26 @@ function Client(config) { callback ); }; - + + /** + * Request elevating a piece up. + * @param {Piece} piece - Denormalized position from which a piece has to be moved + * @param {number} height - Number of steps to move up + * @param {messageCallback} [callback] - Callback function to be called when server sends a reply + */ + this.reqUp = function (piece, height, callback) { + console.log('Move (up) sequence: ', this.match.currentGame.moveSequence); + this.sendMessage( + comm.Message.UP_PIECE, + { + 'piece': piece, + 'height': height, + 'moveSequence': this.match.currentGame.moveSequence + }, + callback + ); + }; + /** * Notify UI that DOM was rezised and UI may have to be updated */ diff --git a/lib/comm.js b/lib/comm.js index c8db42f..b95a52f 100644 --- a/lib/comm.js +++ b/lib/comm.js @@ -24,6 +24,7 @@ var Message = { JOIN_MATCH: 'joinMatch', ROLL_DICE: 'rollDice', MOVE_PIECE: 'movePiece', + UP_PIECE: 'upPiece', CONFIRM_MOVES: 'confirmMoves', UNDO_MOVES: 'undoMoves', RESIGN_GAME: 'resignGame', @@ -32,6 +33,7 @@ var Message = { EVENT_TURN_START: 'eventTurnStart', EVENT_DICE_ROLL: 'eventDiceRoll', EVENT_PIECE_MOVE: 'eventPieceMove', + EVENT_PIECE_UP: 'eventPieceUp', EVENT_MATCH_START: 'eventMatchStart', EVENT_MATCH_OVER: 'eventMatchOver', EVENT_GAME_OVER: 'eventGameOver', diff --git a/lib/model.js b/lib/model.js index 11fd59d..5e11a97 100644 --- a/lib/model.js +++ b/lib/model.js @@ -86,7 +86,7 @@ Utils.removeItem = function (array, item) { * * Example: * [6, 4, 1] becomes [4, 1, 6] - * + * * @param {Array} array - Array of elements */ Utils.rotateLeft = function (array) { @@ -95,7 +95,7 @@ Utils.rotateLeft = function (array) { /** * Create shallow copy of object. - * + * * @param {Object} oldObj - Object to copy * @returns {Object} - Shallow copy of object */ @@ -112,7 +112,7 @@ Utils.shallowCopy = function (oldObj) { /** * Create deep copy of a value object. * The object should have no functions/methods. - * + * * @param {Object} oldObj - Object to copy * @returns {Object} - Deep copy of object */ @@ -177,7 +177,7 @@ function Random() { */ Random.get = function() { // TODO: replace with quality random generator - + // Combine Math random generator with crypto one var buffer = crypto.randomBytes(1); var value = buffer.readUInt8(0); @@ -186,7 +186,7 @@ Random.get = function() { value = 255; } var k = value / 256; - + return (Math.floor(k * 6) + 1); }; @@ -216,16 +216,16 @@ function Piece(type, id) { */ function Dice() { /** - * Values of the two dice + * Values of the three dice * @type {Array} */ - this.values = [0, 0]; + this.values = [0, 0, 0]; /** * List of moves the player can make. Usually moves are equal to values, * but in most rules doubles (eg. 6:6) are played four times, instead of * two, in which case moves array will contain four values in stead of - * only two (eg. [6, 6, 6, 6]). + * only two (eg. [6, 6, 6, 6]). With three dice, if all three match then 8 * @type {Array} */ this.moves = []; @@ -238,7 +238,7 @@ function Dice() { * @type {Array} */ this.movesLeft = []; - + /** * After a piece is moved, the value of the die used is added to movesPlayed array. * @type {Array} @@ -252,9 +252,10 @@ function Dice() { */ Dice.roll = function() { var dice = new Dice(); - + dice.values[0] = Random.get(); dice.values[1] = Random.get(); + dice.values[2] = Random.get(); dice.values.sort(function (a, b) { return b - a; }); return dice; }; @@ -275,14 +276,14 @@ Dice.markAsPlayed = function (dice, move) { throw new Error("No such move!"); }; -/** - * Check if the dice object has double (equal) values. - * @param {Dice} dice - New dice with random values - * @returns {boolean} - True if dice object has dobule values, false otherwise - */ -Dice.isDouble = function (dice) { - return dice.values[0] === dice.values[1]; -}; +// /** +// * Check if the dice object has double (equal) values. +// * @param {Dice} dice - New dice with random values +// * @returns {boolean} - True if dice object has dobule values, false otherwise +// */ +// Dice.isDouble = function (dice) { +// return dice.values[0] === dice.values[1] || dice.values[1] === dice.values[2] || dice.values[0] === dice.values[2]; +// }; /** * Get remaining moves from dice object - moves that have not been played. @@ -303,7 +304,7 @@ Dice.getRemainingMoves = function (dice) { remaining.push(dice.moves[i]); } } - + return remaining; }; @@ -328,6 +329,12 @@ function State() { */ this.points = []; + /** + * Height: if a piece(s) has moved vertically, store its new height here; index is + * piece number + */ + this.heightOverrides = []; + /** * Players have separate bar places and so separate list. * First element of array is for white pieces and second one for black. @@ -370,6 +377,7 @@ State.clear = function(state) { for (var i = 0; i < state.points.length; i++) { state.points[i].length = 0; } + state.heightOverrides = []; state.whiteBar.length = 0; state.blackBar.length = 0; state.whiteOutside.length = 0; @@ -611,7 +619,7 @@ function Player() { * @type {string} */ this.name = ''; - + /** * Reference to current match * @type {Match} @@ -683,7 +691,7 @@ function Game() { * @type {boolean} */ this.isOver = false; - + /** * Number (index) of turn * @type {number} @@ -854,25 +862,25 @@ function Match() { * @type {string} */ this.ruleName = ''; - + /** * Match length - the score needed to win the match. * @type {number} */ this.length = 5; - + /** * Score of players for current match * @type {Array} */ this.score = []; - + /** * Current game * @type {Game} */ this.currentGame = null; - + /** * Is match over * @type {boolean} @@ -973,7 +981,9 @@ var MoveActionType = { /** HIT: Hit opponent's piece and sent it to bar */ HIT: 'hit', /** BEAR: Bear piece - move it outside the board */ - BEAR: 'bear' + BEAR: 'bear', + /** UP: Move vertically to pass a tower */ + UP: 'up', }; /** diff --git a/lib/rules/RuleBgCasual.js b/lib/rules/RuleBgCasual.js index 772623e..fa237ef 100644 --- a/lib/rules/RuleBgCasual.js +++ b/lib/rules/RuleBgCasual.js @@ -19,7 +19,7 @@ function RuleBgCasual() { * Short title describing rule specifics * @type {string} */ - this.title = 'General'; + this.title = 'Traditional'; /** * Full description of rule diff --git a/lib/rules/RuleUsUpgammon.js b/lib/rules/RuleUsUpgammon.js new file mode 100644 index 0000000..f67124e --- /dev/null +++ b/lib/rules/RuleUsUpgammon.js @@ -0,0 +1,454 @@ +var model = require('../model.js'); +var Rule = require('./rule.js'); + +/** + * Most popular variant played in Bulgaria called casual (обикновена). + * @constructor + * @extends Rule + */ +function RuleUsUpgammon() { + Rule.call(this); + + /** + * Rule name, matching the class name (eg. 'RuleBgCasual') + * @type {string} + */ + this.name = 'RuleUsUpgammon'; + + /** + * Short title describing rule specifics + * @type {string} + */ + this.title = 'Upgammon'; + + /** + * Full description of rule + * @type {string} + */ + this.description = 'Backgammon in two dimensions'; + + /** + * Full name of country where this rule (variant) is played. + * To list multiple countries use a pipe ('|') character as separator. + * @type {string} + */ + this.country = 'USA'; + + /** + * Two character ISO code of country where this rule (variant) is played. + * To list multiple codes use a pipe ('|') character as separator. + * List codes in same order as countries in the field above. + * @type {string} + */ + this.countryCode = 'us'; + + /** + * Descendents should list all action types that are allowed in this rule. + * @type {MoveActionType[]} + */ + this.allowedActions = [ + model.MoveActionType.MOVE, + model.MoveActionType.BEAR, + model.MoveActionType.HIT, + model.MoveActionType.RECOVER, + model.MoveActionType.UP, + ]; +} + +RuleUsUpgammon.prototype = Object.create(Rule.prototype); +RuleUsUpgammon.prototype.constructor = RuleUsUpgammon; + +/** + * Reset state to initial position of pieces according to current rule. + * @memberOf RuleUsUpgammon + * @param {State} state - Board state + */ +RuleUsUpgammon.prototype.resetState = function(state) { + /** + * Move pieces to correct initial positions for both players. + * Values in state.points are zero based and denote the . + * the number of pieces on each position. + * Index 0 of array is position 1 and increments to the number of maximum + * points. + * + * Position: |12 13 14 15 16 17| |18 19 20 21 22 23| White + * |5w 3b | |5b 2w| <- + * | | | | + * | | | | + * | | | | + * |5b 3w | |5w 2b| <- + * Position: |11 10 09 08 07 06| |05 04 03 02 01 00| Black + * + */ + + + model.State.clear(state); + + this.place(state, 5, model.PieceType.WHITE, 5); + this.place(state, 3, model.PieceType.WHITE, 7); + this.place(state, 5, model.PieceType.WHITE, 12); + this.place(state, 2, model.PieceType.WHITE, 23); + + this.place(state, 5, model.PieceType.BLACK, 18); + this.place(state, 3, model.PieceType.BLACK, 16); + this.place(state, 5, model.PieceType.BLACK, 11); + this.place(state, 2, model.PieceType.BLACK, 0); +}; + +/** + * Increment position by specified number of steps and return an incremented position + * @memberOf RuleUsUpgammon + * @param {number} position - Denormalized position + * @param {PieceType} type - Type of piece + * @param {number} steps - Number of steps to increment towards first home position + * @returns {number} - Incremented position (denormalized) + */ +RuleUsUpgammon.prototype.incPos = function(position, type, steps) { + var newPosition; + if (type === model.PieceType.WHITE) { + newPosition = position - steps; + } + else { + newPosition = position + steps; + } + + return newPosition; +}; + +/** + * Normalize position - Normalized positions start from 0 to 23 for both players, + * where 0 is the first position in the home part of the board, 6 is the last + * position in the home part and 23 is the furthest position - in the opponent's + * home. + * @memberOf RuleUsUpgammon + * @param {number} position - Denormalized position (0 to 23 for white and 23 to 0 for black) + * @param {PieceType} type - Type of piece (white/black) + * @returns {number} - Normalized position (0 to 23 for both players) + */ +RuleUsUpgammon.prototype.normPos = function(position, type) { + var normPosition = position; + + if (type === model.PieceType.BLACK) { + normPosition = 23 - position; + } + return normPosition; +}; + +/** + * Get denormalized position - start from 0 to 23 for white player and from + * 23 to 0 for black player. + * @memberOf RuleUsUpgammon + * @param {number} position - Normalized position (0 to 23 for both players) + * @param {PieceType} type - Type of piece (white/black) + * @return {number} - Denormalized position (0 to 23 for white and 23 to 0 for black) + */ +RuleUsUpgammon.prototype.denormPos = function(position, type) { + var denormPosition = position; + + if (type === model.PieceType.BLACK) { + denormPosition = 23 - position; + } + return denormPosition; +}; + +/** + * Validate piece UP move. + * + * This is the base method for validation of moves that make a few general + * checks like: + * - Is the game started and is finished? + * - Is it player's turn? + * - Was dice rolled? + * - Are moves with values equal to the steps left? + * + * Descendant rules must extend this method and add additional validation checks + * according to the rule specifics. + * + * @memberOf Rule + * @param {Game} game - Game + * @param {Player} player - Player requesting move + * @param {Piece} piece - Piece to move + * @param {number} height - Number of steps to make forward to the first home position + * @returns {boolean} True if move is valid and should be allowed. + */ +Rule.prototype.validateUpMove = function(game, player, piece, steps) { + if (!this.validateTurn(game, player)) { + return false; + } + + if (!model.Game.diceWasRolled(game)) { + console.log('Dice was not rolled!'); + return false; + } + + if (!model.Game.hasMove(game, steps)) { + console.log('No such move left!'); + return false; + } + + if (piece.type !== player.currentPieceType) { + console.log('Piece is of wrong type!'); + return false; + } + + if (this.isMoveActionRestricted(game.state, game.turnDice.movesLeft, piece, steps, model.MoveActionType.UP)) { + console.log('Move is restricted -- this is using old (non-UP) logic!'); + return false; + } + + return true; +}; + +/** + * Call this method after a request for moving a piece has been made. + * Determines if the move is allowed and what actions will have to be made as + * a result. Actions can be `move`, `place`, `hit` or `bear`. + * + * If move is allowed or not depends on the current state of the game. For example, + * if the player has pieces on the bar, they will only be allowed to place pieces. + * + * Multiple actions can be returned, if required. Placing (or moving) a piece over + * an opponent's blot will result in two actions: `hit` first, then `place` (or `move`). + * + * The list of actions returned would usually be appllied to game state and then + * sent to client. The client's UI would play the actions (eg. with movement animation) + * in the same order. + * + * @memberOf RuleUsUpgammon + * @param {State} state - State + * @param {Piece} piece - Piece to move + * @param {number} steps - Number of steps to increment towards first home position + * @param {type} moveType - Type of move (MOVE or UP) + * @returns {MoveAction[]} - List of actions if move is allowed, empty list otherwise. + */ +RuleUsUpgammon.prototype.getMoveActions = function(state, piece, steps, moveType = model.MoveActionType.MOVE) { + var actionList = []; + + // Next, check conditions specific to this game rule and build the list of + // actions that has to be made. + + /** + * Create a new move action and add it to actionList. Used internally. + * + * @alias RuleUsUpgammon.getMoveActions.addAction + * @memberof RuleUsUpgammon.getMoveActions + * @method RuleUsUpgammon.getMoveActions.addAction + * @param {MoveActionType} moveActionType - Type of move action (eg. move, hit, bear) + * @param {Piece} piece - Piece to move + * @param {number} from - Denormalized source position. If action uses only one position parameter, this one is used. + * @param {number} to - Denormalized destination position. + * @returns {MoveAction} + * @see {@link getMoveActions} for more information on purpose of move actions. + */ + function addAction(moveActionType, piece, from, to) { + var action = new model.MoveAction(); + action.type = moveActionType; + action.piece = piece; + action.position = from; + action.from = from; + if (typeof to != "undefined") { + action.to = to; + } + actionList.push(action); + return action; + } + + // TODO: Catch exceptions due to disallowed move requests and pass them as error message to the client. + try { + var position = model.State.getPiecePos(state, piece); + + // TODO: Consider using state machine? Is it worth, can it be useful in other methods too? + if (this.havePiecesOnBar(state, piece.type)) { + /* + If there are pieces on the bar, the player can only place pieces on. + Input data: steps=3 + Cases: + - Opponent has no pieces there --> place the checker at position 21 + - Opponent has exactly one piece --> hit oponent piece and place at position 21 + - Opponent has two or more pieces --> point is blocked, cannot place piece there + ! + +12-13-14-15-16-17------18-19-20-21-22-23-+ + | O | @ | X | + | O | | X | + | | | X | + | | | | + | | | | + | | | | + | | | | + | | | | + | | | | + | | | O | + | | | O O O | + +11-10--9--8--7--6-------5--4--3--2--1--0-+ -1 + + */ + + if (model.State.isPieceOnBar(state, piece)) { + // Make sure that the piece that the player wants to move + // is on the bar + + var destination = (piece.type === model.PieceType.WHITE) ? (24 - steps) : (steps - 1); + var destTopPiece = model.State.getTopPiece(state, destination); + var destTopPieceType = (destTopPiece) ? destTopPiece.type : null; + + if ((destTopPieceType === null) || (destTopPieceType === piece.type)) { + // There are no pieces at this point or the top piece is owned by player, + // so directly place piece from bar to opponent's home field + + addAction( + model.MoveActionType.RECOVER, piece, destination + ); + } + else if (model.State.countAtPos(state, destination, destTopPieceType) === 1) { + // The top piece is opponent's and is only one (i.e. the point is not blocked), + // so hit opponent's piece from destination and place ours at this position + + addAction( + model.MoveActionType.HIT, destTopPiece, destination + ); + + addAction( + model.MoveActionType.RECOVER, piece, destination + ); + } + } + } + else if (this.allPiecesAreHome(state, piece.type)) { + /* + If all pieces are in home field, the player can bear pieces + Cases: + - Normalized position >= 0 --> Just move the piece + - Normalized position === -1 --> Bear piece + - Normalized position < -1 --> Bear piece, only if there are no player pieces at higher positions + + +12-13-14-15-16-17------18-19-20-21-22-23-+ + | | | | + | | | | + | | | | + | | | | + | | | | + | | | | + | | | | + | | | | + | | | | + | | | O O | + | | | O O O | + +11-10--9--8--7--6-------5--4--3--2--1--0-+ -1 + + */ + var destination = this.incPos(position, piece.type, steps); + var normDestination = this.normPos(destination, piece.type); + + // Move the piece, unless point is blocked by opponent + if (normDestination >= 0) { + + var destTopPiece = model.State.getTopPiece(state, destination); + var destTopPieceType = (destTopPiece) ? destTopPiece.type : null; + + // There are no pieces at this point or the top piece is owned by player, + // so just move piece to that position + if ((destTopPieceType === null) || (destTopPieceType === piece.type)) { + addAction( + model.MoveActionType.MOVE, piece, position, destination + ); + } + // The top piece is opponent's and is only one (i.e. the point is not blocked), + // so hit opponent's piece from destination and move ours at this position + else if (model.State.countAtPos(state, destination, destTopPieceType) === 1) { + addAction( + model.MoveActionType.HIT, destTopPiece, destination + ); + + addAction( + model.MoveActionType.MOVE, piece, position, destination + ); + } + } + // If steps are just enought to reach position -1, bear piece + else if (normDestination === -1) { + addAction( + model.MoveActionType.BEAR, piece, position + ); + } + // If steps move the piece beyond -1 position, the player can bear the piece, + // only if there are no other pieces at higher positions + else { + var normSource = this.normPos(position, piece.type); + if (this.countAtHigherPos(state, normSource + 1, piece.type) <= 0) { + addAction( + model.MoveActionType.BEAR, piece, position + ); + } + } + } + else { + if (moveType === model.MoveActionType.UP) { + addAction( + model.MoveActionType.UP, piece, position, steps + ) + } else { + /* + If there are no pieces at bar, and at least one piece outside home, + just move the piece. + Input data: position=13, steps=3 + Cases: + - Opponent has no pieces there --> place the checker at position 10 + - Opponent has exactly one piece --> hit oponent piece and place at position 10 + - Opponent has two or more pieces --> point is blocked, cannot place piece there + ! + +12-13-14-15-16-17------18-19-20-21-22-23-+ + | O | | X | + | O | | X | + | | | X | + | | | | + | | | | + | | | | + | | | | + | | | | + | | | | + | | | O | + | | | O O O | + +11-10--9--8--7--6-------5--4--3--2--1--0-+ -1 + ! + */ + + var destination = this.incPos(position, piece.type, steps); + + // Make sure that destination is within board + if ((destination >= 0) && (destination <= 23)) { + var normDest = this.normPos(destination, piece.type); + // TODO: Make sure position is not outside board + + var destTopPiece = model.State.getTopPiece(state, destination); + var destTopPieceType = (destTopPiece) ? destTopPiece.type : null; + + // There are no pieces at this point or the top piece is owned by player + if ((destTopPieceType === null) || (destTopPieceType === piece.type)) { + addAction( + model.MoveActionType.MOVE, piece, position, destination + ); + } + // The top piece is opponent's and is a blot (i.e. the point is not blocked) + else if (model.State.countAtPos(state, destination, destTopPieceType) === 1) { + addAction( + model.MoveActionType.HIT, destTopPiece, destination + ); + + addAction( + model.MoveActionType.MOVE, piece, position, destination + ); + } + } + } + } + } + catch (e) { + console.log('Error in RuleUsUpgammon.getMoveActions: ' + e); + actionList = []; + return actionList; + } + + return actionList; +}; + +module.exports = new RuleUsUpgammon(); diff --git a/lib/rules/rule.js b/lib/rules/rule.js index 6be2cb4..7d99447 100644 --- a/lib/rules/rule.js +++ b/lib/rules/rule.js @@ -109,33 +109,43 @@ Rule.prototype.initialize = function(state) { Rule.prototype.rollDice = function(game, values) { // Create dice object with 2 random values var dice = model.Dice.roll(); - + if (typeof values !== "undefined") { dice.values[0] = values[0]; dice.values[1] = values[1]; + dice.values[2] = values[2]; } // Add those values to moves list - the individual moves the player has to make dice.moves = dice.moves.concat(dice.values); - // Dices with equal values are played four times, so add two more moves - if (dice.moves[0] == dice.moves[1]) { - dice.moves = dice.moves.concat(dice.values); + // Doubles & Triples? + if (dice.values[0] == dice.values[1]) { + dice.moves.push(dice.values[0]); + dice.moves.push(dice.values[1]); + } + if (dice.values[1] == dice.values[2]) { + dice.moves.push(dice.values[1]); + dice.moves.push(dice.values[2]); + } + if (dice.values[0] == dice.values[2]) { + dice.moves.push(dice.values[0]); + dice.moves.push(dice.values[2]); } // Sort moves in descending order for convenience later in enforcing // move rules dice.moves.sort(function (a, b) { return b - a; }); - + // TODO: Put in movesLeft only moves that are playable. var weight = this.calculateMoveWeights(game.state, dice.moves, game.turnPlayer.currentPieceType, null, true); dice.moves = weight.playableMoves; - + // Copy move values to movesLeft array. Moves will be removed from movesLeft // after being played by player, whereas values in moves array will remain // in case the player wants to undo his actions. dice.movesLeft = dice.movesLeft.concat(dice.moves); - + console.log('Playable moves:', weight.playableMoves); return dice; @@ -303,16 +313,16 @@ Rule.prototype.validateMove = function(game, player, piece, steps) { console.log('No such move left!'); return false; } - + if (piece.type !== player.currentPieceType) { console.log('Piece is of wrong type!'); return false; } - - if (this.isMoveActionRestricted(game.state, game.turnDice.movesLeft, piece, steps)) { + + if (this.isMoveActionRestricted(game.state, game.turnDice.movesLeft, piece, steps, model.MoveActionType.MOVE)) { return false; } - + return true; }; @@ -407,29 +417,29 @@ Rule.prototype.validateUndo = function(game, player) { * @returns {Object} - Map containing maximum weight for each branch, indexed by piece ID * and total maximum weight for all branches, accessed with 'max' index */ -Rule.prototype.calculateMoveWeights = function(state, movesLeft, pieceType, rootPiece, stopAtMax) { +Rule.prototype.calculateMoveWeights = function(state, movesLeft, pieceType, rootPiece, stopAtMax, moveType) { var weight = {}; weight.max = 0; weight.playableMoves = []; - + var self = this; - + var movesLeftSum = 0; for (var i = 0; i < movesLeft.length; i++) { movesLeftSum += movesLeft[i]; } - + // TODO: Replace recursion with linear loop over a queue // Don't check moves twice (eg. 5:2 and 2:5 for the same pieces) - - function calculateBranchWeights(st, moves, id, branchSum, level, branchMoves) { + + function calculateBranchWeights(st, moves, id, branchSum, level, branchMoves, moveType) { // 1. Try out all possible moves (for all of player's pieces). // 2. Sum the move values for all resulting branches with possible moves. // 3. Check if the move request of the player can be used in a branch that allows // all move values to be used. // 4. If there are no better branches than the one chosen by the player, allow the // move - + /** * Check if recursion should stop because a branch that allows * the player to use all moves has been found. @@ -445,91 +455,16 @@ Rule.prototype.calculateMoveWeights = function(state, movesLeft, pieceType, root } return false; } - - /** Local copy of moves left */ - var movesLeft = moves.slice(); - - // Get steps (value) for next move - var steps = movesLeft.shift(); - if (!steps) { - return; - } - - //console.log('Piece type:', pieceType); - console.log(Array(level + 2).join("-") + ' Begin consider move ' + steps); - - // Iterate all of player's pieces - for (var p = 0; p < st.pieces[pieceType].length; p++) { - var piece = st.pieces[pieceType][p]; - if ((!piece) || (piece.type !== pieceType)) { - continue; - } - - // If a root piece has been specified, check - // only the branches that start at this piece. - // Ignore other branches - if (level === 0 && rootPiece) { - if (rootPiece.id !== piece.id) { - continue; - } - } - - // Do not check pieces that are already outside the board - if (model.State.isPieceOutside(st, piece)) { - continue; - } - - console.log('Piece ID', piece.id); - console.log('Outside', st.outside); - if (piece.id === 5) { - console.log('TEN'); - console.log(piece.type); - console.log(st.outside[0].length); - console.log(st.outside[0]); - console.log(st.outside); - console.log(pieceType); - } - - // Check if the player has any pieces on bar. If that is the - // case only pieces on the bar can be moved - if (model.State.havePiecesOnBar(st, pieceType)) { - // Player can only move the top piece on the bar. - // If there are more pieces on the bar they could be moved on next - // move, but not on this one - if (model.State.getBarTopPiece(st, pieceType).id !== piece.id) { - continue; - } - //console.log('Bar'); - } - else { - // If there are no pieces on the bar, make sure this piece is the - // top piece at its position. Only top pieces can be moved - var pos = model.State.getPiecePos(st, piece); - if (model.State.getTopPiece(st, pos).id !== piece.id) { - continue; - } - //console.log('Pos', pos); - } - // Make a deep copy of the state. Moves will be applied to the copy. The - // copy will be passed one level down - to the move (next node of the branch). - var tempState = model.Utils.deepCopy(st); - - // Check if current piece can be moved - var actions = self.getMoveActions(tempState, piece, steps); - //console.log('Piece ID', piece.id); - if (actions.length === 0) { - //console.log('No actions, next piece'); - continue; - } - + // create a new State where we apply these actions + function applyTempActions (actions,tempState,id,branchSum,shouldStop,calculateBranchWeights,level,moveType) { // If yes, apply the move action to the temporary state. - //console.log('Actions', actions); + console.log('Applying actions', actions); self.applyMoveActions(tempState, actions); - + var tempMoves = branchMoves.slice(); tempMoves.push(steps); - + // If we are still at level 0, create a new branch var pieceID = (id !== 0) ? id: piece.id; if (!weight[pieceID]) { @@ -538,45 +473,134 @@ Rule.prototype.calculateMoveWeights = function(state, movesLeft, pieceType, root moves: [] }; } - + // Keep track of the maximum weight for this branch. The branch starts with the first // piece moved (the root node) and check if the moves in this branch (in the nodes so far, // it might extend on next recursion) have a total sum greather than the one saved for this // branch (the root node - associated with the first piece being moved). - + var w = branchSum + steps; - + if (w > weight[pieceID].max) { weight[pieceID].max = w; } weight[pieceID].moves = tempMoves; - + if (w > weight.max) { weight.max = w; weight.playableMoves = tempMoves; } - + if (shouldStop()) { return; } if (movesLeft.length > 0) { - calculateBranchWeights(tempState, movesLeft, pieceID, w, level + 1, tempMoves); + calculateBranchWeights(tempState, movesLeft, pieceID, w, level + 1, tempMoves, moveType); } } - + + /** Local copy of moves left */ + var movesLeft = moves.slice(); + + // Get steps (value) for next move + var steps = movesLeft.shift(); + if (!steps) { + return; + } + + //console.log('Piece type:', pieceType); + console.log(Array(level + 2).join("-") + ' Begin consider move with die=' + steps); + + // Iterate all of player's pieces + for (var p = 0; p < st.pieces[pieceType].length; p++) { + var piece = st.pieces[pieceType][p]; + if ((!piece) || (piece.type !== pieceType)) { + continue; + } + + // If a root piece has been specified, check + // only the branches that start at this piece. + // Ignore other branches + if (level === 0 && rootPiece) { + if (rootPiece.id !== piece.id) { + continue; + } + } + + // Do not check pieces that are already outside the board + if (model.State.isPieceOutside(st, piece)) { + continue; + } + + console.log('Considering moving Piece ID', piece.id); + // console.log('Outside', st.outside); + // if (piece.id === 5) { + // console.log('TEN'); + // console.log(piece.type); + // console.log(st.outside[0].length); + // console.log(st.outside[0]); + // console.log(st.outside); + // console.log(pieceType); + // } + + // Check if the player has any pieces on bar. If that is the + // case only pieces on the bar can be moved + if (model.State.havePiecesOnBar(st, pieceType)) { + // Player can only move the top piece on the bar. + // If there are more pieces on the bar they could be moved on next + // move, but not on this one + if (model.State.getBarTopPiece(st, pieceType).id !== piece.id) { + continue; + } + //console.log('Bar'); + } + else { + // If there are no pieces on the bar, make sure this piece is the + // top piece at its position. Only top pieces can be moved + var pos = model.State.getPiecePos(st, piece); + if (model.State.getTopPiece(st, pos).id !== piece.id) { + continue; + } + //console.log('Pos', pos); + } + + // Make a deep copy of the state. Moves will be applied to the copy. The + // copy will be passed one level down - to the move (next node of the branch). + var tempState = model.Utils.deepCopy(st); + + // Check if current piece can be moved either UP or MOVE + var moveActions = self.getMoveActions(tempState, piece, steps, model.MoveActionType.MOVE); + + // If the piece can be moved + //console.log('Piece ID', piece.id); + if (moveActions.length === 0) { + console.log('No MOVE actions for this piece'); + } + else { + applyTempActions(moveActions,tempState,id,branchSum,shouldStop,calculateBranchWeights,level,model.MoveActionType.MOVE); + } + var upActions = self.getMoveActions(tempState, piece, steps, model.MoveActionType.UP); + if (upActions.length === 0) { + console.log('No UP actions for this piece'); + } + else { + applyTempActions(upActions,tempState,id,branchSum,shouldStop,calculateBranchWeights,level,model.MoveActionType.UP); + } + } + console.log(Array(level + 2).join("-") + ' End consider move ' + steps); } - + console.time('Recursion time'); - + // Simulate moving the piece with all dice values, starting from highest die value // (eg. for dice 5:3 try moving 5 first and after that 3). Try this for all pieces // (multiple branches) console.log('moves 1:', movesLeft); - calculateBranchWeights(state, movesLeft, 0, 0, 0, []); + calculateBranchWeights(state, movesLeft, 0, 0, 0, [], moveType); console.log('Intermediate weight:', weight); - + // Then try playing from lowest die value first // (eg. for dice 5:3 try moving 3 first and after that 5). Also try this for all pieces // (more branches) @@ -585,13 +609,13 @@ Rule.prototype.calculateMoveWeights = function(state, movesLeft, pieceType, root movesLeft = movesLeft.slice(); movesLeft.reverse(); console.log('moves2:', movesLeft); - calculateBranchWeights(state, movesLeft, 0, 0, 0, []); + calculateBranchWeights(state, movesLeft, 0, 0, 0, [], moveType); } console.log('Final weight:', weight); - + console.timeEnd('Recursion time'); - + return weight; }; @@ -609,17 +633,17 @@ Rule.prototype.calculateMoveWeights = function(state, movesLeft, pieceType, root * @param {number} steps - Number of steps to move * @returns {boolean} - Returns true if move is restricted (not allowed). */ -Rule.prototype.isMoveActionRestricted = function(state, movesLeft, piece, steps) { +Rule.prototype.isMoveActionRestricted = function(state, movesLeft, piece, steps, moveType) { // 1. Try out all possible moves (for all of player's pieces). // 2. Sum the move values for all resulting branches with possible moves. // 3. Check if the move request of the player can be used in a branch that allows // all move values to be used. // 4. If there are no better branches than the one chosen by the player, allow the // move - - var weight = this.calculateMoveWeights(state, movesLeft, piece.type, piece, false); + + var weight = this.calculateMoveWeights(state, movesLeft, piece.type, piece, false, moveType); var maxWeight = weight.max; - + if ((!weight[piece.id]) || (weight[piece.id].max < maxWeight)) { console.log('There is better move. Piece weight:', weight[piece.id]); return true; @@ -651,7 +675,7 @@ Rule.prototype.isMoveActionRestricted = function(state, movesLeft, piece, steps) * @returns {MoveAction[]} - List of actions if move is allowed, empty list otherwise. * @see {@link RuleBgCasual.getMoveActions} for an example on how to implement this method */ -Rule.prototype.getMoveActions = function(state, piece, steps) { +Rule.prototype.getMoveActions = function(state, piece, steps, moveType) { throw new Error("Abstract method!"); }; @@ -683,6 +707,9 @@ Rule.prototype.applyMoveActions = function(state, actionList) { else if (action.type === model.MoveActionType.BEAR) { this.bear(state, action.piece); } + else if (action.type === model.MoveActionType.UP) { + this.up(state, action.piece, action.to); + } } }; @@ -699,6 +726,7 @@ Rule.prototype.place = function (state, number, type, position) { var piece = new model.Piece(type, state.nextPieceID); state.pieces[type].push(piece); state.points[position].push(piece); + state.heightOverrides[state.nextPieceID] = 0; state.nextPieceID++; } }; @@ -714,7 +742,7 @@ Rule.prototype.place = function (state, number, type, position) { Rule.prototype.move = function(state, piece, toPos) { // Find the current position of the piece var fromPos = model.State.getPiecePos(state, piece); - + var topPiece = state.points[fromPos].pop(); if (!topPiece) { throw new Error("No piece found at position " + parseInt(fromPos) + " !"); @@ -739,7 +767,7 @@ Rule.prototype.move = function(state, piece, toPos) { Rule.prototype.bear = function (state, piece) { // Find the current position of the piece var fromPos = model.State.getPiecePos(state, piece); - + var topPiece = state.points[fromPos].pop(); if (!topPiece) { throw new Error("No piece found at position " + parseInt(fromPos) + " !"); @@ -754,6 +782,41 @@ Rule.prototype.bear = function (state, piece) { state.outside[piece.type].push(topPiece); }; +/** + * UP piece - remove from board and place outside + * @memberOf Rule + * @param {State} state - Board state + * @param {Piece} piece - Piece to move UP + * @param {int} steps - How high to move up + * @throws Throws an error if there is no piece at fromPos or piece is of wrong type + */ +Rule.prototype.up = function (state, piece, steps) { + // Find the current position of the piece + var fromPos = model.State.getPiecePos(state, piece); + + var topPiece = state.points[fromPos].pop(); + if (!topPiece) { + throw new Error("No piece found at position " + parseInt(fromPos) + " !"); + } + // put it back because it'll remain on this point + state.points[fromPos].push(topPiece); + + if (topPiece.id !== piece.id) { + console.log(fromPos, topPiece); + throw new Error("The top piece at position " + fromPos + " is different than the one the player wants to move!"); + } + + // set the HEIGHT appropriately + // TODO(risher): move this into the Piece itself + if (state.heightOverrides[piece] === undefined) + state.heightOverrides[piece] = 0; + state.heightOverrides[piece] += steps; + if (piece.heightBoost === undefined) + piece.heightBoost = 0; + piece.heightBoost += steps; + +}; + /** * Hit piece - send piece to bar * @memberOf Rule @@ -764,7 +827,7 @@ Rule.prototype.bear = function (state, piece) { Rule.prototype.hit = function (state, piece) { // Find the current position of the piece var fromPos = model.State.getPiecePos(state, piece); - + var topPiece = state.points[fromPos].pop(); if (!topPiece) { throw new Error("No piece found at position " + parseInt(fromPos) + " !"); @@ -832,7 +895,7 @@ Rule.prototype.hasWon = function (state, player) { /** * Check game state and determine how much points the player * should be awared for this state. - * + * * If opponent player has not borne any pieces, award 2 points. * If opponent has not borne any pieces, and still has pieces in home field of player, award 3 points. * In all other cases award 1 point. @@ -847,7 +910,7 @@ Rule.prototype.getGameScore = function (state, player) { model.PieceType.BLACK : model.PieceType.WHITE; - + if (state.outside[oppType].length <= 0) { // The opponent has not borne any pieces, so we need to check // if the player should be awarded 2 or 3 points @@ -871,8 +934,8 @@ Rule.prototype.getGameScore = function (state, player) { * Start next turn: * 1. Reset turn * 2. Change players - * 3. Roll new dice - * + * 3. Roll new dice + * * @memberOf Rule * @param {Match} match - Match */