Compare commits

...

18 commits

Author SHA1 Message Date
f15375c512 Reload games too when loading a saved account 2020-02-04 17:29:32 +01:00
18a5fc3321 Fixed variables forgotten by renaming 2020-02-04 13:23:12 +01:00
6615a7db65 Implement reconnection during account loading 2020-02-03 23:34:52 +01:00
49df598d7d Implement Hello messages for changing name while connected 2020-02-03 19:10:01 +01:00
a136cab1c8 Forgot the theming of the download button 2020-02-03 18:50:42 +01:00
f7a0ff1be7 Implement account saving / loading 2020-02-02 19:16:27 +01:00
394a7699d8 Add a navigation menu to browse screens once connected and add a new Settings screen where account export will be implemented 2020-02-01 22:42:04 +01:00
244215f09b Game coordinates store months' names, not their index so get the index first before comparing 2020-01-30 21:00:40 +01:00
313d91bcaf Oops, forgot the 'to' field in Yield messages 2020-01-30 15:21:09 +01:00
fb3bb64ff6 Bugfix : code wasn't comparing the game coordinates 2020-01-30 15:05:12 +01:00
c5e5a7c958 Now that we can receive game messages from several games at once, make sure the Game module only handles the ones for the current game 2020-01-28 18:24:34 +01:00
81216e3f01 Fix stuff implementing the re-synchronization after connection loss 2020-01-25 10:56:55 +01:00
19aa959c64 Try and make sense of the two data structures related to games with a good renaming 2020-01-23 22:17:57 +01:00
428e48f6f4 Follow move of 'logs' into PublicGame structure and start implementing re-sync protocol on the client side 2020-01-20 22:58:57 +01:00
ef947f7942 Keep previous state of game and use it to replay latest move 2020-01-18 23:10:22 +01:00
02edb77285 Explode too big procedures into simple short sub-functions 2020-01-18 23:09:06 +01:00
97b0bc0cc8 Follow «Coordinates» change in protocol 2020-01-18 09:35:51 +01:00
421b91c0ff Stop emitting the (temporarily ?) deprecated Quit message at the endof a game and factorize the function to go back to main menu instead 2020-01-16 22:33:41 +01:00
21 changed files with 468 additions and 194 deletions

View file

@ -7,15 +7,24 @@
<link rel="stylesheet" href="skin.css" type="text/css"/>
</head>
<body>
<div id="reception" class="on">
<ul id="sidebar" class="unactive"></ul>
<div id="login" class="on">
<h1>KoiKoi</h1>
<form id="login">
<form id="loginForm">
<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>
<form id="accountLoader">
<input type="submit" name="submitButton" hidden disabled/>
<p>or</p>
<p id="loadAccount">
<label for="pickFile"></label><input type="file" name="pickFile" accept="application/json,.json"/>
<input type="submit" name="doLoad" disabled/>
</p>
</form>
</div>
<div id="hall">
<form id="room">
@ -34,6 +43,11 @@
<ul></ul>
</div>
</div>
<div id="settings">
<p id="export">
<span></span><a download="koikoi.json" href=""></a>
</p>
</div>
<div id="dialog">
</div>
<p id="error"></p>

View file

