implemented new app API

This commit is contained in:
mig 2017-04-04 23:55:50 +02:00
parent 8e811c8f0b
commit 7a3510251f
12 changed files with 1003 additions and 797 deletions

View file

@ -26,50 +26,63 @@
function StartGame(gameName) {
var area = $("<div>").addClass("game-area-mini");
function NotifyWinner(winner) {
var txt;
switch(winner) {
case 1:
txt = "Player A wins";
break;
case -1:
txt = "Player B wins";
break;
case 2:
txt = "Draw";
break;
}
area.parent().append($("<div>").addClass("game-winner-mini").text(txt));
}
function Listen(message) {
var game = this;
switch(message.type) {
case "human-move":
if(message.finished)
NotifyWinner(message.winner);
else {
game.machineTurn({
level: game.config.model.levels[2],
threaded: true
});
}
break;
case "machine-move":
if(message.finished)
NotifyWinner(message.winner);
else
game.humanTurn();
break;
}
}
$("<div>").addClass("game-area-mini-cont").append(area).appendTo($("body"));
Jocly.createGame(gameName,area[0]).then((game) => {
game.listen(Listen)
.then(()=>{
game.humanTurn();
function NotifyWinner(winner) {
var txt = "?";
if(winner==Jocly.PLAYER_A)
txt = "Player A wins";
else if(winner==Jocly.PLAYER_B)
txt = "Player B wins";
else if(winner==Jocly.DRAW)
txt = "Draw";
alert(txt);
}
function RunMatch(match, progressBar) {
function NextMove() {
match.getTurn()
.then( (player) => {
var promise;
if(player==Jocly.PLAYER_A)
promise = match.userTurn();
else {
if(progressBar) {
progressBar.style.display = "block";
progressBar.style.width = 0;
}
promise = match.machineSearch({
progress: (progress) => {
if(progressBar)
progressBar.style.width = progress +"%";
}
})
.then( (result) => {
return match.playMove(result.move);
})
.then( () => {
if(progressBar)
progressBar.style.display = "none";
});
}
promise.then( () => {
return match.getFinished()
})
.then( (result) => {
if(result.finished)
NotifyWinner(result.winner);
else
NextMove();
});
})
}
NextMove();
}
Jocly.createMatch(gameName).then((match) => {
match.attachElement(area[0])
.then( () => {
RunMatch(match);
});
});

View file

@ -1,68 +1,65 @@
var progressBar;
function NotifyWinner(winner) {
var txt;
switch(winner) {
case 1:
txt = "Player A wins";
break;
case -1:
txt = "Player B wins";
break;
case 2:
txt = "Draw";
break;
}
var txt = "?";
if(winner==Jocly.PLAYER_A)
txt = "Player A wins";
else if(winner==Jocly.PLAYER_B)
txt = "Player B wins";
else if(winner==Jocly.DRAW)
txt = "Draw";
alert(txt);
}
function PrintMove(game,move) {
game.getMoveString(move)
.then((move)=>{
console.info("=>",move);
});
}
function Listen(message) {
var game = this;
switch(message.type) {
case "human-move":
PrintMove(game,message.move);
if(message.finished)
NotifyWinner(message.winner);
else {
game.machineTurn({
level: game.config.model.levels[2],
threaded: true
});
progressBar.style.display = "block";
}
break;
case "machine-move":
PrintMove(game,message.move);
progressBar.style.display = "none";
if(message.finished)
NotifyWinner(message.winner);
else
game.humanTurn();
break;
case "machine-progress":
progressBar.style.width = message.progress +"%";
break;
function RunMatch(match, progressBar) {
function NextMove() {
match.getTurn()
.then( (player) => {
var promise;
if(player==Jocly.PLAYER_A)
promise = match.userTurn();
else {
if(progressBar) {
progressBar.style.display = "block";
progressBar.style.width = 0;
}
promise = match.machineSearch({
progress: (progress) => {
if(progressBar)
progressBar.style.width = progress +"%";
}
})
.then( (result) => {
return match.playMove(result.move);
})
.then( () => {
if(progressBar)
progressBar.style.display = "none";
});
}
promise.then( () => {
return match.getFinished()
})
.then( (result) => {
if(result.finished)
NotifyWinner(result.winner);
else
NextMove();
});
})
}
NextMove();
}
window.addEventListener("DOMContentLoaded", function () {
progressBar = document.getElementById("progress-bar");
var match = /\?game=([^&]+)/.exec(window.location.href);
var gameName = match && match[1] || "classic-chess";
var m = /\?game=([^&]+)/.exec(window.location.href);
var gameName = m && m[1] || "classic-chess";
var elementId = "game-area1";
var area = document.getElementById(elementId);
Jocly.createGame(gameName,area).then((game) => {
game.listen(Listen)
.then(()=>{
game.humanTurn();
Jocly.createMatch(gameName).then((match) => {
match.attachElement(area)
.then( () => {
RunMatch(match,progressBar);
});
});
});

View file

@ -5,8 +5,8 @@
<head>
<meta charset="utf-8">
<title>Jocly Test - Simple</title>
<meta name="description" content="Jocly simple test">
<title>Jocly Test Multiple</title>
<meta name="description" content="Jocly example multiple boards">
<meta name="author" content="Jocly">
<link rel="stylesheet" href="css/styles.css">

View file

@ -5,8 +5,8 @@
<head>
<meta charset="utf-8">
<title>Jocly Test - Simple</title>
<meta name="description" content="Jocly simple test">
<title>Jocly Example Simple</title>
<meta name="description" content="Jocly simple example">
<meta name="author" content="Jocly">
<link rel="stylesheet" href="css/styles.css">

View file

@ -1,58 +1,50 @@
var jocly = require("../../dist/node");
var Jocly = require("../../dist/node");
var moveCount = 0;
process.on('uncaughtException', function(err) {
console.error(err,err.stack)
});
function RunMatch(match) {
function NextMove() {
var move;
match.machineSearch()
.then( (result) => {
move = result.move;
return match.getMoveString(move);
})
.then( (moveStr) => {
console.info(
((!(moveCount%2) && ((moveCount>>1)+1)+". ") || "")
+(moveCount%2 && " " || "")+" "+moveStr
);
moveCount++;
})
.then( () => {
return match.applyMove(move);
})
.then( (result) => {
if(result.finished) {
if(result.winner==Jocly.PLAYER_A)
console.info("Player A wins");
else if(result.winner==Jocly.PLAYER_B)
console.info("Player B wins");
else if(result.winner==Jocly.DRAW)
console.info("Draw");
} else
NextMove();
})
.catch( (e) => {
console.info("Error",e);
})
function Turn(game) {
game.machineTurn({
level: game.config.model.levels[2],
})
.then(()=>{
},(e)=>{
console.error("cannot machine turn",e);
})
.catch((e)=>{
console.error("error in machine turn",e);
})
;
}
function Listen(message) {
//console.info("Top receives",message);
switch(message.type) {
case "machine-move":
var move = this.getMoveString(message.move)
.then((move)=>{
console.info(
((!(moveCount%2) && ((moveCount>>1)+1)+". ") || "")
+(moveCount%2 && " " || "")+" "+move
);
moveCount++;
if(message.finished)
switch(message.winner) {
case 1: console.info("Player A wins"); break;
case -1: console.info("Player B wins"); break;
case 2: console.info("Draw"); break;
}
else
Turn(this);
});
break;
}
NextMove();
}
jocly.createGame("classic-chess")
.then((game)=>{
game.listen(Listen);
Turn(game);
},(e)=>{
console.info("error",e);
Jocly.createMatch("classic-chess")
.then((match)=>{
RunMatch(match);
})
.catch((e)=>{
console.info("error",e);
})
;
console.info("Error creating match",e);
});

View file

@ -151,7 +151,8 @@ function ProcessJS(stream,concatName,skipBabel) {
stream = stream.pipe(sourcemaps.init());
if(!skipBabel)
stream = stream.pipe(babel({
presets: ['es2015']
presets: ['es2015'],
compact: !!argv.prod
}));
if(argv.prod)
stream = stream.pipe(uglify())
@ -184,17 +185,11 @@ gulp.task("build-node-core",function() {
"src/node/package.json",
]);
var joclyExtraScriptStream =
ProcessJS(gulp.src([
"src/node/jocly.proxy.js"
]));
var allGamesStream = source('jocly-allgames.js');
allGamesStream.end('exports.games = '+JSON.stringify(allGames));
allGamesStream = ProcessJS(allGamesStream.pipe(buffer()));
return merge(joclyCoreStream,allGamesStream,joclyBaseStream,
joclyExtraStream,joclyExtraScriptStream)
return merge(joclyCoreStream,allGamesStream,joclyBaseStream,joclyExtraStream)
.pipe(gulp.dest("dist/node"));
})
@ -248,7 +243,6 @@ gulp.task("build-browser-core",function() {
var joclyExtraScriptsStream = ProcessJS(gulp.src([
"src/browser/jocly.aiworker.js",
"src/browser/jocly.proxy.js",
"src/browser/jocly.embed.js"
]));
@ -322,8 +316,8 @@ gulp.task("clean",function() {
gulp.task("watch",function() {
gulp.watch("src/games/**/*",["build-node-games","build-browser-games"]);
gulp.watch("src/{browser,core,lib}",["build-browser-core","build-browser-xdview"]);
gulp.watch("src/{node,core}",["build-node-core"]);
gulp.watch("src/{browser,core,lib}/**/*",["build-browser-core","build-browser-xdview"]);
gulp.watch("src/{node,core}/**/*",["build-node-core"]);
});
gulp.task("help", function() {

View file

@ -44,7 +44,7 @@ onmessage = function(e) {
importTime = Date.now() - t0;
break;
case "Play":
Jocly.createInternalGame(message.gameName,options).then((game)=>{
Jocly._createInternalGame(message.gameName,options).then((game)=>{
game.mBoard.mMoves = [];
game.Load({
playedMoves: message.playedMoves

View file

@ -25,39 +25,57 @@
* then also delete it in the license file.
*/
var gameId;
var gGame;
var matchId;
var gMatch;
window.addEventListener("message", receiveMessage, false);
window.addEventListener("message", ReceiveMessage, false);
function receiveMessage(event)
function ReceiveMessage(event)
{
console.info("embed receives message",event.data);
//console.info("embed receives message",event.data);
var origin = event.origin || event.originalEvent.origin;
var url = new URL(window.location);
if(origin!=url.origin)
return;
function Reply(message) {
message = message || {};
window.parent.postMessage({
joclyEmbeddedGameId: gameId,
joclyEmbeddedGameId: matchId,
replyId: event.data.replyId,
message: message
},"*");
}
function ReplyError(error) {
if(typeof error=="object")
error = {
message: error.message,
fileName: error.fileName,
lineNumber: error.lineNumber,
stack: error.stack
}
Reply({
type: "error",
error: error
});
}
var message = event.data.message;
switch(message.type) {
case "init":
gameId = message.id;
matchId = message.id;
var area = document.getElementById("area");
Jocly.createInternalGame(message.gameName).then((game) => {
gGame = game;
game.AddListener(Listen);
Jocly.createMatch(message.gameName).then((match) => {
gMatch = match;
match.game.Load({
playedMoves: message.playedMoves
});
function Start() {
game.AttachElement(area).then(()=>{
game.GameInitView();
game.DisplayBoard();
Reply();
});
match.game.AttachElement(area).then(()=>{
match.area = area;
match.game.GameInitView();
match.game.DisplayBoard();
Reply();
})
.catch(ReplyError);
}
if (document.readyState === "complete"
|| document.readyState === "loaded"
@ -67,65 +85,30 @@ function receiveMessage(event)
window.addEventListener("DOMContentLoaded", () => {
Start();
});
});
})
.catch(ReplyError);
break;
case "humanTurn":
gGame.HumanTurn();
case "method":
//console.info("embed execute method",message.methodName,message.args);
try {
gMatch[message.methodName].apply(gMatch,message.args)
.then( function() {
Reply({
args: Array.from(arguments)
})
},ReplyError)
} catch(e) {
ReplyError(e);
}
break;
case "machineTurn":
gGame.StartMachine(message.options);
break;
case "getFinished":
var finished = gGame.GetFinished();
Reply({
finished: !!finished,
winner: finished
});
break;
case "getMoveString":
var move = new (gGame.GetMoveClass())(message.move);
Reply(move.ToString());
break;
}
}
function Listen(message) {
switch(message.type) {
case "human-move":
gGame.ApplyMove(message.move);
var finished = gGame.GetFinished();
if(!finished)
gGame.InvertWho();
gGame.DisplayBoard();
Object.assign(message,{
finished: !!finished,
winner: finished
});
break;
case "machine-move":
var move = message.result.move;
delete message.result.move;
gGame.PlayMove(move)
.then(()=>{
var finished = gGame.GetFinished();
if(!finished)
gGame.InvertWho();
gGame.DisplayBoard();
window.parent.postMessage({
joclyEmbeddedGameId: gameId,
message: {
type: "machine-move",
moveData: message.result,
move: move,
finished: !!finished,
winner: finished
}
},"*");
case "destroy":
var playedMoves = gMatch.game.mPlayedMoves;
gMatch.destroy()
.then( ()=> {
Reply({
playedMoves: playedMoves
});
});
return;
break;
}
window.parent.postMessage({
joclyEmbeddedGameId: gameId,
message: message
},"*");
}

View file

@ -58,7 +58,10 @@ function ExportFunction(fName) {
}
}
["listGames","createGame","createInternalGame"].forEach((fName)=>{
["listGames","createMatch"].forEach((fName)=>{
ExportFunction(fName);
});
exports.PLAYER_A = 1;
exports.PLAYER_B = -1;
exports.DRAW = 2;

View file

@ -1,232 +0,0 @@
/* 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.
*/
var gameConfigs = {}
var gameIdRef = 0;
var messageListenerInstalled = false;
var games = {};
var msgIdRef = 0;
var messageReplies = {};
function MessageListener(event) {
var game = games[event.data.joclyEmbeddedGameId];
console.info("proxy receives message",event.data);
if(game) {
var replyId = event.data.replyId;
if(replyId) {
var reply = messageReplies[replyId];
if(reply) {
delete messageReplies[replyId];
reply(event.data.message);
}
} else
game.listeners.forEach((listener)=>{
listener.call(game,event.data.message);
});
}
}
function GameProxyClass(gameName) {
this.gameName = gameName;
this.listeners = [];
this.id = ++gameIdRef;
}
GameProxyClass.prototype.attachElement = function(element,options) {
var self = this;
var promise = new Promise((resolve,reject)=>{
if(!messageListenerInstalled) {
messageListenerInstalled = true;
window.addEventListener("message",MessageListener);
}
this.element = element;
var iframeUrl = "jocly.embed.html";
var iframe = document.createElement("iframe");
var attrs = {
name: "jocly-embedded-"+self.id,
frameborder: 0,
src: SystemJS.getConfig().baseURL+iframeUrl,
width: "100%",
height: "100%"
}
Object.keys(attrs).forEach((attr)=>{
iframe.setAttribute(attr,attrs[attr]);
});
Object.assign(iframe.style,{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
whiteSpace: "normal",
});
var wrapper = document.createElement("div");
Object.assign(wrapper.style,{
position: "relative",
whiteSpace: "nowrap",
width: "100%",
height: "100%"
});
wrapper.appendChild(iframe);
element.appendChild(wrapper);
this.iframe = iframe;
iframe.onload = ()=>{
PostMessage(self,{
type: "init",
id: this.id,
gameName: this.gameName
},(reply)=>{
resolve();
});
};
});
return promise;
}
function PostMessage(game,message,reply) {
var wrapper = {
message: message
}
if(reply) {
var replyId = ++msgIdRef;
wrapper.replyId = replyId;
messageReplies[replyId] = reply;
}
game.iframe.contentWindow.postMessage(wrapper,"*");
}
GameProxyClass.prototype.listen = function(listener) {
var self = this;
var promise = new Promise((resolve,reject)=>{
self.listeners.push(listener);
resolve();
});
return promise;
}
GameProxyClass.prototype.unlisten = function(listener) {
var self = this;
var promise = new Promise((resolve,reject)=>{
for(var i = self.listeners.length-1;i>=0;i--)
if(self.listeners[i]==listener)
self.listeners.splice(i,1);
resolve();
});
return promise;
}
GameProxyClass.prototype.humanTurn = function() {
var self = this;
var promise = new Promise((resolve,reject)=>{
PostMessage(self,{
type: "humanTurn"
});
resolve();
});
return promise;
}
GameProxyClass.prototype.machineTurn = function(options) {
var self = this;
var promise = new Promise((resolve,reject)=>{
PostMessage(self,{
type: "machineTurn",
options: options
});
resolve();
});
return promise;
}
GameProxyClass.prototype.getFinished = function() {
var self = this;
var promise = new Promise((resolve,reject)=>{
PostMessage(self,{
type: "getFinished"
},(result) => {
resolve(result);
});
});
return promise;
}
GameProxyClass.prototype.getMoveString = function(move) {
var self = this;
var promise = new Promise((resolve,reject)=>{
PostMessage(self,{
type: "getMoveString",
move: move
},(move) => {
resolve(move);
});
});
return promise;
}
function CreateGame(gameName) {
var promise = new Promise((resolve,reject)=>{
function CreateGame(config) {
var game = new GameProxyClass(gameName);
games[game.id] = game;
Object.assign(game,config);
resolve(game);
}
var config = gameConfigs[gameName];
if(config)
CreateGame(config);
else {
Jocly.listGames()
.then((games)=>{
var gameDescr = games[gameName];
if (!gameDescr)
return reject(new Error("Game " + gameName + " not found"));
SystemJS.import("games/" + gameDescr.module + "/" + gameName + "-config.js")
.then((config)=>{
gameConfigs[gameName] = config;
CreateGame(config);
});
},reject);
}
});
return promise;
}
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope)
this.GameProxy = {
createGame: CreateGame
}
else
exports.createGame = CreateGame;

View file

@ -27,193 +27,811 @@
"use strict";
if (typeof WorkerGlobalScope == 'undefined' || !(self instanceof WorkerGlobalScope)) {
var GameProxy = require("./jocly.proxy.js");
}
/*
* Keys to understand this code
*
* This script can be executed under various contexts:
* A. in the browser within the application context
* B. in the browser within an iframe controlled by the application
* C. in the browser as a worker for running the AI
* D. in node.js
*
* In case A, the Match object can either be:
* - attached to an element (A1)
* - not attached (A2)
*
* A Match object is only glue. The actual object doing the job is generated
* from function CreateInternalGame(). This object is constructed by defining
* a base (from jocly.game.js) overloaded by the model (<game-name>_model.js),
* and possibly by the view (<game-name>_view.js) if there is a need for a UI
* (case B only).
*
* In cases A2, B, C and D, the Match object contains 'game', the actual smart object
* In case A1, it contains property 'iframe' which is an actual iframe running
* the code as case B. Properties 'game' and 'iframe' should never be set at the
* same time.
*
* Match.attachElement() moves from case A2 to A1, creating an iframe running in
* in case B and passing the responsability of the actual game object to it.
* Match.detachElement() (A1 to A2) gets the game back from the iframe and destroys
* the iframe.
*
* In case B (and only in this case), the Match object has a property 'area'
* representing the DOM element containing the UI.
*
* By convention, Match object method names starts with a lowercase character,
* the game object with uppercase. Both APIs may have some similarities but
* are actually different.
*
* Most Match API methods do the following: check whether we are in case A1,
* if so, the command/reply is passed/answered to/from the iframe running case B.
* In all other cases, the action is performed locally using the API of the game
* object.
*
*/
exports.listGames = function () {
(function() {
var promise = new Promise((resolve, reject) => {
if(typeof SystemJS!="undefined")
SystemJS.import("jocly-allgames.js").then((m) => {
if(typeof SystemJS!="undefined") {
var baseURL = SystemJS.getConfig().baseURL;
for(var gameName in m.games) {
var game = m.games[gameName];
game.thumbnail = baseURL + "games/" + game.module + "/" + game.thumbnail;
}
}
resolve(m.games);
}, (e) => {
console.warn("Could not load Jocly games", e);
reject(e);
var gameClassCache = {};
function CreateGame(gameName, gameDescr,base, model, config) {
var Game = function () {
this.g = {}; // some games assume this member exists
};
Object.assign(Game.prototype, base.Game.prototype, model.model.Game);
var Board = function () { };
Object.assign(Board.prototype, base.Board.prototype, model.model.Board);
Game.prototype.mBoardClass = Board;
var Move = function (args) { this.Init(args); };
Object.assign(Move.prototype, base.Move.prototype, model.model.Move);
Game.prototype.mMoveClass = Move;
Game.prototype.config = config.config;
if(typeof SystemJS!="undefined") {
Game.prototype.config.baseURL = SystemJS.getConfig().baseURL;
Game.prototype.config.view.fullPath = SystemJS.getConfig().baseURL + "games/" + gameDescr.module;
}
Game.prototype.module = gameDescr.module;
Game.prototype.name = gameName;
var initArgs = {
game: config.config.model.gameOptions,
view: config.config.view
};
gameClassCache[gameName] = {
gameClass: Game,
initArgs: initArgs
}
var game = new Game();
game.Init(initArgs);
return game;
}
function CreateInternalGame(gameName, options) {
var gameClassData = gameClassCache[gameName];
if(gameClassData) {
var promise = new Promise((resolve,reject)=>{
var game = new gameClassData.gameClass();
game.Init(gameClassData.initArgs);
resolve(game);
});
return promise;
}
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope)
return WorkerCreateGame(gameName, options);
else if(typeof process!=="undefined" && process.title === "node")
return NodeCreateGame(gameName,options);
else
resolve(require("./jocly-allgames.js").games);
});
return promise;
}
var gameClassCache = {};
function CreateGame(gameName, gameDescr,base, model, config) {
var Game = function () {
this.g = {}; // some games assume this member exists
};
Object.assign(Game.prototype, base.Game.prototype, model.model.Game);
var Board = function () { };
Object.assign(Board.prototype, base.Board.prototype, model.model.Board);
Game.prototype.mBoardClass = Board;
var Move = function (args) { this.Init(args); };
Object.assign(Move.prototype, base.Move.prototype, model.model.Move);
Game.prototype.mMoveClass = Move;
Game.prototype.config = config.config;
if(typeof SystemJS!="undefined") {
Game.prototype.config.baseURL = SystemJS.getConfig().baseURL;
Game.prototype.config.view.fullPath = SystemJS.getConfig().baseURL + "games/" + gameDescr.module;
}
Game.prototype.module = gameDescr.module;
Game.prototype.name = gameName;
var initArgs = {
game: config.config.model.gameOptions,
view: config.config.view
};
gameClassCache[gameName] = {
gameClass: Game,
initArgs: initArgs
return BrowserCreateGame(gameName, options);
}
var game = new Game();
function NodeCreateGame(gameName, options) {
var t0 = Date.now();
options = options || {};
var promise = new Promise((resolve, reject) => {
game.Init(initArgs);
return game;
}
exports.createInternalGame = function (gameName, options) {
var gameClassData = gameClassCache[gameName];
if(gameClassData) {
var promise = new Promise((resolve,reject)=>{
var game = new gameClassData.gameClass();
game.Init(gameClassData.initArgs);
resolve(game);
});
return promise;
}
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope)
return WorkerCreateGame(gameName, options);
else if(typeof process!=="undefined" && process.title === "node")
return NodeCreateGame(gameName,options);
else
return BrowserCreateGame(gameName, options);
}
function NodeCreateGame(gameName, options) {
var t0 = Date.now();
options = options || {};
var promise = new Promise((resolve, reject) => {
var games = require("./jocly-allgames.js").games;
var gameDescr = games[gameName];
if (!gameDescr)
return reject(new Error("Game " + gameName + " not found"));
var base = require("./jocly.game.js");
var model = require("./games/" + gameDescr.module + "/" + gameName + "-model.js");
var config = require("./games/" + gameDescr.module + "/" + gameName + "-config.js");
var game = CreateGame(gameName, gameDescr, base, model, config);
resolve(game);
});
return promise;
}
function WorkerCreateGame(gameName, options) {
var t0 = Date.now();
options = options || {};
var promise = new Promise((resolve, reject) => {
importScripts("jocly-allgames.js");
var games = exports.games;
var gameDescr = games[gameName];
if (!gameDescr)
return reject(new Error("Game " + gameName + " not found"));
var originalExports = exports;
var base = exports = {};
importScripts("jocly.game.js");
var model = exports = {};
importScripts("games/" + gameDescr.module + "/" + gameName + "-model.js");
var config = exports = {};
importScripts("games/" + gameDescr.module + "/" + gameName + "-config.js");
exports = originalExports;
var game = CreateGame(gameName, gameDescr, base, model, config);
resolve(game);
});
return promise;
}
function BrowserCreateGame(gameName, options) {
options = options || {};
var promise = new Promise((resolve, reject) => {
exports.listGames().then((games) => {
var games = require("./jocly-allgames.js").games;
var gameDescr = games[gameName];
if (!gameDescr)
return reject(new Error("Game " + gameName + " not found"));
var gamePromises = [
SystemJS.import("jocly.game.js"),
SystemJS.import("games/" + gameDescr.module + "/" + gameName + "-model.js"),
SystemJS.import("games/" + gameDescr.module + "/" + gameName + "-config.js")
];
var base = require("./jocly.game.js");
var model = require("./games/" + gameDescr.module + "/" + gameName + "-model.js");
var config = require("./games/" + gameDescr.module + "/" + gameName + "-config.js");
Promise.all(gamePromises).then(([base, model, config]) => {
var game = CreateGame(gameName, gameDescr, base, model, config);
var game = CreateGame(gameName, gameDescr, base, model, config);
resolve(game);
});
return promise;
}
resolve(game);
}, (e) => {
reject(e);
});
}, reject);
function WorkerCreateGame(gameName, options) {
var t0 = Date.now();
options = options || {};
var promise = new Promise((resolve, reject) => {
});
return promise;
}
importScripts("jocly-allgames.js");
var games = exports.games;
exports.createGame = function (gameName) {
var options, element;
if(typeof Element !== "undefined" && arguments[1] instanceof Element) {
element = arguments[1];
options = arguments[2];
} else
options = arguments[1];
var gameDescr = games[gameName];
if (!gameDescr)
return reject(new Error("Game " + gameName + " not found"));
var originalExports = exports;
var base = exports = {};
importScripts("jocly.game.js");
var model = exports = {};
importScripts("games/" + gameDescr.module + "/" + gameName + "-model.js");
var config = exports = {};
importScripts("games/" + gameDescr.module + "/" + gameName + "-config.js");
exports = originalExports;
var game = CreateGame(gameName, gameDescr, base, model, config);
resolve(game);
});
return promise;
}
function BrowserCreateGame(gameName, options) {
options = options || {};
var promise = new Promise((resolve, reject) => {
exports.listGames().then((games) => {
var gameDescr = games[gameName];
if (!gameDescr)
return reject(new Error("Game " + gameName + " not found"));
var gamePromises = [
SystemJS.import("jocly.game.js"),
SystemJS.import("games/" + gameDescr.module + "/" + gameName + "-model.js"),
SystemJS.import("games/" + gameDescr.module + "/" + gameName + "-config.js")
];
Promise.all(gamePromises).then(([base, model, config]) => {
var game = CreateGame(gameName, gameDescr, base, model, config);
var promise = new Promise((resolve,reject)=>{
GameProxy.createGame(gameName,options)
.then((game)=>{
if(element) {
game.attachElement(element,options)
.then(()=>{
resolve(game);
},reject);
} else
resolve(game);
},reject);
});
return promise;
}
}, (e) => {
reject(e);
});
}, reject);
});
return promise;
}
// Check in what context this code is running
var jsContext = "browser";
if(typeof process!=="undefined" && process.title === "node")
jsContext = "node";
else if(typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope)
jsContext = "worker";
exports.listGames = function () {
var promise = new Promise((resolve, reject) => {
if(typeof SystemJS!="undefined")
SystemJS.import("jocly-allgames.js").then((m) => {
if(typeof SystemJS!="undefined") {
var baseURL = SystemJS.getConfig().baseURL;
for(var gameName in m.games) {
var game = m.games[gameName];
game.thumbnail = baseURL + "games/" + game.module + "/" + game.thumbnail;
}
}
resolve(m.games);
}, (e) => {
console.warn("Could not load Jocly games", e);
reject(e);
});
else
resolve(require("./jocly-allgames.js").games);
});
return promise;
}
function CreateMatch(gameName) {
var promise = new Promise( function(resolve, reject) {
CreateInternalGame(gameName)
.then( function(game) {
var proxy = new GameProxy(gameName,game);
resolve(proxy);
}, function(error) {
reject(error);
});
});
return promise;
}
var messageListenerInstalled = false;
var matches = {};
var msgIdRef = 0;
var messageReplies = {};
var matchIdRef = 0;
function GameProxy(gameName,game) {
this.gameName = gameName;
this.game = game;
this.iframe = null;
this.id = ++matchIdRef;
matches[this.id] = this;
}
function MessageListener(event) {
var message = event.data;
var match = matches[message.joclyEmbeddedGameId];
//console.info("proxy receives message",event.data,!!match);
if(match) {
var replyId = message.replyId;
if(replyId) {
var reply = messageReplies[replyId];
if(reply) {
delete messageReplies[replyId];
if(message.message.type=="error") {
var error = new Error(message.message.error.message,message.message.error.fileName,message.message.error.lineNumber);
reply(error);
} else
reply(null,message.message);
}
} else if(message.message.type=="machine-progress") {
if(match.machineProgressListener)
match.machineProgressListener(message.message.progress);
}
}
}
function PostMessage(match,message,reply) {
var wrapper = {
message: message
}
if(reply) {
var replyId = ++msgIdRef;
wrapper.replyId = replyId;
messageReplies[replyId] = reply;
}
try {
match.iframe.contentWindow.postMessage(wrapper,"*");
} catch(e) {
console.error("Message",message,"could not be posted:",e);
}
}
GameProxy.prototype.attachElement = function(element, options) {
if(jsContext=="node")
return Promise.reject(new Error("attachElement(): not supported in node.js"));
if(!this.game || this.iframe)
return Promise.reject(new Error("attachElement(): match is already attached"));
options = options || {};
var self = this;
var promise = new Promise( function(resolve, reject) {
if(!messageListenerInstalled) {
messageListenerInstalled = true;
window.addEventListener("message",MessageListener);
}
self.element = element;
var iframeUrl = "jocly.embed.html";
var iframe = document.createElement("iframe");
var attrs = {
name: "jocly-embedded-"+self.id,
frameborder: 0,
src: SystemJS.getConfig().baseURL+iframeUrl,
width: "100%",
height: "100%"
}
Object.keys(attrs).forEach((attr)=>{
iframe.setAttribute(attr,attrs[attr]);
});
Object.assign(iframe.style,{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
whiteSpace: "normal",
});
var wrapper = document.createElement("div");
Object.assign(wrapper.style,{
position: "relative",
whiteSpace: "nowrap",
width: "100%",
height: "100%"
});
wrapper.appendChild(iframe);
element.appendChild(wrapper);
self.iframe = iframe;
iframe.onload = ()=>{
PostMessage(self,{
type: "init",
id: self.id,
gameName: self.gameName,
playedMoves: self.game.mPlayedMoves
},(error,reply)=>{
if(error)
reject(error);
else {
self.game = null;
resolve();
}
});
};
});
return promise;
}
GameProxy.prototype.detachElement = function() {
if(jsContext=="node")
return Promise.reject(new Error("detachElement(): not supported in node.js"));
if(this.game || !this.iframe)
return Promise.reject(new Error("detachElement(): match is not attached"));
var self = this;
var promise = new Promise( function(resolve, reject) {
Promise.all([
CreateInternalGame(self.gameName),
ProxiedMethod(self,"getPlayedMoves")
])
.then( ([game,moves]) => {
self.game = game;
game.Load({
playedMoves: moves
});
return ProxiedMethod(self,"destroy");
})
.then( () => {
while(self.element.firstChild)
self.element.removeChild(self.element.firstChild);
delete self.element;
delete self.iframe;
resolve();
})
.catch( reject );
});
return promise;
}
function ProxiedMethod(match,methodName,args) {
args = Array.from(args || []);
var promise = new Promise( function(resolve, reject) {
PostMessage(match,{
type: "method",
id: match.id,
methodName: methodName,
args: args,
},(error,reply)=>{
if(error)
reject(error);
else
resolve.apply(null,reply && reply.args || []);
});
});
return promise;
}
GameProxy.prototype.getTurn = function() {
if(this.game) {
var self = this;
var promise = new Promise( function(resolve, reject) {
var who = self.game.GetWho();
resolve(who);
});
return promise;
} else
return ProxiedMethod(this,"getTurn",arguments);
}
GameProxy.prototype.getMoveString = function(move) {
if(this.game) {
var self = this;
var promise = new Promise( function(resolve, reject) {
if(Array.isArray(move))
resolve(
move.map( (m) => {
return self.game.CreateMove(m).ToString();
})
)
else
resolve(self.game.CreateMove(move).ToString());
});
return promise;
} else
return ProxiedMethod(this,"getMoveString",arguments);
}
GameProxy.prototype.pickMove = function(moveString) {
if(this.game) {
var self = this;
var promise = new Promise( function(resolve, reject) {
if(!self.game.mBoard.mMoves || self.game.mBoard.mMoves.length==0)
self.game.mBoard.GenerateMoves(self.game);
var move = self.game.GetBestMatchingMove(moveString,self.game.mBoard.mMoves);
resolve(move);
});
return promise;
} else
return ProxiedMethod(this,"pickMove",arguments);
}
GameProxy.prototype.playMove = function(move) {
if(jsContext=="node" || (!this.iframe && !this.area))
return this.applyMove(move);
if(this.game) {
var self = this;
var promise = new Promise( function(resolve, reject) {
self.game.PlayMove(move)
.then(()=>{
var finished = self.game.GetFinished();
if(!finished)
self.game.InvertWho();
self.game.DisplayBoard();
resolve({
finished: !!finished,
winner: finished
});
})
.catch( (err) => {
reject(err);
});
});
return promise;
} else
return ProxiedMethod(this,"playMove",arguments);
}
GameProxy.prototype.applyMove = function(move) {
if(this.game) {
var self = this;
var promise = new Promise( function(resolve, reject) {
self.game.ApplyMove(move);
var finished = self.game.GetFinished();
if(!finished)
self.game.InvertWho();
if(self.area)
self.game.DisplayBoard();
resolve({
finished: !!finished,
winner: finished
});
});
return promise;
} else
return ProxiedMethod(this,"applyMove",arguments);
}
GameProxy.prototype.destroy = function(move) {
var self = this;
if(this.game) {
var promise = new Promise( function(resolve, reject) {
if(self.area)
self.game.GameDestroyView();
resolve();
});
return promise;
} else
return this.detachElement()
.then( () => {
delete matches[self.id];
})
}
GameProxy.prototype.getPlayedMoves = function() {
if(this.game) {
var self = this;
var promise = new Promise( function(resolve, reject) {
resolve(Array.from(self.game.mPlayedMoves));
});
return promise;
} else
return ProxiedMethod(this,"getPlayedMoves");
}
GameProxy.prototype.userTurn = function() {
if(jsContext=="node")
return Promise.reject(new Error("userTurn(): not supported in node.js"));
if(!this.area && !this.iframe)
return Promise.reject(new Error("userTurn(): match is not attached to DOM element"));
if(this.game) {
var self = this;
if(this.userTurnReject)
return Promise.reject(new Error("userTurn(): already in user input mode"));
var savedHumanMove;
function Restore() {
self.game.HumanMove = savedHumanMove;
delete self.userTurnReject;
}
var promise = new Promise( function(resolve, reject) {
self.userTurnReject = reject;
function HumanMove(move) {
self.game.ApplyMove(move);
var finished = self.game.GetFinished();
if(!finished)
self.game.InvertWho();
self.game.DisplayBoard();
resolve({
move: move,
finished: !!finished,
winner: finished
});
}
savedHumanMove = self.game.HumanMove;
self.game.HumanMove = HumanMove;
self.game.HumanTurn();
});
return promise
.then( (result) => {
Restore();
return result;
})
.catch( (err) => {
Restore();
throw err;
})
} else
return ProxiedMethod(this,"userTurn");
}
GameProxy.prototype.abortUserTurn = function() {
if(jsContext=="node")
return Promise.reject(new Error("abortUserTurn(): not supported in node.js"));
if(!this.area && !this.iframe)
return Promise.reject(new Error("abortUserTurn(): match is not attached to DOM element"));
if(this.game) {
var self = this;
if(!this.userTurnReject)
return Promise.reject(new Error("abortUserTurn(): not in user input mode"));
var promise = new Promise( function(resolve, reject) {
self.game.HumanTurnEnd();
var userTurnReject = self.userTurnReject;
delete self.userTurnReject;
userTurnReject(new Error("User input aborted"));
resolve();
});
return promise;
} else
return ProxiedMethod(this,"abortUserTurn");
}
GameProxy.prototype.machineSearch = function(options) {
var self = this;
if(this.game) {
options = Object.assign({
level: self.game.config.model.levels[0],
threaded: true
},options);
var savedMachineMove, savedMachineProgress;
function Restore() {
self.game.MachineMove = savedMachineMove;
self.game.MachineProgress = savedMachineProgress;
delete self.machineSearchReject;
}
var promise = new Promise( function(resolve, reject) {
self.machineSearchReject = reject;
function MachineMove(result) {
var move = result.move;
delete result.move;
var finished = self.game.GetFinished();
resolve({
move: move,
finished: !!finished,
winner: finished
});
}
savedMachineMove = self.game.MachineMove;
self.game.MachineMove = MachineMove;
function MachineProgress(percent) {
if(self.area)
window.parent.postMessage({
joclyEmbeddedGameId: self.id,
message: {
type: "machine-progress",
progress: percent
}
},"*");
}
savedMachineProgress = self.game.MachineProgress;
self.game.MachineProgress = MachineProgress;
self.game.StartMachine(options);
});
return promise
.then( (result) => {
Restore();
return result;
})
.catch( (err) => {
Restore();
throw err;
})
} else {
function Restore2() {
delete self.machineProgressListener;
}
options = options || {};
self.machineProgressListener = options.progress || function() {};
delete options.progress;
return ProxiedMethod(this,"machineSearch",[options])
.then( (result) => {
Restore2();
return result;
})
.catch( (err) => {
Restore2();
throw err;
})
}
}
GameProxy.prototype.abortMachineSearch = function() {
if(this.game) {
var self = this;
if(!this.machineSearchReject)
return Promise.reject(new Error("abortMachineSearch(): machine not searching"));
var promise = new Promise( function(resolve, reject) {
self.game.StopThreadedMachine();
var machineSearchReject = self.machineSearchReject;
delete self.machineSearchReject;
machineSearchReject(new Error("Machine search aborted"));
resolve();
});
return promise;
} else
return ProxiedMethod(this,"abortMachineSearch");
}
GameProxy.prototype.setViewOptions = function(options) {
if(jsContext=="node")
return Promise.reject(new Error("setViewOptions(): not supported in node.js"));
if(!this.area && !this.iframe)
return Promise.reject(new Error("setViewOptions(): match is not attached to DOM element"));
if(this.game) {
var self = this;
var promise = new Promise( function(resolve, reject) {
self.game.GameDestroyView();
const optDefs = {
"mSkin": "skin",
"mNotation": "notation",
"mSounds": "sounds",
"mShowMoves": "showMoves",
"mAutoComplete": "autoComplete"
}
for(var o in optDefs)
if(typeof options[optDefs[o]]!="undefined")
self.game[o] = options[optDefs[o]];
self.game.GameInitView();
resolve();
});
return promise;
} else
return ProxiedMethod(this,"setViewOptions",arguments);
}
GameProxy.prototype.getFinished = function() {
if(this.game) {
var self = this;
var promise = new Promise( function(resolve, reject) {
var finished = self.game.GetFinished();
resolve({
finished: !!finished,
winner: finished
});
});
return promise;
} else
return ProxiedMethod(this,"getFinished");
}
GameProxy.prototype.rollback = function(index) {
if(this.game) {
var self = this;
var promise = new Promise( function(resolve, reject) {
if(index<0)
index = self.game.mPlayedMoves.length + index;
index = Math.max(0,Math.min(index,self.game.mPlayedMoves.length));
var playedMoves = self.game.mPlayedMoves;
self.game.BackTo(index,playedMoves);
if(self.area)
self.game.DisplayBoard();
resolve();
});
return promise;
} else
return ProxiedMethod(this,"rollback",arguments);
}
GameProxy.prototype.otherPlayer = function(player) {
var promise = new Promise( function(resolve, reject) {
if(player==1)
resolve(-1);
else if(player==-1)
resolve(1);
else
reject(new Error("otherPlayer: invalid input"));
});
return promise;
}
GameProxy.prototype.save = function() {
if(this.game) {
var self = this;
var promise = new Promise( function(resolve, reject) {
resolve({
playedMoves: Array.from(self.game.mPlayedMoves)
})
});
return promise;
} else
return ProxiedMethod(this,"save");
}
GameProxy.prototype.load = function(data) {
if(this.game) {
var self = this;
var promise = new Promise( function(resolve, reject) {
self.game.Load(data);
if(self.area)
self.game.DisplayBoard();
resolve();
});
return promise;
} else
return ProxiedMethod(this,"load",arguments);
}
exports.createMatch = CreateMatch;
exports._createInternalGame = CreateInternalGame; // do not use this
exports.PLAYER_A = 1;
exports.PLAYER_B = -1;
exports.DRAW = 2;
})();

View file

@ -1,162 +0,0 @@
/* 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.
*/
var joclyGame = require("./jocly.game");
var joclyCore = require("./jocly.core");
var gameConfigs = {}
function GameProxyClass(gameName) {
this.gameName = gameName;
this.listeners = [];
}
GameProxyClass.prototype.listen = function(listener) {
var self = this;
var promise = new Promise((resolve,reject)=>{
self.listeners.push(listener);
resolve();
});
return promise;
}
GameProxyClass.prototype.unlisten = function(listener) {
var self = this;
var promise = new Promise((resolve,reject)=>{
for(var i = self.listeners.length-1;i>=0;i--)
if(self.listeners[i]==listener)
self.listeners.splice(i,1);
resolve();
});
return promise;
}
GameProxyClass.prototype.humanTurn = function() {
return this.game.humanTurn();
}
GameProxyClass.prototype.machineTurn = function(options) {
var self = this;
var promise = new Promise((resolve,reject)=>{
self.game.StartMachine(options);
resolve();
});
return promise;
}
GameProxyClass.prototype.getFinished = function() {
var self = this;
var promise = new Promise((resolve,reject)=>{
var finished = self.gameName.GetFinished();
resolve(finished);
});
return promise;
}
GameProxyClass.prototype.getMoveString = function(move) {
var self = this;
var promise = new Promise((resolve,reject)=>{
var m = new (self.game.GetMoveClass())(move);
resolve(m.ToString());
});
return promise;
}
function Post(game,message) {
game.listeners.forEach((listener)=>{
listener.call(game,message);
});
}
function GetListener(game) {
return function(message) {
var _game = this;
switch(message.type) {
case "human-move":
_game.ApplyMove(message.move);
var finished = _game.GetFinished();
if(!finished)
_game.InvertWho();
Object.assign(message,{
finished: !!finished,
winner: finished
});
break;
case "machine-move":
var move = message.result.move;
delete message.result.move;
_game.ApplyMove(move);
var finished = _game.GetFinished();
if(!finished)
_game.InvertWho();
Post(game,{
type: "machine-move",
moveData: message.result,
move: move,
finished: !!finished,
winner: finished
});
return;
}
Post(game,message);
}
}
function CreateGame(gameName) {
var promise = new Promise((resolve,reject)=>{
function CreateGame(config) {
var game = new GameProxyClass(gameName);
Object.assign(game,config);
game.game = new joclyCore.createInternalGame(gameName)
.then((_game)=>{
_game.AddListener(GetListener(game));
game.game = _game;
resolve(game);
},reject);
}
var config = gameConfigs[gameName];
if(config)
CreateGame(config);
else {
joclyCore.listGames()
.then((games)=>{
var gameDescr = games[gameName];
if (!gameDescr)
return reject(new Error("Game " + gameName + " not found"));
var config = require("./games/" + gameDescr.module + "/" + gameName + "-config.js")
gameConfigs[gameName] = config;
CreateGame(config);
},reject);
}
});
return promise;
}
exports.createGame = CreateGame;