Import code from hanafuda-webapp and adapt it for SJW
This commit is contained in:
commit
f625b9954f
21 changed files with 1120 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
game.js
|
||||||
|
index.js
|
||||||
|
skin.css
|
17
Makefile
Normal file
17
Makefile
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
PACKAGES=unitJS
|
||||||
|
TARGETS=index.js game.js skin.css
|
||||||
|
|
||||||
|
.PHONY: mrproper
|
||||||
|
|
||||||
|
all: $(TARGETS)
|
||||||
|
|
||||||
|
%.js: js/
|
||||||
|
sjw -o $@ $(PACKAGES:%=-I %) -m Main.$(@:%.js=%) $^
|
||||||
|
|
||||||
|
skin.css: skin/
|
||||||
|
cat $^*.css > $@
|
||||||
|
|
||||||
|
rebuild: mrproper all
|
||||||
|
|
||||||
|
mrproper:
|
||||||
|
rm -f $(TARGETS)
|
39
game.html
Normal file
39
game.html
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>KoiKoi</title>
|
||||||
|
<script src="game.js"></script>
|
||||||
|
<link rel="stylesheet" href="skin.css" type="text/css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="game">
|
||||||
|
<div id="them">
|
||||||
|
<ul class="hand"></ul>
|
||||||
|
<ul class="kasu"></ul>
|
||||||
|
<ul class="tane"></ul>
|
||||||
|
<ul class="tan"></ul>
|
||||||
|
<ul class="hikari"></ul>
|
||||||
|
<span class="score"></span>
|
||||||
|
</div>
|
||||||
|
<div id="table">
|
||||||
|
<ul id="deck">
|
||||||
|
<li class="card init" id="rest"></li>
|
||||||
|
</ul>
|
||||||
|
<ul id="river"></ul>
|
||||||
|
<ul id="status"></ul>
|
||||||
|
</div>
|
||||||
|
<div id="you">
|
||||||
|
<ul class="hand"></ul>
|
||||||
|
<ul class="kasu"></ul>
|
||||||
|
<ul class="tane"></ul>
|
||||||
|
<ul class="tan"></ul>
|
||||||
|
<ul class="hikari"></ul>
|
||||||
|
<span class="score"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="dialog">
|
||||||
|
</div>
|
||||||
|
<p id="error"></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
41
index.html
Normal file
41
index.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>KoiKoi</title>
|
||||||
|
<script src="index.js"></script>
|
||||||
|
<link rel="stylesheet" href="skin.css" type="text/css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="reception" class="on">
|
||||||
|
<h1>KoiKoi</h1>
|
||||||
|
<form id="login">
|
||||||
|
<input type="submit" name="submitButton" hidden disabled/>
|
||||||
|
<p id="join">
|
||||||
|
<label for="you"></label><input type="text" name="you"/>
|
||||||
|
<input type="submit" name="join" disabled/>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="hall">
|
||||||
|
<form id="room">
|
||||||
|
<input type="submit" name="submitButton" hidden disabled/>
|
||||||
|
<p id="invite">
|
||||||
|
<label for="them"></label><input type="text" name="them"/>
|
||||||
|
<input type="submit" name="invite" disabled/>
|
||||||
|
</p>
|
||||||
|
<div class="listSelector" id="players">
|
||||||
|
<span class="message"></span>
|
||||||
|
<ul></ul>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="listSelector" id="games">
|
||||||
|
<span class="message"></span>
|
||||||
|
<ul></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="dialog">
|
||||||
|
</div>
|
||||||
|
<p id="error"></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
45
js/GUI/ConnectedForm.js
Normal file
45
js/GUI/ConnectedForm.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import Messaging;
|
||||||
|
|
||||||
|
var connectedForms = {};
|
||||||
|
|
||||||
|
Messaging.addEventListener('open', refreshForms);
|
||||||
|
Messaging.addEventListener('close', refreshForms);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: get
|
||||||
|
};
|
||||||
|
|
||||||
|
function ConnectedForm(root) {
|
||||||
|
var submits = root.querySelectorAll('[type=submit]');
|
||||||
|
var enabled = false
|
||||||
|
|
||||||
|
return {
|
||||||
|
enable: enable,
|
||||||
|
refresh: refresh,
|
||||||
|
root: root
|
||||||
|
}
|
||||||
|
|
||||||
|
function enable(setEnabled) {
|
||||||
|
enabled = setEnabled || undefined == setEnabled;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
submits.forEach(function(button) {
|
||||||
|
button.disabled = !(Messaging.isOn() && enabled);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(formId) {
|
||||||
|
if(connectedForms[formId] == undefined) {
|
||||||
|
connectedForms[formId] = ConnectedForm(document.getElementById(formId));
|
||||||
|
}
|
||||||
|
return connectedForms[formId];
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshForms() {
|
||||||
|
for(var key in connectedForms) {
|
||||||
|
connectedForms[key].refresh();
|
||||||
|
}
|
||||||
|
}
|
24
js/GUI/ListSelector.js
Normal file
24
js/GUI/ListSelector.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import * as Dom from UnitJS.Dom;
|
||||||
|
|
||||||
|
function ListSelector(domId, lineOfElement) {
|
||||||
|
var root = document.getElementById(domId);
|
||||||
|
var message = root.getElementsByClassName('message')[0];
|
||||||
|
var list = root.getElementsByTagName('ul')[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: message,
|
||||||
|
refresh: refresh
|
||||||
|
};
|
||||||
|
|
||||||
|
function refresh(sortedElements) {
|
||||||
|
Dom.clear(list);
|
||||||
|
message.textContent = '';
|
||||||
|
sortedElements.forEach(function(element) {
|
||||||
|
list.appendChild(lineOfElement(element));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
make: ListSelector
|
||||||
|
};
|
49
js/GUI/Screen.js
Normal file
49
js/GUI/Screen.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import * as Dom from UnitJS.Dom;
|
||||||
|
import I18n;
|
||||||
|
|
||||||
|
var current = document.querySelector("body > div.on");
|
||||||
|
var errorBox = document.getElementById('error');
|
||||||
|
errorBox.addEventListener('click', function() {
|
||||||
|
errorBox.className = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: error,
|
||||||
|
dialog: dialog,
|
||||||
|
select: select
|
||||||
|
};
|
||||||
|
|
||||||
|
function select(name) {
|
||||||
|
current.className = "";
|
||||||
|
current = document.getElementById(name);
|
||||||
|
current.className = "on";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAndRun(dialog, action) {
|
||||||
|
return function() {
|
||||||
|
dialog.className = '';
|
||||||
|
Dom.clear(dialog);
|
||||||
|
action();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function dialog(config) {
|
||||||
|
var layer = document.getElementById('dialog');
|
||||||
|
var dialog = Dom.make('div', {});
|
||||||
|
dialog.appendChild(Dom.make('p', {textContent: config.text}));
|
||||||
|
var answers = Dom.make('p', {class: 'answers'});
|
||||||
|
for(var i in config.answers) {
|
||||||
|
answers.appendChild(Dom.make('button', {
|
||||||
|
textContent: I18n.get(config.answers[i].label),
|
||||||
|
onClick: closeAndRun(layer, config.answers[i].action)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
dialog.appendChild(answers);
|
||||||
|
layer.appendChild(dialog);
|
||||||
|
layer.className = "on";
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(message) {
|
||||||
|
errorBox.textContent = message;
|
||||||
|
errorBox.className = "on";
|
||||||
|
}
|
140
js/GUI/Screen/Hall.js
Normal file
140
js/GUI/Screen/Hall.js
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import * as Dom from UnitJS.Dom;
|
||||||
|
import I18n;
|
||||||
|
import * as ConnectedForm from GUI.ConnectedForm;
|
||||||
|
import * as ListSelector from GUI.ListSelector;
|
||||||
|
import Messaging;
|
||||||
|
import room as players from Room;
|
||||||
|
import Session;
|
||||||
|
import StatusHandler;
|
||||||
|
import Table;
|
||||||
|
|
||||||
|
var room = ConnectedForm.get('room');
|
||||||
|
var form = room.root;
|
||||||
|
|
||||||
|
var playersList = ListSelector.make('players', showPlayer);
|
||||||
|
var games = Table.make(game, 'date');
|
||||||
|
var gamesList = ListSelector.make('games', showGame);
|
||||||
|
var them = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init
|
||||||
|
};
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
initDOMEvents();
|
||||||
|
initMessageHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initDOMEvents() {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Messaging.send({tag: "Invitation", to: them});
|
||||||
|
});
|
||||||
|
form.them.addEventListener("input", function() {refreshPlayers();});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMessageHandlers() {
|
||||||
|
Messaging.addEventListener(["Okaeri"], function(o) {
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
Messaging.addEventListener(["Welcome"], function(o) {
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
Messaging.addEventListener(["LogIn"], function(o) {
|
||||||
|
if(!Session.is(o.from)) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Messaging.addEventListener(["LogOut"], function(o) {
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
Messaging.addEventListener(["Relay", "Invitation"], function(o) {
|
||||||
|
var from = players.get(o.from);
|
||||||
|
// invitations should come only from known players, in doubt say «no»
|
||||||
|
if(from != undefined && from.name) {
|
||||||
|
StatusHandler.set("🎴");
|
||||||
|
games.insert(o.from, from.name);
|
||||||
|
refreshGames();
|
||||||
|
} else {
|
||||||
|
Messaging.send({tag: "Answer", accept: false});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Messaging.addEventListener(["Relay", "Answer"], function(o) {
|
||||||
|
games.remove(o.from);
|
||||||
|
refreshGames();
|
||||||
|
/*
|
||||||
|
if(o.message.accept) {
|
||||||
|
modules.screen.select("game");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
Messaging.addEventListener(["Game"], function(o) {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPlayer(player) {
|
||||||
|
return Dom.make('li', {
|
||||||
|
textContent: player.name,
|
||||||
|
onClick: function() {form.them.value = player.name; refreshPlayers();},
|
||||||
|
class: 'player'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function game(key, vs) {
|
||||||
|
return {
|
||||||
|
key: key,
|
||||||
|
vs: vs,
|
||||||
|
date: Date.now()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function showGame(game) {
|
||||||
|
return Dom.make('li', {}, [
|
||||||
|
Dom.make('button', {
|
||||||
|
textContent: I18n.get('accept'),
|
||||||
|
onClick: function() {
|
||||||
|
Messaging.send({tag: "Answer", accept: true, to: game.key});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Dom.make('span', {textContent: 'A game vs. ' + game.vs}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
refreshPlayers();
|
||||||
|
refreshGames();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshPlayers() {
|
||||||
|
var name = form.them.value;
|
||||||
|
them = null;
|
||||||
|
var filtered = players.getAll(
|
||||||
|
function(player) {return player.name.match(name);}
|
||||||
|
);
|
||||||
|
playersList.refresh(filtered);
|
||||||
|
var exact = filtered.find(exactMatch(name));
|
||||||
|
if(exact != undefined) {
|
||||||
|
them = exact.key;
|
||||||
|
} else if(filtered.length == 1) {
|
||||||
|
them = filtered[0].key;
|
||||||
|
} else if(filtered.length == 0) {
|
||||||
|
playersList.message.textContent = I18n.get(
|
||||||
|
name.length > 0 ? "notFound" : "alone"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
room.enable(them != undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exactMatch(name) {
|
||||||
|
return function(player) {
|
||||||
|
return player.name === name;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshGames() {
|
||||||
|
var sortedGames = games.getAll();
|
||||||
|
gamesList.refresh(sortedGames);
|
||||||
|
if(sortedGames.length < 1) {
|
||||||
|
gamesList.message.textContent = I18n.get('noGames');
|
||||||
|
}
|
||||||
|
}
|
45
js/GUI/Screen/Login.js
Normal file
45
js/GUI/Screen/Login.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import I18n;
|
||||||
|
import GUI.ConnectedForm;
|
||||||
|
import select from GUI.Screen;
|
||||||
|
import Messaging;
|
||||||
|
import Session;
|
||||||
|
import Save;
|
||||||
|
|
||||||
|
var login = GUI.ConnectedForm.get('login');
|
||||||
|
var form = login.root;
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init
|
||||||
|
};
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
initDOM();
|
||||||
|
initMessageHandlers();
|
||||||
|
var name = Save.get('player.name');
|
||||||
|
if(name != undefined && name.length > 0) {
|
||||||
|
form.you.value = name;
|
||||||
|
login.enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initDOM() {
|
||||||
|
form.getElementsByTagName('label')[0].textContent = I18n.get('pickName');
|
||||||
|
form.join.value = I18n.get('join');
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Session.start(form.you.value);
|
||||||
|
});
|
||||||
|
form.you.addEventListener("input", validate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMessageHandlers() {
|
||||||
|
Messaging.addEventListener(["LogIn"], function(o) {
|
||||||
|
if(Session.is(o.from)) {
|
||||||
|
select('hall');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(e) {
|
||||||
|
login.enable(e.target.value != "");
|
||||||
|
}
|
32
js/I18n.js
Normal file
32
js/I18n.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import Translations;
|
||||||
|
|
||||||
|
var language = chooseLanguage();
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: get
|
||||||
|
};
|
||||||
|
|
||||||
|
function chooseLanguage() {
|
||||||
|
var userPreference = navigator.language || navigator.userLanguage;
|
||||||
|
if(userPreference != undefined) {
|
||||||
|
if(Translations[userPreference] != undefined) {
|
||||||
|
return userPreference;
|
||||||
|
}
|
||||||
|
var lang = userPreference.replace(/-.*/, '');
|
||||||
|
for(var key in Translations) {
|
||||||
|
if(key.replace(/-.*/, '') == lang) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(Translations['en-US'] != undefined) {
|
||||||
|
return 'en-US';
|
||||||
|
}
|
||||||
|
for(var key in Translations) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(textId) {
|
||||||
|
return Translations[language][textId] || ('TRANSLATE "'+textId+'" !!');
|
||||||
|
}
|
127
js/Messaging.js
Normal file
127
js/Messaging.js
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import error as popError from GUI.Screen;
|
||||||
|
|
||||||
|
var wsLocation = window.location.origin.replace(/^http/, 'ws') + '/play/';
|
||||||
|
var ws;
|
||||||
|
var debug = getParameters().debug;
|
||||||
|
var doLog = debug != undefined && debug.match(/^(?:1|t(?:rue)?|v(?:rai)?)$/i);
|
||||||
|
var on = false;
|
||||||
|
var s = 1000; /* ms */
|
||||||
|
var keepAlivePeriod = 20;
|
||||||
|
var reconnectDelay = 1;
|
||||||
|
var routes = {callbacks: [], children: {}};
|
||||||
|
var wsHandlers = {
|
||||||
|
open: [function() {on = true; reconnectDelay = 1}, ping],
|
||||||
|
close: [function() {on = false;}, reconnect]
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
return {
|
||||||
|
addEventListener: addEventListener,
|
||||||
|
isOn: isOn,
|
||||||
|
send: send
|
||||||
|
};
|
||||||
|
|
||||||
|
function get(obj, path, write) {
|
||||||
|
write = write || false;
|
||||||
|
if(path.length < 1) {
|
||||||
|
return obj;
|
||||||
|
} else {
|
||||||
|
if(obj.children[path[0]] == undefined && write) {
|
||||||
|
obj.children[path[0]] = {callbacks: [], children: {}};
|
||||||
|
}
|
||||||
|
if(obj.children[path[0]] != undefined) {
|
||||||
|
return get(obj.children[path[0]], path.slice(1), write);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParameters() {
|
||||||
|
var o = {};
|
||||||
|
window.location.search.substr(1).split('&').forEach(function(s) {
|
||||||
|
var t = s.split('=');
|
||||||
|
o[t[0]] = t[1];
|
||||||
|
});
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEventListener(path, callback) {
|
||||||
|
if(Array.isArray(path)) {
|
||||||
|
var route = get(routes, path, true);
|
||||||
|
route.callbacks.push(callback);
|
||||||
|
} else {
|
||||||
|
if(wsHandlers[path] != undefined) {
|
||||||
|
wsHandlers[path].push(callback);
|
||||||
|
} else {
|
||||||
|
log('Unsupported websocket event "' + path + '"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function messageListener(event) {
|
||||||
|
var o = JSON.parse(event.data);
|
||||||
|
var path = [];
|
||||||
|
var tmp = o;
|
||||||
|
while(tmp != undefined && tmp.tag != undefined) {
|
||||||
|
path.push(tmp.tag);
|
||||||
|
tmp = tmp.message;
|
||||||
|
}
|
||||||
|
var route = get(routes, path);
|
||||||
|
if(route != undefined && route.callbacks != undefined) {
|
||||||
|
route.callbacks.forEach(function(f) {f(o);});
|
||||||
|
} else {
|
||||||
|
console.log("No route found for " + event.data);
|
||||||
|
}
|
||||||
|
o.direction = 'client < server';
|
||||||
|
log(o);
|
||||||
|
};
|
||||||
|
|
||||||
|
function send(o) {
|
||||||
|
ws.send(JSON.stringify(o));
|
||||||
|
o.direction = 'client > server';
|
||||||
|
log(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
if(doLog) {
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
connect();
|
||||||
|
addEventListener(["Pong"], ping);
|
||||||
|
addEventListener(["Error"], function(o) {popError(o.error);});
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
ws = new WebSocket(window.location.origin.replace(/^http/, 'ws') + '/play/');
|
||||||
|
ws.addEventListener('message', messageListener);
|
||||||
|
ws.addEventListener('open', function(e) {
|
||||||
|
wsHandlers.open.forEach(function(handler) {handler(e);});
|
||||||
|
});
|
||||||
|
ws.addEventListener('close', function(e) {
|
||||||
|
wsHandlers.close.forEach(function(handler) {handler(e);});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconnect() {
|
||||||
|
setTimeout(connect, reconnectDelay * s);
|
||||||
|
if(reconnectDelay < 16) {
|
||||||
|
reconnectDelay *= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOn() {
|
||||||
|
return on;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ping() {
|
||||||
|
setTimeout(function() {
|
||||||
|
if(isOn()) {
|
||||||
|
send({tag: "Ping"});
|
||||||
|
}
|
||||||
|
}, keepAlivePeriod * s);
|
||||||
|
}
|
34
js/Room.js
Normal file
34
js/Room.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import Messaging;
|
||||||
|
import Session;
|
||||||
|
import Table;
|
||||||
|
|
||||||
|
var room = Table.make(player, 'name');
|
||||||
|
initMessageHandlers();
|
||||||
|
|
||||||
|
return {
|
||||||
|
room: room
|
||||||
|
};
|
||||||
|
|
||||||
|
function player(key, name) {
|
||||||
|
return {
|
||||||
|
key: key,
|
||||||
|
name: name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMessageHandlers() {
|
||||||
|
Messaging.addEventListener(["Okaeri"], function(o) {
|
||||||
|
room.insertAll(o.room);
|
||||||
|
});
|
||||||
|
Messaging.addEventListener(["Welcome"], function(o) {
|
||||||
|
room.insertAll(o.room);
|
||||||
|
});
|
||||||
|
Messaging.addEventListener(["LogIn"], function(o) {
|
||||||
|
if(!Session.is(o.from)) {
|
||||||
|
room.insert(o.from, o.as);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Messaging.addEventListener(["LogOut"], function(o) {
|
||||||
|
room.remove(o.from);
|
||||||
|
});
|
||||||
|
}
|
46
js/Save.js
Normal file
46
js/Save.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
var save = JSON.parse(localStorage.getItem('save')) || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: get,
|
||||||
|
set: set
|
||||||
|
};
|
||||||
|
|
||||||
|
function move(coordinates) {
|
||||||
|
if(coordinates.path.length == 1) {
|
||||||
|
return coordinates;
|
||||||
|
} else {
|
||||||
|
var newFocus = coordinates.focus[coordinates.path[0]];
|
||||||
|
if (newFocus != undefined) {
|
||||||
|
var newCoordinates = {path: coordinates.path.slice(1), focus: newFocus};
|
||||||
|
return move(newCoordinates);
|
||||||
|
} else {
|
||||||
|
return coordinates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(key) {
|
||||||
|
if(key != undefined) {
|
||||||
|
var outputCoordinates = move({path: key.split('.'), focus: save});
|
||||||
|
if(outputCoordinates.focus != undefined && outputCoordinates.path.length == 1) {
|
||||||
|
return outputCoordinates.focus[outputCoordinates.path[0]]
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(key, value) {
|
||||||
|
if(key != undefined) {
|
||||||
|
var outputCoordinates = move({path: key.split('.'), focus: save});
|
||||||
|
while(outputCoordinates.path.length > 1) {
|
||||||
|
outputCoordinates.focus[outputCoordinates.path[0]] = {};
|
||||||
|
outputCoordinates.focus = outputCoordinates.focus[outputCoordinates.path[0]];
|
||||||
|
outputCoordinates.path = outputCoordinates.path.slice(1);
|
||||||
|
}
|
||||||
|
outputCoordinates.focus[outputCoordinates.path[0]] = value;
|
||||||
|
} else {
|
||||||
|
save = value;
|
||||||
|
}
|
||||||
|
localStorage.setItem('save', JSON.stringify(save));
|
||||||
|
}
|
49
js/Session.js
Normal file
49
js/Session.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import Messaging;
|
||||||
|
import Save;
|
||||||
|
|
||||||
|
var key = null;
|
||||||
|
var playerKey = null;
|
||||||
|
var name = null;
|
||||||
|
var loggedIn = false;
|
||||||
|
|
||||||
|
Messaging.addEventListener(["Welcome"], function(o) {
|
||||||
|
playerKey = o.key;
|
||||||
|
Save.set('player.id', o.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
Messaging.addEventListener(["LogIn"], function(o) {
|
||||||
|
if(is(o.from)) {
|
||||||
|
name = o.as;
|
||||||
|
loggedIn = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
is: is,
|
||||||
|
getKey: getKey,
|
||||||
|
isLoggedIn: isLoggedIn,
|
||||||
|
start: start
|
||||||
|
};
|
||||||
|
|
||||||
|
function is(somePlayerKey) {
|
||||||
|
return playerKey == somePlayerKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLoggedIn() {
|
||||||
|
return loggedIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function start(name) {
|
||||||
|
var myID = Save.get('player.id');
|
||||||
|
if(myID != undefined) {
|
||||||
|
Messaging.send({tag: 'Tadaima', myID: myID, name: name});
|
||||||
|
playerKey = myID;
|
||||||
|
} else {
|
||||||
|
Messaging.send({tag: 'Hello', name: name});
|
||||||
|
}
|
||||||
|
Save.set('player.name', name);
|
||||||
|
}
|
17
js/StatusHandler.js
Normal file
17
js/StatusHandler.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
var baseTitle = document.title;
|
||||||
|
window.addEventListener('focus', reset);
|
||||||
|
|
||||||
|
return {
|
||||||
|
reset: reset,
|
||||||
|
set: set
|
||||||
|
};
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
document.title = baseTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(newStatus) {
|
||||||
|
if(!document.hasFocus()) {
|
||||||
|
document.title = newStatus + " - " + baseTitle;
|
||||||
|
}
|
||||||
|
}
|
42
js/Table.js
Normal file
42
js/Table.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import of from UnitJS.Fun;
|
||||||
|
import {compare, of, proj} from UnitJS.Fun;
|
||||||
|
|
||||||
|
function Table(itemMaker, sortCriterion) {
|
||||||
|
var items = {};
|
||||||
|
return {
|
||||||
|
get: get,
|
||||||
|
getAll: getAll,
|
||||||
|
insert: insert,
|
||||||
|
insertAll: insertAll,
|
||||||
|
remove: remove
|
||||||
|
};
|
||||||
|
|
||||||
|
function get(key) {
|
||||||
|
return items[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAll(criterion) {
|
||||||
|
return Object.keys(items)
|
||||||
|
.map(of(items))
|
||||||
|
.filter(criterion || function() {return true;})
|
||||||
|
.sort(compare(proj(sortCriterion)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function insert(key, value) {
|
||||||
|
items[key] = itemMaker(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertAll(itemsByKey) {
|
||||||
|
for(var key in itemsByKey) {
|
||||||
|
insert(key, itemsByKey[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(key) {
|
||||||
|
delete items[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
make: Table
|
||||||
|
}
|
90
js/Translations.js
Normal file
90
js/Translations.js
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
return {
|
||||||
|
'en-US': {
|
||||||
|
BushClover: "bush clover",
|
||||||
|
Cherry: "cherry",
|
||||||
|
Chrysanthemum: "chrysanthemum",
|
||||||
|
Iris: "iris",
|
||||||
|
Maple: "maple",
|
||||||
|
Paulownia: "paulownias",
|
||||||
|
Peony: "peony",
|
||||||
|
Pine: "pine",
|
||||||
|
Plum: "plum",
|
||||||
|
SusukiGrass: "susuki grass",
|
||||||
|
Willow: "willow",
|
||||||
|
Wisteria: "wisteria",
|
||||||
|
accept: "Accept",
|
||||||
|
alone: "No one to play with yet ! Wait a little",
|
||||||
|
decline: "Decline",
|
||||||
|
endRound: "End the round",
|
||||||
|
endGame: "Return to main menu",
|
||||||
|
join: "Join",
|
||||||
|
invite: "Invite",
|
||||||
|
invited: function(name) {
|
||||||
|
return name + " has invited you to a game";
|
||||||
|
},
|
||||||
|
koikoi: "KoiKoi !!",
|
||||||
|
leave: "Leave",
|
||||||
|
lost: "You lost the game",
|
||||||
|
monthFlower: function(flower) {
|
||||||
|
return "This month's flower is the " + flower;
|
||||||
|
},
|
||||||
|
noGames: "No games being played",
|
||||||
|
notFound: "No one goes by that name",
|
||||||
|
pickName: "Pick a name you like",
|
||||||
|
playing: function(name) {
|
||||||
|
return name + " is playing";
|
||||||
|
},
|
||||||
|
ok: "Ok",
|
||||||
|
startGameWith: "Start a game with",
|
||||||
|
theyScored: function(name) {
|
||||||
|
return name + " scored";
|
||||||
|
},
|
||||||
|
won: "You won !",
|
||||||
|
yourTurn: "Your turn",
|
||||||
|
youScored: "You scored ! Do you want to get your points and end the round or KoiKoi ?"
|
||||||
|
},
|
||||||
|
'fr-FR': {
|
||||||
|
BushClover: "lespédézas",
|
||||||
|
Cherry: "cerisiers",
|
||||||
|
Chrysanthemum: "chrysanthèmes",
|
||||||
|
Iris: "iris",
|
||||||
|
Maple: "érables",
|
||||||
|
Paulownia: "paulownias",
|
||||||
|
Peony: "pivoines",
|
||||||
|
Pine: "pins",
|
||||||
|
Plum: "prunus",
|
||||||
|
SusukiGrass: "herbes susukis",
|
||||||
|
Willow: "saules",
|
||||||
|
Wisteria: "glycines",
|
||||||
|
accept: "Accepter",
|
||||||
|
alone: "Personne pour jouer pour l'instant ! Attendez un peu",
|
||||||
|
decline: "Refuser",
|
||||||
|
endRound: "Finir la manche",
|
||||||
|
endGame: "Retourner au menu principal",
|
||||||
|
join: "Entrer",
|
||||||
|
invite: "Inviter",
|
||||||
|
invited: function(name) {
|
||||||
|
return name + " vous propose une partie";
|
||||||
|
},
|
||||||
|
koikoi: "KoiKoi !!",
|
||||||
|
leave: "Partir",
|
||||||
|
lost: "Vous avez perdu",
|
||||||
|
monthFlower: function(flower) {
|
||||||
|
return "C'est le mois des " + flower;
|
||||||
|
},
|
||||||
|
noGames: "Aucune partie en cours",
|
||||||
|
notFound: "Personne ne s'appelle comme ça",
|
||||||
|
pickName: "Choisissez votre nom",
|
||||||
|
playing: function(name) {
|
||||||
|
return "C'est à " + name;
|
||||||
|
},
|
||||||
|
ok: "Ok",
|
||||||
|
startGameWith: "Commencer une partie avec",
|
||||||
|
theyScored: function(name) {
|
||||||
|
return name + " a marqué";
|
||||||
|
},
|
||||||
|
won: "Vous avez gagné !",
|
||||||
|
yourTurn: "À vous",
|
||||||
|
youScored: "Vous avez marqué ! Voulez-vous empocher vos gains et terminer la manche ou faire KoiKoi ?"
|
||||||
|
}
|
||||||
|
}
|
BIN
skin/cards.jpg
Normal file
BIN
skin/cards.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 711 KiB |
201
skin/game.css
Normal file
201
skin/game.css
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
#game > div {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#them {
|
||||||
|
top: 0;
|
||||||
|
bottom: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table {
|
||||||
|
top: 25%;
|
||||||
|
bottom: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#you {
|
||||||
|
top: 75%;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card {
|
||||||
|
background: url("/cards.jpg") no-repeat;
|
||||||
|
background-size: 400% 1300%;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
border: 1px solid #555;
|
||||||
|
width: 4.5em;
|
||||||
|
height: 7em;
|
||||||
|
float: left;
|
||||||
|
margin: 0.5em;
|
||||||
|
background-position: 0% 100%; /* back of the card */
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.value0 {
|
||||||
|
background-position-x: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.value1 {
|
||||||
|
background-position-x: 33.3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.value2 {
|
||||||
|
background-position-x: 66.7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.value3 {
|
||||||
|
background-position-x: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.month0 {
|
||||||
|
background-position-y: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.month1 {
|
||||||
|
background-position-y: 8.3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.month2 {
|
||||||
|
background-position-y: 16.7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.month3 {
|
||||||
|
background-position-y: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.month4 {
|
||||||
|
background-position-y: 33.3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.month5 {
|
||||||
|
background-position-y: 41.7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.month6 {
|
||||||
|
background-position-y: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.month7 {
|
||||||
|
background-position-y: 58.3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.month8 {
|
||||||
|
background-position-y: 66.7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.month9 {
|
||||||
|
background-position-y: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.month10 {
|
||||||
|
background-position-y: 83.3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.month11 {
|
||||||
|
background-position-y: 91.7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.slot {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .card.moving {
|
||||||
|
position: relative;
|
||||||
|
transition-property: left, top;
|
||||||
|
transition-duration: 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game #rest {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rest.init, #rest.turn0 {
|
||||||
|
box-shadow: 2px 3px 0 0 #555, 4px 6px 0 0 #555, 6px 9px 0 0 #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rest.turn2 {
|
||||||
|
box-shadow: 2px 3px 0 0 #555, 4px 6px 0 0 #555, 5.5px 8.3px 0 0 #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rest.turn4 {
|
||||||
|
box-shadow: 2px 3px 0 0 #555, 4px 6px 0 0 #555, 5px 7.5px 0 0 #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rest.turn6 {
|
||||||
|
box-shadow: 2px 3px 0 0 #555, 4px 6px 0 0 #555, 4.5px 6.8px 0 0 #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rest.turn8 {
|
||||||
|
box-shadow: 2px 3px 0 0 #555, 4px 6px 0 0 #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rest.turn10 {
|
||||||
|
box-shadow: 2px 3px 0 0 #555, 3.5px 5.3px 0 0 #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rest.turn12 {
|
||||||
|
box-shadow: 2px 3px 0 0 #555, 3px 4.5px 0 0 #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rest.turn14 {
|
||||||
|
box-shadow: 2px 3px 0 0 #555, 2.5px 3.8px 0 0 #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rest.turn16 {
|
||||||
|
box-shadow: 2px 3px 0 0 #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game #turned {
|
||||||
|
margin: -0.1em 0 0 -4.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#river li.card.candidate, #you .hand.yourTurn li.card {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#river li.card.candidate {
|
||||||
|
box-shadow: 0 0 0.5em 0.1em #fc0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#you .hand li.card.selected {
|
||||||
|
margin-top: -1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game ul {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 0 0 3.5em;
|
||||||
|
padding: 0;
|
||||||
|
float: right;
|
||||||
|
position: relative;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#game ul#deck {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game ul#river {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
margin-left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#them .card, #you .card {
|
||||||
|
margin-left: -3.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .hand {
|
||||||
|
margin-left: 0;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game .hand .card {
|
||||||
|
margin: 0.5em 0.1em;
|
||||||
|
}
|
25
skin/hall.css
Normal file
25
skin/hall.css
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
.listSelector {
|
||||||
|
min-height: 4em;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listSelector .message {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin: 1em;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listSelector .message:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listSelector ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listSelector .player {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
54
skin/screen.css
Normal file
54
skin/screen.css
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
body > div {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > div.on {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialog {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialog > div {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 1em;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialog p.answers {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialog button {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#error {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 1em;
|
||||||
|
right: 1em;
|
||||||
|
max-width: 20em;
|
||||||
|
border: 1px solid #e0afac;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
background: bisque;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#error.on {
|
||||||
|
display: block;
|
||||||
|
}
|
Loading…
Reference in a new issue