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

View file

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

View file

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

View file

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

View file

@ -14,38 +14,6 @@ return {
getQueue: getQueue 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() { function catchUp() {
if(queue.length > 0) { if(queue.length > 0) {
var length = queue.length; var length = queue.length;
@ -66,6 +34,48 @@ function getQueue() {
return queue; 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) { function insertCard(toSet, card) {
if(toSet.dom != undefined) { if(toSet.dom != undefined) {
toSet.card[card.value.name] = card; toSet.card[card.value.name] = card;
@ -75,34 +85,54 @@ function insertCard(toSet, card) {
} }
} }
function moveCard(fromSet, toSet, card) { function getSource(fromSet, card) {
var from, originalCard; var source, origin;
var slot = Dom.make('li', {class: ['card', 'slot']}); if(fromSet == 'river') {
if (fromSet.card[card.value.name] != undefined) { source = State.sets.river;
originalCard = fromSet.card[card.value.name].dom; } else if(typeof fromSet == 'object') {
delete fromSet.card[card.value.name]; source = fromSet;
} else { } 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(); origin = origin || source.card[card.value.name].dom;
fromSet.dom.replaceChild(slot, originalCard); 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'; card.dom.style.visibility = 'hidden';
insertCard(toSet, card); insertCard(toSet, card);
var to = card.dom.getBoundingClientRect(); offsetInPlaceOf(slot, card.dom)
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;
return Async.sequence( return Async.sequence(
Async.wait(10), Async.wait(10),
Async.apply(function() { Async.apply(resetPostion(card)),
card.dom.style.left = 0;
card.dom.style.top = 0;
}),
Async.wait(1000), Async.wait(1000),
Async.apply(function() { Async.apply(cleanUpMove(source, card, slot))
fromSet.dom.removeChild(slot);
card.dom.classList.remove('moving');
})
); );
} }

View file

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

View file

@ -1,6 +1,7 @@
import * as Dom from UnitJS.Dom; import * as Dom from UnitJS.Dom;
import Games; import Games;
import I18n; import I18n;
import register from GUI.Screen;
import * as Players from GUI.Screen.Hall.Players; import * as Players from GUI.Screen.Hall.Players;
import * as GamesGUI from GUI.Screen.Hall.Games; import * as GamesGUI from GUI.Screen.Hall.Games;
import Messaging; import Messaging;
@ -11,6 +12,7 @@ return {
}; };
function init() { function init() {
register('hall');
Players.init(); Players.init();
GamesGUI.init(); GamesGUI.init();
Messaging.addEventListener(["Okaeri"], function(o) { Messaging.addEventListener(["Okaeri"], function(o) {
@ -24,7 +26,7 @@ function init() {
}); });
Messaging.addEventListener(["LogOut"], function(o) { 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 // 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(); refresh();
}); });
Messaging.addEventListener(["Relay", "Invitation"], function(o) { Messaging.addEventListener(["Relay", "Invitation"], function(o) {
@ -33,12 +35,12 @@ function init() {
GamesGUI.refresh(); GamesGUI.refresh();
}); });
Messaging.addEventListener(["Relay", "Answer"], function(o) { Messaging.addEventListener(["Relay", "Answer"], function(o) {
var gameEntry = Games.entries.get(o.from); var gameEntry = Games.metadata.get(o.from);
if(gameEntry != undefined) { if(gameEntry != undefined) {
if(!o.message.accept) { if(!o.message.accept) {
gameEntry.answer = false; gameEntry.answer = false;
} else { } else {
Games.entries.remove(o.from); Games.metadata.remove(o.from);
} }
GamesGUI.refresh(); GamesGUI.refresh();
} }

View file

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

View file

@ -1,37 +1,53 @@
import I18n; import I18n;
import GUI.ConnectedForm; import GUI.ConnectedForm;
import select from GUI.Screen; import {dialog, register, select} from GUI.Screen;
import Games;
import Messaging; import Messaging;
import Session; import Session;
import Save; import Save;
var login; var login;
var form; var loginForm;
var accountLoader;
var accountLoaderForm;
return { return {
init: init init: init
}; };
function init() { function init() {
login = GUI.ConnectedForm.get('login'); register('login');
form = login.root; initLogin();
initDOM(); initLoader();
initMessageHandlers(); initMessageHandlers();
var name = Save.get('player.name'); restoreName();
if(name != undefined && name.length > 0) {
form.you.value = name;
login.enable();
}
} }
function initDOM() { function initLogin() {
form.getElementsByTagName('label')[0].textContent = I18n.get('pickName'); login = GUI.ConnectedForm.get('loginForm');
form.join.value = I18n.get('join'); loginForm = login.root;
form.addEventListener('submit', function(e) { loginForm.getElementsByTagName('label')[0].textContent = I18n.get('pickName');
loginForm.join.value = I18n.get('join');
loginForm.addEventListener('submit', function(e) {
e.preventDefault(); 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() { 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) { function validate(e) {
login.enable(e.target.value != ""); 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 Messaging;
import player from Room; import player from Room;
import Save; import Save;
@ -5,16 +6,79 @@ import Session;
import Table; import Table;
import Time; import Time;
var entries = Table.make(function(o) {return o.value.date;}); var states = Table.make(function(o) {return 0;});
entries.insertAll(Save.get('games.entry') || {}); var metadata = Table.make(function(o) {return o.value.date;});
reload();
initMessageHandlers(); initMessageHandlers();
return { return {
entries: entries, metadata: metadata,
proposal: proposal 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(); var sessionKey = Session.getKey();
return Time.timestamp({ return Time.timestamp({
vs: player(state.public.nextPlayer[sessionKey]), vs: player(state.public.nextPlayer[sessionKey]),
@ -23,18 +87,23 @@ function makeEntry(state) {
} }
function proposal(playerID, yourTurn) { function proposal(playerID, yourTurn) {
entries.insert( metadata.insert(
playerID, playerID,
Time.timestamp({vs: player(playerID), yourTurn: yourTurn}) Time.timestamp({vs: player(playerID), yourTurn: yourTurn})
); );
} }
function initMessageHandlers() { function pushState(gameID, newState) {
Messaging.addEventListener(["Game"], function(o) { if(states.get(gameID) == undefined) {
var gameID = o.state.public.gameState.gameID; states.insert(gameID, {});
var entry = makeEntry(o.state); }
Save.set("games.message." + gameID, o); var state = states.get(gameID);
Save.set("games.entry." + gameID, entry); state.former = state.latest;
entries.insert(gameID, entry); 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 Login from GUI.Screen.Login;
import * as Hall from GUI.Screen.Hall; import * as Hall from GUI.Screen.Hall;
import * as Game from GUI.Screen.Game; 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]+)/); var gamePath = window.location.pathname.match(/\/game\/([0-9A-Fa-f]+)/);
@ -9,4 +10,5 @@ if(gamePath) {
} else { } else {
Login.init(); Login.init();
Hall.init(); Hall.init();
Settings.init();
} }

View file

@ -20,6 +20,7 @@ init();
return { return {
addEventListener: addEventListener, addEventListener: addEventListener,
isOn: isOn, isOn: isOn,
reset: reset,
send: send send: send
}; };
@ -136,3 +137,9 @@ function ping() {
} }
}, keepAlivePeriod * s); }, 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)}; return {id: key, name: players.get(key)};
} }
function initMessageHandlers() { function enterAll(o) {
Messaging.addEventListener(["Okaeri"], function(o) { for(key in o) {
players.insertAll(o.room); enterPlayer(key, o[key]);
}); }
Messaging.addEventListener(["Welcome"], function(o) { }
players.insertAll(o.room);
}); function enterPlayer(key, name) {
Messaging.addEventListener(["LogIn"], function(o) { if(!Session.is(key)) {
if(!Session.is(o.from)) { players.insert(key, name);
players.insert(o.from, o.as); }
} }
});
Messaging.addEventListener(["LogOut"], function(o) { function initMessageHandlers() {
players.remove(o.from); 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 { } else {
return null; return null;
} }
} else {
return save;
} }
} }

View file

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

View file

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

View file

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