jocly/src/core/jocly.game.js
2017-04-04 23:35:05 +02:00

1297 lines
36 KiB
JavaScript

/* Copyright 2017 Jocly
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the GNU Affero General Public License in all respects
* for all of the code used other than as permitted herein. If you modify
* file(s) with this exception, you may extend this exception to your
* version of the file(s), but you are not obligated to do so. If you do not
* wish to do so, delete this exception statement from your version. If you
* delete this exception statement from all source files in the program,
* then also delete it in the license file.
*/
try {
exports.Game = JocGame = function() {}
exports.Board = JocBoard = function() {}
exports.Move = JocMove = function() {}
} catch(e) {
global.JocGame = exports.Game = function() {};
global.JocBoard = exports.Board = function() {};
global.JocMove = exports.Move = function() {};
(function() {
var r = require;
var ju = r("./jocly.util.js");
global.MersenneTwister = ju.MersenneTwister;
global.JocUtil = ju.JocUtil;
global.JoclyUCT = r("./jocly.uct.js").JoclyUCT;
})();
}
JocGame.PLAYER_A = 1;
JocGame.PLAYER_B = -1;
JocGame.DRAW = 2;
if(typeof document!="undefined")
JocGame.CLICK=('ontouchstart' in document.documentElement)?"touchstart":"click";
else
JocGame.CLICK="click";
/*
JocGame.MOUSEMOVE_EVENT=('ontouchstart' in document.documentElement)?"touchmove":"mousemove";
JocGame.MOUSEDOWN_EVENT=('ontouchstart' in document.documentElement)?"touchstart":"mousedown";
JocGame.MOUSEUP_EVENT=('ontouchstart' in document.documentElement)?"touchend":"mouseup";
*/
JocGame.MOUSEMOVE_EVENT="touchmove mousemove";
JocGame.MOUSEDOWN_EVENT="touchstart mousedown";
JocGame.MOUSEUP_EVENT="touchend mouseup joclyclick";
/* biggest integer with unit precision:
Math.pow(2,53)-1 < Math.pow(2,53) is true
Math.pow(2,54)-1 < Math.pow(2,54) is false */
JocGame.MAX_VALUE = Math.pow(2,53);
JocGame.prototype = {}
JocGame.prototype.Init = function(aOptions) {
this.mWho = JocGame.PLAYER_A;
this.mViewAs = JocGame.PLAYER_A;
this.mTopLevel = 3;
this.mLoopMax = 300;
this.mPreventRepeat = false;
if(aOptions) {
this.mOptions = aOptions.game;
this.mViewOptions = aOptions.view;
this.mSkin = this.mViewOptions.skins[0].name; // TODO check if 3D not supported
this.mNotation=false;
this.mShowMoves=this.mViewOptions.useShowMoves;
this.mSounds=!!this.mViewOptions.sounds;
this.mAutoComplete=false;
if(typeof(this.mOptions.level)!="undefined")
this.mTopLevel = this.mOptions.level;
if (typeof(this.mOptions.loopMax)!="undefined")
this.mLoopMax = this.mOptions.loopMax;
this.mVisitedBoards = {};
if(typeof(this.mOptions.viewAs)!="undefined")
this.mViewAs = this.mOptions.viewAs;
}
this.mNextSchedule = null;
this.mPlayedMoves = [];
this.mFullPlayedMoves = [];
this.mViewInited = false;
this.mGameInited = false;
if(aOptions && aOptions.initial)
this.GameInitGame(aOptions.initial);
else
this.GameInitGame();
this.mBoard = new (this.GetBoardClass())(this);
if(this.mBoard.InitialPosition)
this.mBoard.InitialPosition(this);
this.mBoard.mMoves=[];
this.mBoard.mWho = this.mWho;
this.listeners = [];
}
JocGame.prototype.AddListener = function(listener) {
this.listeners.push(listener);
}
JocGame.prototype.RemoveListener = function(listener) {
for(var i=this.listeners.length-1;i>=0;i--)
if(this.listeners[i]==listener)
this.listeners.splice(i,1);
}
JocGame.prototype.DispatchMessage = function(message) {
var self = this;
this.listeners.forEach(function(listener) {
listener.call(self,message);
});
}
JocGame.prototype.HumanMove = function(move) {
this.DispatchMessage({
type: "human-move",
move: move
});
}
JocGame.prototype.MachineMove = function(result) {
this.DispatchMessage({
type: "machine-move",
result: result
});
}
JocGame.prototype.MachineProgress = function(progress) {
this.DispatchMessage({
type: "machine-progress",
progress: progress
});
}
JocGame.prototype.PlayMove = function(move) {
var self = this;
var promise = new Promise(function(resolve,reject) {
self.mOldBoard=new (self.GetBoardClass())(self);
self.mOldBoard.CopyFrom(self.mBoard);
self.ApplyMove(move);
var moveShown = self.mBoard.PlayedMove(self,move);
if(moveShown)
resolve();
else
self.MoveShown = function() {
delete self.MoveShown;
resolve();
}
});
return promise;
}
JocGame.prototype.InvertWho = function() {
var who = this.GetWho();
this.SetWho(-who);
}
JocGame.prototype.AttachElement = function (element, options) {
options = options || {};
var game = this;
this.widget = element;
var promise = new Promise(function(resolve, reject) {
if (game.gamePreAttachProto)
reject(new Error("Game already attached"));
else {
var systemJSConfig = {
meta: {
"jocly-xdview.js": {
globals: {
jQuery: "jquery.js",
THREE: "three.js"
}
}
}
}
systemJSConfig.meta["games/" + game.module + "/" + game.name + "-view.js"] = {
globals: {
xdview: "jocly-xdview.js",
jQuery: "jquery.js",
THREE: "three.js"
}
}
};
SystemJS.config(systemJSConfig);
Promise.all([
SystemJS.import("jocly-xdview.js"),
SystemJS.import("games/" + game.module + "/" + game.name + "-view.js")
]).then(function(args) {
var xdview = args[0], view = args[1];
game.gamePreAttachProto = Object.getPrototypeOf(game);
var gameProto = Object.assign({}, game.gamePreAttachProto, xdview.view.Game, view.view.Game);
Object.setPrototypeOf(game, gameProto);
var Board = game.mBoardClass;
game.boardPreAttachProto = Board.prototype;
Object.assign(Board.prototype, game.boardPreAttachProto, xdview.view.Board, view.view.Board);
var Move = game.mMoveClass;
game.movePreAttachProto = Move.prototype;
Object.assign(Move.prototype, game.movePreAttachProto, view.view.Move);
game.mGeometry = {
width: game.widget.clientWidth,
height: game.widget.clientHeight
}
game.mWidget = jQuery(game.widget);
game.UpdateSounds();
resolve();
}, function(e) {
reject(e);
});
});
return promise;
}
JocGame.prototype.DetachElement = function () {
var game = this;
this.widget = element;
var promise = new Promise(function(resolve, reject) {
if (!game.gamePreAttachProto)
reject(new Error("Game not attached"));
else {
// TODO
resolve();
}
});
return promise;
}
JocGame.prototype.GetBoardClass = function() {
return this.mBoardClass;
}
JocGame.prototype.GetMoveClass = function() {
return this.mMoveClass;
}
JocGame.prototype.CreateMove = function(args) {
return new this.mMoveClass(args);
}
JocGame.prototype.CloneBoard = function(board) {
var newBoard=new (this.GetBoardClass())(this);
newBoard.CopyFrom(board);
return newBoard;
}
JocGame.prototype.InitView = function() {
console.log("Abstract InitView called");
}
JocGame.prototype.GameInitView = function() {
if(this.mGeometry.width>0 && this.mGeometry.height>0) {
this.InitView();
this.mViewInited=true;
}
}
JocGame.prototype.DestroyView = function() {
if(this.mWidget)
this.mWidget.empty();
}
JocGame.prototype.GameDestroyView = function() {
if(this.mViewInited) {
this.DestroyView();
this.mViewInited=false;
}
}
JocGame.prototype.CanPlaySound = function(tag) {
return true;
}
JocGame.prototype.UpdateSounds = function() {
var joclySounds = $("#jocly-sounds");
if(joclySounds.length==0)
joclySounds = $("<div/>").attr("id","jocly-sounds").css({display:"none"}).appendTo($("body"));
function AddSound(tag, path, fname) {
var audio = $("<audio/>").attr("id", "jocly-sound-" + tag).attr("preload","auto");
$("<source/>").attr("src", path + "/res/sounds/" + fname + ".ogg").attr("type", "audio/ogg").appendTo(audio);
$("<source/>").attr("src", path + "/res/sounds/" + fname + ".mp3").attr("type", "audio/mp3").appendTo(audio);
audio.appendTo(joclySounds);
}
joclySounds.empty();
var defaultSounds = {
useraction: "bells1",
usermove: "bells1",
win: "winblues",
loss: "lose",
end: "draw",
}
for (var i in defaultSounds)
AddSound(i, this.config.baseURL, defaultSounds[i]);
if (this.config.view.sounds) {
for (var i in this.config.view.sounds) {
$("#jocly-sound-" + i).remove();
if (this.config.view.sounds[i])
if (this.config.view.sounds[i])
AddSound(i, this.config.baseURL+"games/"+this.config.model.module, this.config.view.sounds[i]);
}
}
}
JocGame.prototype.PlaySound = function(tag) {
if(!this.CanPlaySound(tag))
return;
var audio=document.getElementById("jocly-sound-"+tag);
if(audio && this.mSounds) {
if(typeof this.mNeedPhonegapMedia=="undefined") {
this.mNeedPhonegapMedia=false;
this.mNeedPhonegapMedia = window && window.cordova && (typeof Media != "undefined");
}
if(this.mNeedPhonegapMedia) {
if(typeof this.mPhonegapMediaLib=="undefined")
this.mPhonegapMediaLib={};
if(typeof this.mPhonegapMediaLib[tag]=="undefined") {
var node=audio.firstChild;
while(node) {
if(/source/i.test(node.nodeName) && node.getAttribute("type")=="audio/mp3")
break;
node=node.nextSibling;
}
if(node) {
var src=node.getAttribute("src");
var m=/^([^#\?]*)\/[^#\?]+/.exec(window.location.pathname);
if(m)
src=src.replace(/^\./,m[1]);
src=src.replace(/%20/g," ");
this.mPhonegapMediaLib[tag]=new Media(src, function() {
//console.info("PlaySound: Media played "+src);
},function(error) {
console.warn("Jocly PlaySound: Media did not play "+error.code);
},function(status) {
//console.info("PlaySound: mediaStatus "+status);
});
} else
this.mPhonegapMediaLib[tag]=null;
}
if(this.mPhonegapMediaLib[tag]) {
this.mPhonegapMediaLib[tag].play();
}
} else
audio.cloneNode(true).play();
}
}
JocGame.prototype.InitGame = function() {
}
JocGame.prototype.GameInitGame = function() {
if(this.mGameInited==false) {
this.mVisitedBoards={};
if(arguments.length>0 && arguments[0])
this.mInitial=arguments[0];
else
this.mInitial=null;
this.InitGame();
this.mGameInited=true;
}
}
JocGame.prototype.DestroyGame = function() {
}
JocGame.prototype.GameDestroyGame = function() {
if(this.mGameInited) {
this.DestroyGame();
this.mGameInited=false;
}
if(this.aiWorker) {
try {
this.aiWorker.terminate();
delete this.aiWorker;
} catch(e) {
console.warn("Cannot terminate worker",e);
}
}
}
JocGame.prototype.DisplayBoard = function() {
if(this.mBoard.Display)
this.mBoard.Display(this);
}
JocGame.prototype.SetWho = function(aWho) {
this.mWho = aWho;
this.mBoard.mWho = aWho;
}
JocGame.prototype.GetWho = function() {
return this.mWho;
}
JocGame.prototype.HumanTurn = function() {
if(!this.mBoard.mMoves || this.mBoard.mMoves.length==0) {
this.mCurrentLevel=-1;
this.mBoard.GenerateMoves(this);
}
this.mBoard.HumanTurn(this);
}
JocGame.prototype.HumanTurnEnd = function() {
this.mBoard.HumanTurnEnd(this);
}
JocGame.prototype.PlayedMove = function(aMove, aOldBoard) {
this.mOldBoard=aOldBoard;
return this.mBoard.PlayedMove(this,aMove);
}
JocGame.prototype.ShowEnd = function() {
return this.mBoard.ShowEnd(this);
}
JocGame.prototype.EvaluateBoard = function() {
this.mBoard.mFinished=false;
this.mBoard.mMoves=[];
this.mCurrentLevel=-1;
this.mBoard.GenerateMoves(this);
if(this.mBoard.mFinished==false)
this.mBoard.Evaluate(this,true,true);
//JocLog("EvaluatedBoard "+JSON.stringify(this.mBoard));
}
JocGame.prototype.GetFinished = function() {
this.SetWho(-this.mWho);
var moves=this.mBoard.mMoves;
this.EvaluateBoard();
this.mBoard.mMoves=moves;
this.SetWho(-this.mWho);
if(this.mBoard.mFinished)
return this.mBoard.mWinner;
else
return 0;
}
JocGame.prototype.IsValidMove = function(args) {
var move = new (this.GetMoveClass())(args);
return this.mBoard.IsValidMove(this,move);
}
JocGame.prototype.AddVisit = function(board,sign) {
if(board)
sign=board.GetSignature();
var visits=this.mVisitedBoards[sign];
if(visits===undefined)
this.mVisitedBoards[sign]=1;
else
this.mVisitedBoards[sign]++;
}
JocGame.prototype.RemoveVisit = function(board,sign) {
if(board)
sign=board.GetSignature();
var visits=this.mVisitedBoards[sign];
if(visits!==undefined) {
if(visits>1)
this.mVisitedBoards[sign]--;
else
delete this.mVisitedBoards[sign];
}
}
var engdbg_loops, engdbg_time, engdbg_t0;
JocGame.prototype.StartMachine = function(aOptions) {
engdbg_loops=0;
engdbg_time=0;
engdbg_t0=Date.now();
this.mDoneCallback=aOptions.Done || this.MachineMove;
this.mProgressCallback=aOptions.Progress || this.MachineProgress;
if(typeof(aOptions.level)!="undefined")
this.mTopLevel=aOptions.level;
if(typeof(aOptions.maxDepth)!="undefined")
this.mTopLevel=aOptions.maxDepth;
this.mStartTime = new Date().getTime();
this.mExploredCount = 0;
this.mPickedMoveIndex = 0;
this.mBestMoves = [];
this.mContexts = [];
this.mDuration = 0;
this.mAborted = false;
this.mRandomSeed = 0;
if(aOptions.randomSeed && !isNaN(parseInt(aOptions.randomSeed)))
this.mRandomSeed = parseInt(aOptions.randomSeed);
if(typeof this.mBoard.StaticGenerateMoves =="function") {
var moves=this.mBoard.StaticGenerateMoves(this);
if(moves && moves.length>0) {
this.mBestMoves=moves;
JocUtil.schedule(this, "Done", {});
return;
}
}
if(this.mOptions.levelOptions) {
this.mOptions.levelOptionsSaved=JSON.parse(JSON.stringify(this.mOptions.levelOptions));
if(aOptions.level)
Object.assign(this.mOptions.levelOptions,aOptions.level);
}
var aiThread = aOptions.threaded && typeof window=="object" && window.Worker;
if(aOptions.level && aOptions.level.ai=="uct" && JoclyUCT) {
if(aiThread)
this.StartThreadedMachine(aOptions,"uct");
else
JoclyUCT.startMachine(this,aOptions);
}
else { // default is legacy alpha-beta ai
if(aiThread)
this.StartThreadedMachine(aOptions,"alpha-beta");
else {
this.mSavedVisitedBoards={}
for(var s in this.mVisitedBoards)
this.mSavedVisitedBoards[s]=this.mVisitedBoards[s];
this.Engine(this.mBoard, this.mTopLevel, false, 0, aOptions.potential); // start algo
this.Run();
}
}
}
JocGame.prototype.StartThreadedMachine = function(aOptions,algo) {
var $this = this;
delete aOptions.Done;
delete aOptions.Progress;
var t0 = Date.now();
if(!this.aiWorker) {
this.aiWorker = new Worker(this.config.baseURL+'jocly.aiworker.js');
this.aiWorker.postMessage({
type: "Init",
baseURL: this.config.baseURL,
//modelURL: this.config.baseURL+"games/"+this.config.model.module+"/"+this.name+"-model.js",
options: aOptions,
t0: t0
});
}
this.aiWorker.onmessage = function(e) {
var message = e.data;
switch(message.type) {
case "Progress":
$this.mProgressCallback(message.percent);
break;
case "Done":
$this.mBestMoves = message.data.moves;
$this.mPickedMoveIndex = message.data.moveIndex;
$this.mExploredCount = message.data.explored;
$this.mDuration = message.data.duration;
$this.mBoard.evaluation = message.data.evaluation;
$this.Done();
break;
}
}
this.aiWorker.postMessage({
type: "Play",
playedMoves: this.mPlayedMoves,
gameOptions: this.mOptions,
gameName: this.name,
options: aOptions,
algo: algo,
t0: t0
});
}
JocGame.prototype.StopThreadedMachine = function() {
if(this.aiWorker) {
try {
this.aiWorker.terminate();
delete this.aiWorker;
} catch(e) {
console.warn("Cannot terminate worker",e);
}
}
}
JocGame.prototype.ScheduleStep = function() {
this.mNextSchedule = this.ExecuteStep;
}
JocGame.prototype.Random = function(roof) {
var value;
if(this.mRandomSeed)
value = this.mRandomSeed % roof;
else
value = Math.floor(Math.random()*roof);
return value;
}
JocGame.prototype.ArrayShuffle = function(arr) {
var i = arr.length;
if (i<=0) return;
while (--i) {
var j;
if(this.mRandomSeed)
j=this.mRandomSeed%(i+1);
else
j=Math.floor(Math.random()*(i+1));
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
JocGame.prototype.Done = function() {
this.mDuration = new Date().getTime() - this.mStartTime;
if(this.mOptions.levelOptionsSaved) {
this.mOptions.levelOptions=this.mOptions.levelOptionsSaved;
this.mOptions.levelOptionsSaved=null;
}
if(this.mSavedVisitedBoards)
this.mVisitedBoards=this.mSavedVisitedBoards;
if (this.mDoneCallback) {
this.mPickedMoveIndex = this.Random(this.mBestMoves.length);
try {
if(this.mProgressCallback) {
this.mProgressCallback(100);
}
this.mDoneCallback( {
moves : this.mBestMoves,
move : this.mBestMoves[this.mPickedMoveIndex],
moveIndex : this.mPickedMoveIndex,
explored : this.mExploredCount,
duration : this.mDuration,
evaluation : this.mBoard.mEvaluation
});
} catch (e) {
JocLog("!!! Done:" + e,e.stack?e.stack:"");
}
}
}
JocGame.prototype.Run = function() {
var t0=Date.now();
try {
var tNow = new Date().getTime();
while (this.mNextSchedule && new Date().getTime()-tNow<20 && this.mAborted==false) {
var fnt = this.mNextSchedule;
this.mNextSchedule = null;
fnt.call(this);
}
if(this.mAborted) {
this.mAbortCallback();
} else if (this.mNextSchedule) {
JocUtil.schedule(this, "Run", {});
}
} catch(e) {
JocLog("JocGame.Run "+e+"\n"+e.stack);
}
var t1=Date.now();
engdbg_loops++;
engdbg_time+=t1-t0;
}
JocGame.prototype.Abort = function(aAbortCallback) {
var $this=this;
this.mAbortCallback=function() {
if($this.mOptions.levelOptionsSaved) {
$this.mOptions.levelOptions=$this.mOptions.levelOptionsSaved;
$this.mOptions.levelOptionsSaved=null;
}
if($this.mSavedVisitedBoards) {
$this.mVisitedBoards=$this.mSavedVisitedBoards;
$this.mSavedVisitedBoards=null;
}
aAbortCallback();
}
this.mAborted=true;
}
JocGame.prototype.Engine = function(aBoard, aLevel, aBAlpha, aAlpha, aPotential) {
var context = {
mBoard : aBoard,
mLevel : aLevel,
mBAlpha : aBAlpha,
mAlpha : aAlpha,
mBestEvaluation : 0,
mMoveIndex : 0,
mNextBoard : null,
mNextBoards : null
}
this.mContexts.push(context);
context.mBoard.mFinished = false;
context.mBoard.mWinner = JocGame.DRAW;
this.mCurrentLevel=aLevel;
if(context.mBoard.mMoves.length==0)
context.mBoard.GenerateMoves(this);
//JocLog("Level "+aLevel+" "+context.mBoard.mMoves.length+" moves");
if (context.mBoard.mMoves.length == 0 && context.mBoard.mFinished == false) {
context.mBoard.Evaluate(this,true,false);
if(context.mBoard.mFinished == false) {
JocLog("!!! No move possible while not finished - player",this.mWho,"board",context.mBoard);
context.mBoard.mFinished=true;
}
}
//JocLog("No possible move level "+aLevel+" from "+JSON.stringify(context.mBoard));
if(context.mBoard.mFinished) {
switch (context.mBoard.mWinner) {
case JocGame.PLAYER_A:
context.mBoard.mEvaluation = JocGame.MAX_VALUE - (this.mTopLevel - context.mLevel);
break;
case JocGame.PLAYER_B:
context.mBoard.mEvaluation = -JocGame.MAX_VALUE + (this.mTopLevel - context.mLevel);
break;
}
context.mBestEvaluation = context.mBoard.mEvaluation;
this.ExecuteStep2();
return;
}
context.mExploCtrl={
exploFrom: this.mExploredCount,
exploTo: this.mExploredCount+aPotential,
}
if(context.mBoard.QuickEvaluate) {
var boardsMoves=[];
for(var i in context.mBoard.mMoves) {
var board=context.mBoard.MakeAndApply(this,i);
var quickEval=board.QuickEvaluate(this);
boardsMoves.push({
move: context.mBoard.mMoves[i],
board: board,
evaluation: quickEval
});
}
function MoveSort(bm1,bm2) {
return (bm2.evaluation-bm1.evaluation)*context.mBoard.mWho;
}
boardsMoves.sort(MoveSort);
context.mBoard.mMoves=[];
context.mNextBoards=[];
if(typeof this.mOptions.capMoves != "undefined")
boardsMoves=boardsMoves.slice(0,this.mOptions.capMoves);
for(var i in boardsMoves) {
context.mBoard.mMoves.push(boardsMoves[i].move);
context.mNextBoards.push(boardsMoves[i].board);
}
}
this.ExecuteStep();
}
JocGame.prototype.ExecuteStep = function() {
this.mExploredCount++;
// JocLog("# context: "+this.mContexts.length);
var context = this.mContexts[this.mContexts.length - 1];
//JocLog("ExecuteStep level "+context.mLevel+" index "+context.mMoveIndex+"/"+context.mBoard.mMoves.length);
if(context.mNextBoards) {
context.mNextBoard = context.mNextBoards[context.mMoveIndex];
} else {
context.mNextBoard = context.mBoard.MakeAndApply(this,context.mMoveIndex);
}
if(this.mProgressCallback) {
var percent=null;
if(context.mLevel==this.mTopLevel)
percent=Math.floor((context.mMoveIndex*100)/context.mBoard.mMoves.length);
else if(context.mLevel==this.mTopLevel-1) {
var topContext=this.mContexts[0];
var topStep=1/topContext.mBoard.mMoves.length;
percent=Math.floor(100*(topContext.mMoveIndex*topStep+(context.mMoveIndex*topStep/context.mBoard.mMoves.length)));
}
if(percent!=null)
try {
this.mProgressCallback(percent);
} catch(e) {}
}
var nextBoard = context.mNextBoard;
nextBoard.mFinished = false;
nextBoard.mWinner = 0;
nextBoard.Evaluate(this,context.mLevel==0,false,this);
if(context.mLevel<0) // random mode
nextBoard.mEvaluation=0;
// JocLog("Eval2 "+nextBoard.mFinished+"/"+nextBoard.mWinner+"/"+nextBoard.mEvaluation);
if (nextBoard.mFinished) {
switch (nextBoard.mWinner) {
case JocGame.PLAYER_A:
nextBoard.mEvaluation = JocGame.MAX_VALUE - (this.mTopLevel - context.mLevel);
break;
case JocGame.PLAYER_B:
nextBoard.mEvaluation = -JocGame.MAX_VALUE + (this.mTopLevel - context.mLevel);
break;
case JocGame.DRAW:
nextBoard.mEvaluation = 0;
break;
}
} else if(context.mLevel==this.mTopLevel && context.mBoard.mMoves.length==1) {
// one possible move at top level: no need to recurse
} else if (context.mLevel > 0) {
var potential=(context.mExploCtrl.exploTo-this.mExploredCount)/context.mBoard.mMoves.length;
//JocLog("ExecuteStep",potential,context.mLevel,context.mExploCtrl);
if(potential>=1) {
nextBoard.mWho = -nextBoard.mWho; // player changes
this.Engine(nextBoard, context.mLevel - 1, (context.mMoveIndex != 0),
context.mBestEvaluation,potential); // recurse algo
return;
}
}
this.ExecuteStep2();
}
JocGame.prototype.ExecuteStep2 = function() {
var context = this.mContexts[this.mContexts.length - 1];
//JocLog("ExecuteStep2 level "+context.mLevel+" index "+context.mMoveIndex+" "+JSON.stringify(context.mBoard.board));
if(context.mBoard.mMoves.length>0) {
if (context.mMoveIndex == 0) { // first evaluated move
context.mBestEvaluation = context.mNextBoard.mEvaluation; // then it's the best one so far
if (context.mLevel == this.mTopLevel) // if top level
this.SetBest(context.mBoard.mMoves[0], context.mBoard); // store move
} else { // another move evaluated
if (context.mNextBoard.mWho > 0) { // B plays
if (context.mNextBoard.mEvaluation > context.mBestEvaluation) { // best move ?
context.mBestEvaluation = context.mNextBoard.mEvaluation; // remember it
if (context.mLevel == this.mTopLevel) // if top level
this.SetBest(context.mBoard.mMoves[context.mMoveIndex],
context.mBoard); // then store
} else if (context.mLevel == this.mTopLevel
&& context.mNextBoard.mEvaluation == context.mBestEvaluation) { // top level and
// another best
// move
this.AddBest(context.mBoard.mMoves[context.mMoveIndex],
context.mBoard); // add to best moves
}
} else { // A plays
if (context.mNextBoard.mEvaluation < context.mBestEvaluation) { // best move
context.mBestEvaluation = context.mNextBoard.mEvaluation; // keep it
if (context.mLevel == this.mTopLevel)
this.SetBest(context.mBoard.mMoves[context.mMoveIndex],
context.mBoard);
} else if (context.mLevel == this.mTopLevel
&& context.mNextBoard.mEvaluation == context.mBestEvaluation)
this.AddBest(context.mBoard.mMoves[context.mMoveIndex],
context.mBoard);
}
}
}
context.mBoard.mEvaluation = context.mBestEvaluation; // assign best eval
if (context.mBAlpha) { // alpha-beta pruning
if ((context.mBoard.mWho == JocGame.PLAYER_A && context.mBestEvaluation > context.mAlpha)
|| (context.mBoard.mWho == JocGame.PLAYER_B && context.mBestEvaluation < context.mAlpha)) {
context.mMoveIndex = context.mBoard.mMoves.length - 1;
//JocLog("Alpha-beta pruned level");
// ensure no more looking for other moves at this level
}
}
context.mMoveIndex++;
if (context.mMoveIndex < context.mBoard.mMoves.length) {
this.ScheduleStep();
} else {
//JocLog("BestEval level "+context.mLevel+": "+context.mBestEvaluation+" "+context.mMoveIndex+"/"+context.mBoard.mMoves.length);
this.mContexts.pop();
if (this.mContexts.length > 0) {
var context = this.mContexts[this.mContexts.length - 1];
context.mNextBoard.mWho = -context.mNextBoard.mWho;
this.ExecuteStep2();
} else {
delete context.mBoard.mMoves;
//JocLog("Best eval "+context.mBestEvaluation);
this.Done();
}
}
}
JocGame.prototype.SetBest = function(aMove, aBoard) {
var move = new (this.GetMoveClass())({});
move.CopyFrom(aMove);
this.mBestMoves = [ move ];
}
JocGame.prototype.AddBest = function(aMove, aBoard) {
var move = new (this.GetMoveClass())({});
move.CopyFrom(aMove);
this.mBestMoves.push(move);
}
JocGame.prototype.GetRepeatOccurence = function(board) {
if(!this.mOptions.preventRepeat)
return -1;
var repOcc=this.mVisitedBoards[board.GetSignature()];
return repOcc;
}
JocGame.prototype.HandleRepeat = function(board) {
if(this.mOptions.preventRepeat) {
var sign=board.GetSignature(true);
if(this.mVisitedBoards[sign]===undefined)
this.mVisitedBoards[sign]=1;
else
this.mVisitedBoards[sign]++;
}
}
JocGame.prototype.UnhandleRepeat = function(board) {
if(this.mOptions.preventRepeat) {
var sign=board.GetSignature(true);
if(this.mVisitedBoards[sign]==1)
delete this.mVisitedBoards[sign];
else if(this.mVisitedBoards[sign]>1)
this.mVisitedBoards[sign]--;
}
}
JocGame.prototype.ApplyMove = function(aMove) {
var move = new (this.GetMoveClass())({});
move.CopyFrom(aMove);
this.mPlayedMoves.push(move);
if(this.mFullPlayedMoves.length<this.mPlayedMoves.length)
this.mFullPlayedMoves.push(move);
else if(!move.Equals(this.mFullPlayedMoves[this.mPlayedMoves.length-1])) {
this.mFullPlayedMoves=this.mFullPlayedMoves.slice(0,this.mPlayedMoves.length-1);
this.mFullPlayedMoves.push(move);
}
this.mBoard.ApplyMove(this,aMove);
this.mBoard.mMoves=[];
this.HandleRepeat(this.mBoard);
}
JocGame.prototype.BackTo = function(aIndex,moves) {
if(!moves)
moves=this.mFullPlayedMoves;
this.mWho = JocGame.PLAYER_A;
this.mBoard = new (this.GetBoardClass())(this);
if(this.mBoard.InitialPosition)
this.mBoard.InitialPosition(this);
if(this.mInitial && this.mInitial.turn)
this.mWho = this.mInitial.turn;
this.mBoard.mWho = this.mWho;
this.mBestMoves = [];
this.mVisitedBoards={};
this.mPlayedMoves = [];
for(var i=0;i<aIndex;i++) {
this.mBoard.ApplyMove(this,moves[i]);
this.HandleRepeat(this.mBoard);
this.mBoard.mWho=-this.mBoard.mWho;
this.mPlayedMoves.push(moves[i]);
}
this.mWho=this.mBoard.mWho;
}
JocGame.prototype.Load = function(gameData) {
this.mWho = JocGame.PLAYER_A;
this.mBoard = new (this.GetBoardClass())(this);
if(this.mBoard.InitialPosition)
this.mBoard.InitialPosition(this);
this.mBoard.mWho = this.mWho;
var board = this.GetBoardClass();
if(gameData.initialBoard)
board.CopyFrom(gameData.initialBoard);
this.mBestMoves = [];
this.mVisitedBoards={};
var moves=gameData.playedMoves;
this.mPlayedMoves = [];
this.mFullPlayedMoves = [];
for(var i in moves) {
var move=new (this.GetMoveClass())(moves[i]);
if(!this.IsValidMove(move))
throw "invalid-move";
this.mBoard.ApplyMove(this,move);
this.HandleRepeat(this.mBoard);
this.mBoard.mWho=-this.mBoard.mWho;
this.mPlayedMoves.push(move);
this.mFullPlayedMoves.push(move);
this.mBoard.mMoves=[];
}
this.mWho=this.mBoard.mWho;
if(this.mBoard.mFinished==false)
this.mBoard.Evaluate(this,true,true);
}
JocGame.prototype.CloseView = function() {
}
JocMove.prototype = {}
JocMove.prototype.CopyFrom = function(aMove) {
var fields=JSON.parse(JSON.stringify(aMove));
for(var f in fields) {
this[f]=fields[f];
}
}
JocMove.prototype.Equals = function(move) {
return JSON.stringify(this)==JSON.stringify(move);
}
JocMove.prototype.ToString = function() {
return JSON.stringify(this);
}
JocMove.prototype.Strip = function() {
return this;
}
JocBoard.prototype = {}
JocBoard.prototype.Init = function(aGame) {
}
JocBoard.prototype.InitBoard = function(aGame) {
this.mDepth = 0; // no depth calc
this.mMoves = []; // move storage
this.mEvaluation = 0; // not evaluated yet
this.mFinished = false;
this.mWinner = 0;
this.Init(aGame);
}
JocBoard.prototype.CopyFrom = function(aBoard) {
var signature=aBoard.mSignature;
delete(aBoard.mSignature);
var fields=JSON.parse(JSON.stringify(aBoard));
for(var f in fields) {
this[f]=fields[f];
}
aBoard.mSignature=signature;
}
JocBoard.prototype.GetSignature = function() {
if(arguments[0] || !this.mSignature) {
var moves=this.mMoves;
delete(this.mMoves);
delete(this.mSignature);
this.mSignature=JocUtil.md5(JSON.stringify(this));
//JocLog("signature",this.mSignature,this);
this.mMoves=moves;
}
return this.mSignature;
}
JocBoard.prototype.ApplyMove = function(aGame,aMove) {
JocLog("Method JocBoard:ApplyMove() must be overloaded");
// must be overloaded
}
JocBoard.prototype.GenerateMoves = function(aGame) {
JocLog("Method JocBoard:GenerateMoves() must be overloaded");
// must be overloaded
}
JocBoard.prototype.Evaluate = function(aGame,aFinishOnly,aTopLevel) {
JocLog("Method JocBoard:Evaluate() must be overloaded");
this.mEvaluation = 0; // must be overloaded
}
JocBoard.prototype.HumanTurn = function() {
}
JocBoard.prototype.HumanTurnEnd = function() {
}
JocBoard.prototype.PlayedMove = function() {
}
JocBoard.prototype.ShowEnd = function() {
}
JocBoard.prototype.MakeAndApply = function(aGame,aIndex) {
var board = new (aGame.GetBoardClass())(aGame);
board.CopyFrom(this);
board.mWho = this.mWho;
board.mBoardClass = this.mBoardClass;
board.ApplyMove(aGame,this.mMoves[aIndex]); // apply move
board.mMoves=[];
return board;
}
JocBoard.prototype.IsValidMove = function(aGame,move) {
if(typeof move.Equals != "function")
move=aGame.CreateMove(move);
if(!this.mMoves || this.mMoves.length==0) {
this.mCurrentLevel=-1;
this.GenerateMoves(aGame);
}
for(var i in this.mMoves) {
if(move.Equals(this.mMoves[i]))
return true;
}
console.error("Invalid move "+JSON.stringify(move)+" in "+JSON.stringify(this.mMoves));
return false;
}
JocBoard.prototype.PushMove = function(aGame,args) {
this.mMoves.push(aGame.CreateMove(args));
}
JocBoard.prototype.GenerateMoveObjects = function(aGame) {
var moves=[];
this.mMoves=[];
this.GenerateMoves(aGame);
for(var i=0;i<this.mMoves.length;i++)
moves.push(aGame.CreateMove(this.mMoves[i]));
this.mMoves=moves;
}
JocBoard.prototype.ExportBoardState = function(aGame) {
return JSON.stringify(this);
}
JocGame.prototype.GetBestMatchingMove = function(moveStr,candidateMoves) {
var prettyMoves=[];
var $this=this;
candidateMoves.forEach(function(m) {
if(typeof m.ToString=="function")
prettyMoves.push(m.ToString());
else
prettyMoves.push($this.CreateMove(m).ToString());
});
var bestDist=Infinity;
var bestMatches=[];
candidateMoves.forEach(function(candidate,index) {
var dist=JocGame.Levenshtein(moveStr,prettyMoves[index])/(Math.max(prettyMoves[index].length,moveStr.length)+1);
if(dist==bestDist)
bestMatches.push(index);
else if(dist<bestDist) {
bestMatches=[index];
bestDist=dist;
}
});
if(bestMatches.length==1)
return candidateMoves[bestMatches[0]];
var candidateIndexes=bestMatches;
var matches=[];
candidateIndexes.forEach(function(index) {
var pretty=prettyMoves[index];
if(moveStr.indexOf(pretty)>=0 || pretty.indexOf(moveStr)>=0)
matches.push(index);
});
if(matches.length==1)
return candidateMoves[matches[0]];
bestDist=Infinity;
bestMatches=[];
candidateIndexes.forEach(function(index) {
var dist=0;
var str1=moveStr.replace(/[A-Z]/g,'');
var str2=prettyMoves[index].replace(/[A-Z]/g,'');
dist+=JocGame.Levenshtein(str1,str2)/(Math.max(str1.length,str2.length)+1);
dist+=(str1.indexOf(str2)>=0 || str2.indexOf(str1)>=0)?0:1;
str1=moveStr.replace(/[a-z]/g,'');
str2=prettyMoves[index].replace(/[a-z]/g,'');
dist+=JocGame.Levenshtein(str1,str2).distance/(Math.max(str1.length,str2.length)+1);
dist+=(str1.indexOf(str2)>=0 || str2.indexOf(str1)>=0)?0:1;
if(dist==bestDist)
bestMatches.push(index);
else if(dist<bestDist) {
bestMatches=[index];
bestDist=dist;
}
});
if(bestMatches.length==1)
return candidateMoves[bestMatches[0]];
return null;
}
JocBoard.prototype.PickMoveFromDatabase = function(aGame,database) {
if(!this.mMoves || this.mMoves.length==0) {
var moves=[];
this.mMoves=[];
this.GenerateMoves(aGame);
for(var i=0;i<this.mMoves.length;i++)
moves.push(aGame.CreateMove(this.mMoves[i]));
this.mMoves=moves;
}
if(this.mMoves.length==0)
return null;
var key=""+this.mWho+"#"+this.GetSignature();
var dbMoves=database[key];
if(!dbMoves)
return null;
var totalEval=0;
for(var i=0;i<dbMoves.length;i++)
totalEval+=dbMoves[i].e;
var rnd=Math.random()*totalEval;
var current=0;
for(var i=0;i<dbMoves.length;i++) {
var dbMove=dbMoves[i];
current+=dbMove.e;
if(current>rnd) {
var pickedMove=aGame.GetBestMatchingMove(dbMove.m,this.mMoves);
if(pickedMove)
return [pickedMove];
}
}
return null; // never reached
}
JocBoard.prototype.CompactMoveString = function(aGame,aMove) {
if(typeof aMove.ToString!="function")
aMove=aGame.CreateMove(aMove);
return aMove.ToString();
}
/*-- Zobrist implementation --*/
JocGame.Zobrist=function(params) {
var mt=new MersenneTwister(12345);
var paramNames=[];
for(var f in params)
paramNames.push(f);
paramNames.sort(); // ensures we walk through parameters always in same order so generated pseudo random seeds are always the same
this.seed={};
for(var pi=0;pi<paramNames.length;pi++) {
var f=paramNames[pi];
var param=params[f];
var seed={
values: {},
seeds:[],
}
var vIndex=0;
for(var vi=0;vi<param.values.length;vi++)
seed.values[param.values[vi]]=vIndex++;
switch(param.type) {
case "array":
for(var j=0;j<param.size;j++) {
var seeds0=[];
for(var i=0;i<vIndex;i++)
seeds0.push(mt.genrand_int32());
seed.seeds.push(seeds0);
}
seed.type="array";
break;
default:
for(var i=0;i<vIndex;i++)
seed.seeds.push(mt.genrand_int32());
seed.type="simple";
}
this.seed[f]=seed;
}
//console.log("Created zobrist",this);
}
JocGame.Zobrist.prototype={
update: function(zobrist,name) {
//var zobrist0=zobrist;
var seed=this.seed[name];
if(seed===undefined) {
console.error("Unknown Zobrist parameter",name);
return 0;
}
var vIndex=seed.values[arguments[2]];
if(vIndex===undefined) {
console.error("Undeclared Zobrist value",arguments[2],"as param",name);
return 0;
}
switch(seed.type) {
case "simple":
zobrist^=seed.seeds[vIndex];
break;
case "array":
var seeds=seed.seeds[arguments[3]];
if(seeds===undefined) {
console.error("Undeclared Zobrist array index",arguments[3],"as param",name);
return 0;
}
zobrist^=seeds[vIndex];
//console.log("Zobrist",zobrist0,"=>",name,"array[",arguments[2],"] =",arguments[3],"=>",zobrist);
break;
}
return zobrist;
},
}
/*--- Levenshtein distance implementation ---*/
JocGame.Levenshtein=function(e,f){if(e==f)return 0;var d=e.length,j=f.length;if(0===d)return j;if(0===j)return d;var b=!1;try{b=!"0"[0]}catch(m){b=!0}
b&&(e=e.split(""),f=f.split(""));for(var b=Array(d+1),g=Array(d+1),a=0,h=0,i=0,a=0;a<d+1;a++)b[a]=a;for(var c="",k="",h=1;h<=j;h++){g[0]=h;k=f[h-1];
for(a=0;a<d;a++){var c=e[a],i=c==k?0:1,c=b[a+1]+1,l=g[a]+1,i=b[a]+i;l<c&&(c=l);i<c&&(c=i);g[a+1]=c}a=b;b=g;g=a}return b[d]};