@ -11,7 +11,7 @@ function HandCard(name) {
return card;
function onClick() {
if(State.state.playing && State.state.step == "ToPlay") {
if(State.globalState.playing && State.globalState.step == "ToPlay") {
if(State.getSelected() != undefined) {
State.select(false);
} else {

View file

@ -17,7 +17,7 @@ function RiverCard(name) {
var withCard = State.getSelected().value.name;
State.select(false);
State.play(
State.state.step == 'ToPlay' ?
State.globalState.step == 'ToPlay' ?
{capture: [withCard, name]} : {choose: name}
);
}

View file

@ -1,6 +1,7 @@
import * as Dom from UnitJS.Dom;
import I18n;
var sidebar = document.getElementById('sidebar');
var current = document.querySelector("body > div.on");
var errorBox = document.getElementById('error');
errorBox.addEventListener('click', function() {
@ -10,15 +11,10 @@ errorBox.addEventListener('click', function() {
return {
error: error,
dialog: dialog,
register: register,
select: select
};
function select(name) {
current.className = "";
current = document.getElementById(name);
current.className = "on";
}
function closeAndRun(dialog, action) {
return function() {
dialog.className = '';
@ -47,3 +43,17 @@ function error(message) {
errorBox.textContent = message;
errorBox.className = "on";
}
function register(name) {
sidebar.appendChild(Dom.make('li', {
onClick: function() {select(name);},
textContent: name
}));
}
function select(name) {
sidebar.className = "";
current.className = "";
current = document.getElementById(name);
current.className = "on";
}

View file

@ -5,7 +5,7 @@ import * as TurnedCard from GUI.Card.TurnedCard;
import * as Screen from GUI.Screen;
import {animate, catchUp, delay, getQueue} from GUI.Screen.Game.Animation;
import {
dom, init as initState, play, select, sets, state
dom, init as initState, play, select, sets, globalState
} from GUI.Screen.Game.State;
import Hanafuda;
import I18n;
@ -27,31 +27,33 @@ function init(gameID) {
initMessageHandlers();
Async.run(
Async.bind(
getSavedState(gameID),
function(o) {
getSavedStates(gameID),
function(states) {
return Async.sequence(
startSession(),
setGame(o)
previously(states)
);
}
)
);
}
function backToMain() {
window.location = '..';
}
function fail(errorCode) {
return function(f) {
Screen.dialog({
text: I18n.get(errorCode),
answers: [
{label: 'backToMain', action: function() {window.location = '..';}}
]
answers: [{label: 'backToMain', action: backToMain}]
});
}
}
function getSavedState(gameID) {
var gameState = Save.get('games.message.Game#' + gameID);
return gameState != undefined ? Async.wrap(gameState) : fail('gameNotFound');
function getSavedStates(gameID) {
var states = Save.get('games.state.Game#' + gameID);
return states != undefined ? Async.wrap(states) : fail('gameNotFound');
}
function startSession() {
@ -64,66 +66,79 @@ function startSession() {
}
}
function previously(states) {
if(states.former != undefined && states.latest.logs.length > 0) {
return Async.sequence(
setGame(states.former),
handleGameMessage(states.latest)
);
} else {
return setGame(states.latest);
}
}
function initMessageHandlers() {
window.addEventListener('focus', catchUp);
Messaging.addEventListener(["Game"], function(o) {
delay(handleGameMessage(o));
if(document.hasFocus() && getQueue().length == 1) {
catchUp();
} else {
StatusHandler.set("♪");
if(o.state.public.coordinates.gameID == globalState.game.public.coordinates.gameID) {
delay(handleGameMessage(o.state));
if(document.hasFocus() && getQueue().length == 1) {
catchUp();
} else {
StatusHandler.set("♪");
}
}
});
}
function handleGameMessage(o) {
if(o.state.public.gameState.turns == 0 || state.game == null) {
if(o.logs.length > 0) { // but still some logs, from the previous round
return Async.sequence(applyDiff(o), setGame(o)); // so play the diff, then set the new round
function handleGameMessage(state) {
if(state.public.coordinates.turn == 0) {
if(state.logs.length > 0) { // but still some logs, from the previous round
return Async.sequence(applyDiff(state), setGame(state)); // so play the diff, then set the new round
} else {
return setGame(o); // directly set a whole new game
return setGame(state); // directly set a whole new game
}
} else {
return applyDiff(o);
return applyDiff(state);
}
}
function setGame(o) {
function setGame(state) {
return function(f) {
setStatus(o.state);
setCaptures(o.state);
setStatus(state);
setCaptures(state);
[
[sets.river, o.state.public.river, RiverCard.make],
[sets.you.hand, o.state.playerHand, HandCard.make]
[sets.river, state.public.river, RiverCard.make],
[sets.you.hand, state.playerHand, HandCard.make]
].forEach(function(args) {setCardSet.apply(null, args)});
setTheirCards(o.state);
handleStep(o)(f);
setTheirCards(state);
handleStep(state)(f);
};
}
function handleStep(o) {
function handleStep(state) {
return function(f) {
handleTurnedCard(o, f);
if(state.step == "Scored") {
if(state.playing) {
askKoikoi(o, f);
handleTurnedCard(state, f);
if(globalState.step == "Scored") {
if(globalState.playing) {
askKoikoi(state, f);
} else {
theyScored(o, f);
theyScored(state, f);
}
} else if (state.step == "Over") {
gameEnd(o, f);
} else if (globalState.step == "Over") {
gameEnd(state, f);
} else {
f();
}
};
}
function handleTurnedCard(o, f) {
if(state.step == "Turned") {
setTurned(o.state.public.step.contents);
function handleTurnedCard(state, f) {
if(globalState.step == "Turned") {
setTurned(state.public.step.contents);
} else {
if(state.step == "ToPlay" && o.state.public.playing == o.state.public.oyake) {
rest.className = ["card", "turn" + o.state.public.gameState.turns].join(' ');
if(globalState.step == "ToPlay" && state.public.playing == state.public.oyake) {
dom.rest.className = ["card", "turn" + state.public.coordinates.turn].join(' ');
}
if(deck.lastChild.id != "rest") {
deck.removeChild(deck.lastChild);
@ -131,7 +146,7 @@ function handleTurnedCard(o, f) {
}
}
function askKoikoi(o, f) {
function askKoikoi(state, f) {
Screen.dialog({
text: I18n.get('youScored'),
answers: [
@ -141,59 +156,52 @@ function askKoikoi(o, f) {
});
}
function theyScored(o, f) {
function theyScored(state, f) {
Screen.dialog({
text: I18n.get('theyScored')(players.get(o.state.public.playing)),
text: I18n.get('theyScored')(players.get(state.public.playing)),
answers: [
{label: 'ok', action: f}
]
});
}
function gameEnd(o, f) {
function gameEnd(state, f) {
var winner, maxScore;
for(var key in o.state.public.scores) {
if(maxScore == undefined || o.state.public.scores[key] > maxScore) {
for(var key in state.public.scores) {
if(maxScore == undefined || state.public.scores[key] > maxScore) {
winner = key;
maxScore = o.state.public.scores[key];
maxScore = state.public.scores[key];
}
}
Screen.dialog({
text: I18n.get(Session.is(winner) ? 'won' : 'lost'),
answers: [{
label: 'endGame',
action: function() {
Messaging.send({tag: "Quit"});
Screen.select('reception');
f();
}
}]
answers: [{label: 'endGame', action: backToMain}]
});
}
function applyDiff(o) {
function applyDiff(state) {
return Async.sequence.apply(null,
o.logs.map(animate).concat(
Async.apply(setStatus, o.state),
handleStep(o)
state.logs.map(animate).concat(
Async.apply(setStatus, state),
handleStep(state)
)
);
}
function setStatus(game) {
Dom.clear(dom.status);
state.game = game;
state.step = game.public.step.tag;
if(game.public.month != state.month) {
state.month = game.public.month;
globalState.game = game;
globalState.step = game.public.step.tag;
if(game.public.coordinates.month != globalState.month) {
globalState.month = game.public.coordinates.month;
}
dom.status.appendChild(
Dom.make('li', {textContent: I18n.get('monthFlower')(I18n.get(state.month))})
Dom.make('li', {textContent: I18n.get('monthFlower')(I18n.get(globalState.month))})
);
var turn = null;
state.playing = Session.is(game.public.playing);
if(state.playing) {
sets.you.hand.dom.classList.toggle("yourTurn", state.step == "ToPlay");
globalState.playing = Session.is(game.public.playing);
if(globalState.playing) {
sets.you.hand.dom.classList.toggle("yourTurn", globalState.step == "ToPlay");
turn = I18n.get("yourTurn");
} else {
sets.you.hand.dom.classList.remove("yourTurn");
@ -231,7 +239,7 @@ function setCardSet(set, cardNames, constructor) {
function setTheirCards(game) {
var turnsTheyPlayed = Math.floor(
(game.public.gameState.turns + (Session.is(game.public.oyake) ? 0 : 1)) / 2
(game.public.coordinates.turn + (Session.is(game.public.oyake) ? 0 : 1)) / 2
);
Dom.clear(sets.them.hand.dom);
for(var i = 0; i < 8 - turnsTheyPlayed; i++) {
@ -240,8 +248,8 @@ function setTheirCards(game) {
}
function setTurned(cardName) {
state.turnedCard = TurnedCard.make(cardName);
if(state.playing) {
select(state.turnedCard);
globalState.turnedCard = TurnedCard.make(cardName);
if(globalState.playing) {
select(globalState.turnedCard);
}
}

View file

@ -14,38 +14,6 @@ return {
getQueue: getQueue
};
function animate(movement) {
return Async.bind(
Async.apply(function() {
var card;
var movingCards = [];
var side = (State.state.playing) ? 'you' : 'them';
var dest = State.sets.river;
if(movement.captures != undefined) {
card = Card.make(movement.played);
dest = State.sets[side];
movingCards.push([State.sets.river, dest, Card.make(movement.captures)]);
} else {
card = RiverCard.make(movement.played);
}
if(movement.source == 'Hand') {
movingCards.push([State.sets[side].hand, dest, card]);
} else {
var cardSet = {};
cardSet[card.value.name] = State.state.turnedCard || TurnedCard.make(card.value.name);
State.state.turnedCard = null;
movingCards.push([{card: cardSet, dom: State.dom.deck}, dest, card]);
}
return movingCards;
}),
function(movingCards) {
return Async.parallel.apply(null,
movingCards.map(function(args) { return moveCard.apply(null, args); })
);
}
);
}
function catchUp() {
if(queue.length > 0) {
var length = queue.length;
@ -66,6 +34,48 @@ function getQueue() {
return queue;
}
function animate(movement) {
return Async.bind(
Async.apply(cardMoves, movement),
function(movingCards) {
return Async.parallel.apply(null, movingCards);
}
)
}
function handleCaptures(movement, movingCards, side) {
if(movement.captures != undefined) {
var dest = State.sets[side];
movingCards.push(moveCard('river', dest, Card.make(movement.captures)));
return {card: Card.make(movement.played), dest: dest};
} else {
return {card: RiverCard.make(movement.played), dest: State.sets.river};
}
}
function handleSource(movement, movingCards, side, cardLeft) {
if(movement.source == 'Hand') {
movingCards.push(moveCard(side, cardLeft.dest, cardLeft.card));
} else {
var cardSet = {};
var name = cardLeft.card.value.name;
cardSet[name] = State.globalState.turnedCard || TurnedCard.make(name);
State.globalState.turnedCard = null;
movingCards.push(
moveCard({card: cardSet, dom: State.dom.deck}, cardLeft.dest, cardLeft.card)
);
}
}
function cardMoves(movement) {
var card;
var movingCards = [];
var side = (State.globalState.playing) ? 'you' : 'them';
var cardLeft = handleCaptures(movement, movingCards, side);
handleSource(movement, movingCards, side, cardLeft);
return movingCards;
}
function insertCard(toSet, card) {
if(toSet.dom != undefined) {
toSet.card[card.value.name] = card;
@ -75,34 +85,54 @@ function insertCard(toSet, card) {
}
}
function moveCard(fromSet, toSet, card) {
var from, originalCard;
var slot = Dom.make('li', {class: ['card', 'slot']});
if (fromSet.card[card.value.name] != undefined) {
originalCard = fromSet.card[card.value.name].dom;
delete fromSet.card[card.value.name];
function getSource(fromSet, card) {
var source, origin;
if(fromSet == 'river') {
source = State.sets.river;
} else if(typeof fromSet == 'object') {
source = fromSet;
} else {
originalCard = fromSet.dom.children[fromSet.dom.children.length - 1];
source = State.sets[fromSet].hand;
if(fromSet == 'them') {
origin = source.dom.children[source.dom.children.length - 1];
}
}
from = originalCard.getBoundingClientRect();
fromSet.dom.replaceChild(slot, originalCard);
origin = origin || source.card[card.value.name].dom;
delete source.card[card.value.name];
return {dom: source.dom, origin: origin};
}
function offsetInPlaceOf(referenceCard, targetCard) {
var from = referenceCard.getBoundingClientRect();
var to = targetCard.getBoundingClientRect();
targetCard.style.left = (from.left - to.left) + 'px';
targetCard.style.top = (from.top - to.top) + 'px';
targetCard.classList.add('moving');
targetCard.style.visibility = null;
}
function resetPostion(card) {
return function() {card.dom.style.left = 0; card.dom.style.top = 0;};
}
function cleanUpMove(source, card, slot) {
return function() {
source.dom.removeChild(slot);
card.dom.classList.remove('moving');
};
}
function moveCard(fromSet, toSet, card) {
var source = getSource(fromSet, card);
var slot = Dom.make('li', {class: ['card', 'slot']});
source.dom.replaceChild(slot, source.origin);
card.dom.style.visibility = 'hidden';
insertCard(toSet, card);
var to = card.dom.getBoundingClientRect();
card.dom.style.left = (from.left - to.left) + 'px';
card.dom.style.top = (from.top - to.top) + 'px';
card.dom.classList.add('moving');
card.dom.style.visibility = null;
offsetInPlaceOf(slot, card.dom)
return Async.sequence(
Async.wait(10),
Async.apply(function() {
card.dom.style.left = 0;
card.dom.style.top = 0;
}),
Async.apply(resetPostion(card)),
Async.wait(1000),
Async.apply(function() {
fromSet.dom.removeChild(slot);
card.dom.classList.remove('moving');
})
Async.apply(cleanUpMove(source, card, slot))
);
}

View file

@ -9,7 +9,7 @@ var dom = {
};
var selected;
var sets = {};
var state = {
var globalState = {
game: null,
playing: false,
step: null,
@ -25,7 +25,7 @@ return {
play: play,
select: select,
sets: sets,
state: state
globalState: globalState
};
function init() {
@ -64,7 +64,7 @@ function play(move) {
Messaging.send({
tag: "Play",
move: move,
onGame: state.game
onGame: globalState.game
});
}

View file

@ -1,6 +1,7 @@
import * as Dom from UnitJS.Dom;
import Games;
import I18n;
import register from GUI.Screen;
import * as Players from GUI.Screen.Hall.Players;
import * as GamesGUI from GUI.Screen.Hall.Games;
import Messaging;
@ -11,6 +12,7 @@ return {
};
function init() {
register('hall');
Players.init();
GamesGUI.init();
Messaging.addEventListener(["Okaeri"], function(o) {
@ -24,7 +26,7 @@ function init() {
});
Messaging.addEventListener(["LogOut"], function(o) {
// Just in case there was a game proposal from that player, in which case the game proposal's ID is the player's ID
Games.entries.remove(o.from);
Games.metadata.remove(o.from);
refresh();
});
Messaging.addEventListener(["Relay", "Invitation"], function(o) {
@ -33,12 +35,12 @@ function init() {
GamesGUI.refresh();
});
Messaging.addEventListener(["Relay", "Answer"], function(o) {
var gameEntry = Games.entries.get(o.from);
var gameEntry = Games.metadata.get(o.from);
if(gameEntry != undefined) {
if(!o.message.accept) {
gameEntry.answer = false;
} else {
Games.entries.remove(o.from);
Games.metadata.remove(o.from);
}
GamesGUI.refresh();
}

View file

@ -32,7 +32,7 @@ function gameProposal(game) {
if(game.value.answer != undefined) {
properties.textContent = I18n.get('refusedGame')(game.value.vs.name);
properties.class = 'clickable';
properties.onClick = function() {Games.entries.remove(game.key); refresh();};
properties.onClick = function() {Games.metadata.remove(game.key); refresh();};
} else {
properties.textContent = I18n.get('proposedGame')(
game.value.yourTurn,
@ -60,7 +60,7 @@ function pendingGame(game) {
function answer(key, accept) {
return function() {
Messaging.send({tag: "Answer", accept: accept, to: key});
Games.entries.remove(key);
Games.metadata.remove(key);
refresh();
}
}
@ -79,7 +79,7 @@ function answerDialog(key) {
}
function refresh() {
var sortedGames = Games.entries.getAll();
var sortedGames = Games.metadata.getAll();
list.refresh(sortedGames);
if(sortedGames.length < 1) {
list.message.textContent = I18n.get('noGames');

View file

@ -1,37 +1,53 @@
import I18n;
import GUI.ConnectedForm;
import select from GUI.Screen;
import {dialog, register, select} from GUI.Screen;
import Games;
import Messaging;
import Session;
import Save;
var login;
var form;
var loginForm;
var accountLoader;
var accountLoaderForm;
return {
init: init
};
function init() {
login = GUI.ConnectedForm.get('login');
form = login.root;
initDOM();
register('login');
initLogin();
initLoader();
initMessageHandlers();
var name = Save.get('player.name');
if(name != undefined && name.length > 0) {
form.you.value = name;
login.enable();
}
restoreName();
}
function initDOM() {
form.getElementsByTagName('label')[0].textContent = I18n.get('pickName');
form.join.value = I18n.get('join');
form.addEventListener('submit', function(e) {
function initLogin() {
login = GUI.ConnectedForm.get('loginForm');
loginForm = login.root;
loginForm.getElementsByTagName('label')[0].textContent = I18n.get('pickName');
loginForm.join.value = I18n.get('join');
loginForm.addEventListener('submit', function(e) {
e.preventDefault();
Session.start(form.you.value);
if(!Session.isLoggedIn()) {
Session.start(loginForm.you.value);
} else {
Messaging.send({tag: 'Hello', name: loginForm.you.value});
}
});
loginForm.you.addEventListener("input", validate);
}
function initLoader() {
accountLoader = GUI.ConnectedForm.get('accountLoader');
accountLoaderForm = accountLoader.root;
accountLoaderForm.getElementsByTagName('label')[0].textContent = I18n.get('pickFile');
accountLoaderForm.doLoad.value = I18n.get('doLoad');
accountLoaderForm.addEventListener('submit', loadAccountConfirm);
accountLoaderForm.pickFile.addEventListener("input", function() {
accountLoader.enable();
});
form.you.addEventListener("input", validate);
}
function initMessageHandlers() {
@ -42,6 +58,44 @@ function initMessageHandlers() {
});
}
function loadAccountConfirm(e) {
e.preventDefault();
if(Save.get('player') != undefined) {
dialog({
text: I18n.get('warnExistingAccount'),
answers: [
{label: 'confirmReplace', action: loadAccount},
{label: 'cancel', action: function() {}}
]
});
} else {
loadAccount();
}
}
function loadAccount() {
var fileReader = new FileReader();
fileReader.addEventListener('load', function() {
Save.set(null, JSON.parse(fileReader.result));
var name = restoreName();
Games.reload();
Messaging.reset();
if(name != undefined) {
Session.start(name);
}
});
fileReader.readAsText(accountLoaderForm.pickFile.files[0]);
}
function restoreName() {
var name = Save.get('player.name');
if(name != undefined && name.length > 0) {
loginForm.you.value = name;
login.enable();
return name;
}
}
function validate(e) {
login.enable(e.target.value != "");
}

25
js/GUI/Screen/Settings.js Normal file
View file

@ -0,0 +1,25 @@
import I18n;
import register from GUI.Screen;
import Save;
var button;
return {
init: init
};
function init() {
register('settings');
var exportRoot = document.getElementById('export');
var label = exportRoot.getElementsByTagName('span')[0];
button = exportRoot.getElementsByTagName('a')[0];
label.textContent = I18n.get('exportLabel');
button.textContent = I18n.get('doExport');
button.addEventListener('click', doExport);
}
function doExport() {
var data = encodeURIComponent(JSON.stringify(Save.get()));
button.href = 'data:application/json,' + data;
}

View file

@ -1,3 +1,4 @@
import Flower from Hanafuda;
import Messaging;
import player from Room;
import Save;
@ -5,16 +6,79 @@ import Session;
import Table;
import Time;
var entries = Table.make(function(o) {return o.value.date;});
entries.insertAll(Save.get('games.entry') || {});
var states = Table.make(function(o) {return 0;});
var metadata = Table.make(function(o) {return o.value.date;});
reload();
initMessageHandlers();
return {
entries: entries,
proposal: proposal
metadata: metadata,
proposal: proposal,
reload: reload
};
function makeEntry(state) {
function initMessageHandlers() {
Messaging.addEventListener(["Game"], function(o) {
var gameID = o.state.public.coordinates.gameID;
pushState(gameID, o.state);
metadata.insert(gameID, getMetadata(o.state));
metadata.save('games.metadata');
});
Messaging.addEventListener(["LogIn"], function(o) {
var gamesAgainst = states.getAll(
function(game) {return isAgainst(game.value, o.from);}
);
gamesAgainst.forEach(function(game) {
Messaging.send({
tag: "Sync",
latestKnown: game.value.latest.public.coordinates,
to: o.from
});
});
});
Messaging.addEventListener(["Relay", "Sync"], function(o) {
var gameID = o.message.latestKnown.gameID;
var state = states.get(gameID);
if(state != undefined) {
switch(compare(state.latest.public.coordinates, o.message.latestKnown)) {
case -1: Messaging.send({tag: "Yield", onGameID: gameID, to: o.from});
case 1: Messaging.send({tag: "Share", gameSave: state.latest});
}
}
});
Messaging.addEventListener(["Relay", "Yield"], function(o) {
var gameID = o.message.onGameID;
var state = states.get(gameID);
if(state != undefined && isAgainst(state, o.from)) {
Messaging.send({tag: "Share", gameSave: state.latest});
}
});
}
function isAgainst(state, playerID) {
return state.latest.public.nextPlayer[Session.getKey()] == playerID;
}
function compare(gameCoordinatesA, gameCoordinatesB) {
var monthIndexA = Flower[gameCoordinatesA.month];
var monthIndexB = Flower[gameCoordinatesB.month];
if(monthIndexA < monthIndexB) {
return -1;
} else if(monthIndexA > monthIndexB) {
return 1;
} else if(gameCoordinatesA.turn < gameCoordinatesB.turn) {
return -1;
} else if(gameCoordinatesA.turn > gameCoordinatesB.turn) {
return 1;
} else {
return 0;
}
}
function getMetadata(state) {
var sessionKey = Session.getKey();
return Time.timestamp({
vs: player(state.public.nextPlayer[sessionKey]),
@ -23,18 +87,23 @@ function makeEntry(state) {
}
function proposal(playerID, yourTurn) {
entries.insert(
metadata.insert(
playerID,
Time.timestamp({vs: player(playerID), yourTurn: yourTurn})
);
}
function initMessageHandlers() {
Messaging.addEventListener(["Game"], function(o) {
var gameID = o.state.public.gameState.gameID;
var entry = makeEntry(o.state);
Save.set("games.message." + gameID, o);
Save.set("games.entry." + gameID, entry);
entries.insert(gameID, entry);
});
function pushState(gameID, newState) {
if(states.get(gameID) == undefined) {
states.insert(gameID, {});
}
var state = states.get(gameID);
state.former = state.latest;
state.latest = newState;
states.save('games.state');
}
function reload() {
states.load('games.state');
metadata.load('games.metadata');
}

View file

@ -1,6 +1,7 @@
import * as Login from GUI.Screen.Login;
import * as Hall from GUI.Screen.Hall;
import * as Game from GUI.Screen.Game;
import * as Settings from GUI.Screen.Settings;
var gamePath = window.location.pathname.match(/\/game\/([0-9A-Fa-f]+)/);
@ -9,4 +10,5 @@ if(gamePath) {
} else {
Login.init();
Hall.init();
Settings.init();
}

View file

@ -20,6 +20,7 @@ init();
return {
addEventListener: addEventListener,
isOn: isOn,
reset: reset,
send: send
};
@ -136,3 +137,9 @@ function ping() {
}
}, keepAlivePeriod * s);
}
function reset() {
on = false; // this should be handled by the close event but when other operations are performed right after the reset, the event might not have had time to fire and the other functions, in particular send(), will assume the socket is still open and fail to actually send them instead of gently queueing them for when connectivity goes back up
reconnectDelay = 0.125; // since we're forcing the connection to close, there's no reason to assume the server went down so we manually lower the reconnectDelay to make the reconnection more fluid (there's no point making the user wait the usual 1 s. and the worse that could happen is we overflow the server a little with 3 connection attempts in 1 s.)
ws.close();
}

View file

@ -14,19 +14,21 @@ function player(key) {
return {id: key, name: players.get(key)};
}
function initMessageHandlers() {
Messaging.addEventListener(["Okaeri"], function(o) {
players.insertAll(o.room);
});
Messaging.addEventListener(["Welcome"], function(o) {
players.insertAll(o.room);
});
Messaging.addEventListener(["LogIn"], function(o) {
if(!Session.is(o.from)) {
players.insert(o.from, o.as);
}
});
Messaging.addEventListener(["LogOut"], function(o) {
players.remove(o.from);
});
function enterAll(o) {
for(key in o) {
enterPlayer(key, o[key]);
}
}
function enterPlayer(key, name) {
if(!Session.is(key)) {
players.insert(key, name);
}
}
function initMessageHandlers() {
Messaging.addEventListener(["Okaeri"], function(o) {enterAll(o.room);});
Messaging.addEventListener(["Welcome"], function(o) {enterAll(o.room);});
Messaging.addEventListener(["LogIn"], function(o) {enterPlayer(o.from, o.as);});
Messaging.addEventListener(["LogOut"], function(o) {players.remove(o.from);});
}

View file

@ -27,6 +27,8 @@ function get(key) {
} else {
return null;
}
} else {
return save;
}
}

View file

@ -17,6 +17,8 @@ Messaging.addEventListener(["LogIn"], function(o) {
}
});
Messaging.addEventListener('close', function() {loggedIn = false;});
return {
is: is,
getKey: getKey,

View file

@ -1,3 +1,4 @@
import Save;
import of from UnitJS.Fun;
import {compare, of, proj} from UnitJS.Fun;
@ -8,7 +9,9 @@ function Table(sortCriterion) {
getAll: getAll,
insert: insert,
insertAll: insertAll,
remove: remove
load: load,
remove: remove,
save
};
function get(key) {
@ -22,7 +25,6 @@ function Table(sortCriterion) {
.sort(compare(sortCriterion));
}
function insert(key, value) {
items[key] = value;
}
@ -33,9 +35,17 @@ function Table(sortCriterion) {
}
}
function load(path) {
items = Save.get(path) || {};
}
function remove(key) {
delete items[key];
}
function save(path) {
Save.set(path, items);
}
}
return {

View file

@ -15,9 +15,14 @@ return {
accept: "Let's go !",
alone: "No one to play with yet ! Wait a little",
backToMain: "Back to main menu",
cancel: "Cancel",
confirmReplace: "Yes, do replace my existing account",
decline: "No thanks",
doExport: "Save",
doLoad: "Load",
endRound: "End the round",
endGame: "Return to main menu",
exportLabel: "Save your account data to load it somewhere else",
gameNotFound: "You followed a fishy link, this game is no more",
join: "Join",
invite: "Invite",
@ -36,6 +41,7 @@ return {
var whose = yourTurn ? 'your' : name + "'s";
return 'Game vs. ' + name + ' (' + whose + ' turn)';
},
pickFile: "Load an existing account",
pickName: "Pick a name you like",
playing: function(name) {
return name + " is playing";
@ -45,7 +51,7 @@ return {
if(yourTurn) {
return name + proposed;
} else {
return "You" + proposed + " to " + name;
return "You" + proposed + " to " + name;
}
},
questionAccept: "Do you want to start the game ?",
@ -56,6 +62,7 @@ return {
theyScored: function(name) {
return name + " scored";
},
warnExistingAccount: "Your current account will be erased and lost forever",
won: "You won !",
yourTurn: "Your turn",
youScored: "You scored ! Do you want to get your points and end the round or KoiKoi ?"
@ -76,9 +83,14 @@ return {
accept: "C'est parti !",
alone: "Personne pour jouer pour l'instant ! Attendez un peu",
backToMain: "Retourner au menu principal",
cancel: "Annuler",
confirmReplace: "Oui, remplacer mon compte",
decline: "Non merci",
doExport: "Télécharger",
doLoad: "Charger",
endRound: "Finir la manche",
endGame: "Retourner au menu principal",
exportLabel: "Télécharger les données de votre compte pour les charger ailleurs",
gameNotFound: "Ce lien est louche, la partie n'est plus",
join: "Entrer",
invite: "Inviter",
@ -97,6 +109,7 @@ return {
var whose = yourTurn ? 'vous' : name;
return 'Partie en cours contre ' + name + ' (à ' + whose + ')';
},
pickFile: "Charger un compte existant",
pickName: "Choisissez votre nom",
playing: function(name) {
return "C'est à " + name;
@ -117,6 +130,7 @@ return {
theyScored: function(name) {
return name + " a marqué";
},
warnExistingAccount: "Le compte existant sera remplacé et perdu à tout jamais",
won: "Vous avez gagné !",
yourTurn: "À vous",
youScored: "Vous avez marqué ! Voulez-vous empocher vos gains et terminer la manche ou faire KoiKoi ?"

View file

@ -1,4 +1,4 @@
body > div {
body > div, ul#sidebar.unactive {
display: none;
}
@ -52,3 +52,18 @@ body > div.on {
#error.on {
display: block;
}
ul#sidebar {
position: fixed;
top: 0;
right: 0;
margin: 0;
padding: 0;
list-style: none;
}
ul#sidebar > li {
cursor: pointer;
float: left;
padding: 0.5em 1em;
}

8
skin/settings.css Normal file
View file

@ -0,0 +1,8 @@
#export a {
padding: 0.5em;
margin: 0 1em;
border-radius: 0.5em;
text-decoration: none;
background: #35e;
color: #fff;
}