diff --git a/game.html b/game.html index 1c8b587..bbfa5d3 100644 --- a/game.html +++ b/game.html @@ -7,7 +7,7 @@ -
+
      diff --git a/js/GUI/Card.js b/js/GUI/Card.js index 82cdd11..25b3a83 100644 --- a/js/GUI/Card.js +++ b/js/GUI/Card.js @@ -1,21 +1,16 @@ import Hanafuda; import * as Dom from UnitJS.Dom; -function Card(name) { - var value = Hanafuda.Card[name]; - var dom = Dom.make('li', { - class: [ - "card", - "value" + Hanafuda.getValue(this.value), - "month" + this.value.flower - ] - }); - return { - value: value, - dom: dom - }; -} - return { make: Card }; + +function Card(name) { + var value = Hanafuda.Card[name]; + return { + value: value, + dom: Dom.make('li', { + class: ["card", "value" + value.monthOffset, "month" + value.flower] + }) + }; +} diff --git a/js/GUI/Card/HandCard.js b/js/GUI/Card/HandCard.js index d7043e2..b7e1deb 100644 --- a/js/GUI/Card/HandCard.js +++ b/js/GUI/Card/HandCard.js @@ -1,28 +1,34 @@ +import * as Card from GUI.Card; +import * as State from GUI.Screen.Game.State; -function HandCard() { - Card.apply(this, arguments); -} - -HandCard.prototype.onClick = function() { - var card = this; - return function() { - if(status.playing && status.step == "ToPlay") { - if(selected != undefined) { - selected.setSelected(false); - } else { - card.play(); - } - } - }; +return { + make: HandCard }; -HandCard.prototype.setSelected = setSelected; +function HandCard(name) { + var card = Card.make(name); + card.onClick = onClick; + return card; -HandCard.prototype.play = function() { - var matching = matchingInRiver(this.value); - if(matching.length > 1) { - this.setSelected(true); - } else { - play({play: this.name}); - } + function onClick() { + return function() { + if(State.state.playing && State.state.step == "ToPlay") { + if(State.getSelected() != undefined) { + State.select(false); + } else { + play(); + } + } + }; + } + + function play() { + var matching = State.matchingInRiver(card.value); + if(matching.length > 1) { + State.select(card); + } else { + State.play({play: card.name}); + } + } } + diff --git a/js/GUI/Card/RiverCard.js b/js/GUI/Card/RiverCard.js index ba51be1..ccaa48d 100644 --- a/js/GUI/Card/RiverCard.js +++ b/js/GUI/Card/RiverCard.js @@ -1,4 +1,9 @@ import * as Card from GUI.Card; +import * as State from GUI.Screen.Game.State; + +return { + make: RiverCard +}; function RiverCard(name) { var card = Card.make(name); @@ -10,25 +15,18 @@ function RiverCard(name) { function onClick() { return function() { if(candidate) { - var withCard = selected.name; - selected.setSelected(false); - play( - status.step == 'ToPlay' ? {capture: [withCard, card.name]} : {choose: card.name} + var withCard = State.getSelected().name; + State.select(false); + State.play( + State.state.step == 'ToPlay' ? + {capture: [withCard, card.name]} : {choose: card.name} ); } }; } - function setCandidate() { + function setCandidate(yes) { candidate = yes; card.dom.classList.toggle("candidate", yes); } } - -RiverCard.prototype.onClick = function() { - var card = this; -}; - -return { - make: RiverCard -}; diff --git a/js/GUI/Card/TurnedCard.js b/js/GUI/Card/TurnedCard.js index dbf79f3..e4882a9 100644 --- a/js/GUI/Card/TurnedCard.js +++ b/js/GUI/Card/TurnedCard.js @@ -1,14 +1,13 @@ import * as Card from GUI.Card; - -function TurnedCard(name) { - var card = Card.make(name); - card.dom.id = "turned"; - deck.appendChild(this.dom); -} - -TurnedCard.prototype.onClick = Card.prototype.onClick; -TurnedCard.prototype.setSelected = setSelected; +import dom from GUI.Screen.Game.State; return { make: TurnedCard }; + +function TurnedCard(name) { + var card = Card.make(name); + card.dom.id = "turned"; + dom.deck.appendChild(card.dom); + return card; +} diff --git a/js/GUI/Screen/Game.js b/js/GUI/Screen/Game.js index c381782..0e6fb2f 100644 --- a/js/GUI/Screen/Game.js +++ b/js/GUI/Screen/Game.js @@ -1,74 +1,77 @@ +import * as Card from GUI.Card; +import * as HandCard from GUI.Card.HandCard; +import * as RiverCard from GUI.Card.RiverCard; +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, select, sets, state} from GUI.Screen.Game.State; import Hanafuda; import I18n; import Messaging; import players from Room; -import * as Screen from GUI.Screen; +import Save; import Session; import StatusHandler; import * as Async from UnitJS.Async; import * as Dom from UnitJS.Dom; import * as Fun from UnitJS.Fun; -var deck = document.getElementById("deck"); -var rest = document.getElementById("rest"); -var status = { - dom: document.getElementById("status"), - game: null, - playing: false, - step: null, - month: null -}; -var sets; -var selected = null; -var turnedCard = null; -var queue = []; - return { init: init } -function init(state) { - sets = buildSets(); - window.addEventListener('focus', runQueue); +function init(gameID) { + initState(); + initMessageHandlers(); + Async.run( + Async.bind( + getSavedState(gameID), + function(state) { + return Async.sequence( + startSession(), + setGame({tag: "Game", state: state, logs: []}) + ); + } + ) + ); +} + +function fail(errorCode) { + return function(f) { + Screen.dialog({ + text: I18n.get(errorCode), + answers: [ + {label: 'backToMain', action: function() {window.location = '..';}} + ] + }); + } +} + +function getSavedState(gameID) { + var gameState = Save.get('games.state.Game#' + gameID); + return gameState != undefined ? Async.wrap(gameState) : fail('gameNotFound'); +} + +function startSession() { + var name = Save.get('player.name'); + if(name != undefined) { + Session.start(name); + return Async.wrap(); + } else { + return fail('noNameToPlay'); + } +} + +function initMessageHandlers() { + window.addEventListener('focus', catchUp); Messaging.addEventListener(["Game"], function(o) { - queue.push(handleGameMessage(o)); - if(document.hasFocus() && queue.length == 1) { - runQueue(); + delay(handleGameMessage(o)); + if(document.hasFocus() && getQueue().length == 1) { + catchUp(); } else { StatusHandler.set("♪"); } }); - Async.run(setGame({tag: "Game", state: state, logs: []})); -} - -function buildSets() { - var sets = {}; - ['river', 'you', 'them'].forEach(function(id) { - var dom = document.getElementById(id); - if(dom.tagName.toLowerCase() == 'ul') { - sets[id] = {card: null, dom: dom}; - } else { - sets[id] = {}; - for(var i = 0; i < dom.children.length; i++) { - if(dom.children[i].tagName.toLowerCase() == 'ul') { - sets[id][dom.children[i].className] = {card: {}, dom: dom.children[i]}; - } - } - } - }); - return sets; -} - -function runQueue() { - if(queue.length > 0) { - var length = queue.length; - Async.run.apply(null, queue.concat( - Async.apply(function() { - queue = queue.slice(length); - runQueue(); - }) - )); - } } function handleGameMessage(o) { @@ -88,8 +91,8 @@ function setGame(o) { setStatus(o.state); setCaptures(o.state); [ - [sets.river, o.state.public.river, RiverCard], - [sets.you.hand, o.state.playerHand, HandCard] + [sets.river, o.state.public.river, RiverCard.make], + [sets.you.hand, o.state.playerHand, HandCard.make] ].forEach(function(args) {setCardSet.apply(null, args)}); setTheirCards(o.state); handleStep(o)(f); @@ -99,13 +102,13 @@ function setGame(o) { function handleStep(o) { return function(f) { handleTurnedCard(o, f); - if(status.step == "Scored") { - if(status.playing) { + if(state.step == "Scored") { + if(state.playing) { askKoikoi(o, f); } else { theyScored(o, f); } - } else if (status.step == "Over") { + } else if (state.step == "Over") { gameEnd(o, f); } else { f(); @@ -114,10 +117,10 @@ function handleStep(o) { } function handleTurnedCard(o, f) { - if(status.step == "Turned") { + if(state.step == "Turned") { setTurned(o.state.public.step.contents); } else { - if(status.step == "ToPlay" && o.state.public.playing == o.state.public.oyake) { + if(state.step == "ToPlay" && o.state.public.playing == o.state.public.oyake) { rest.className = ["card", "turn" + o.state.public.turns].join(' '); } if(deck.lastChild.id != "rest") { @@ -134,7 +137,6 @@ function askKoikoi(o, f) { {label: 'koikoi', action: function() {play({koiKoi: true}); f();}} ] }); - } function theyScored(o, f) { @@ -176,114 +178,26 @@ function applyDiff(o) { ); } -function animate(movement) { - return Async.bind( - Async.apply(function() { - var card; - var movingCards = []; - var side = (status.playing) ? 'you' : 'them'; - var dest = sets.river; - if(movement.captures != undefined) { - card = new Card(movement.played); - dest = sets[side]; - movingCards.push([sets.river, dest, new Card(movement.captures)]); - } else { - card = new RiverCard(movement.played); - } - if(movement.source == 'Hand') { - movingCards.push([sets[side].hand, dest, card]); - } else { - var cardSet = {}; - cardSet[card.name] = turnedCard || new TurnedCard(card.name); - turnedCard = null; - movingCards.push([{card: cardSet, dom: deck}, dest, card]); - } - return movingCards; - }), - function(movingCards) { - return Async.parallel.apply(null, - movingCards.map(function(args) { return moveCard.apply(null, args); }) - ); - } - ); -} - -function moveCard(fromSet, toSet, card) { - var from, originalCard; - var slot = Dom.make('li', {class: ['card', 'slot']}); - if (fromSet.card[card.name] != undefined) { - originalCard = fromSet.card[card.name].dom; - delete fromSet.card[card.name]; - } else { - var originalCard = fromSet.dom.children[fromSet.dom.children.length - 1]; - } - from = originalCard.getBoundingClientRect(); - fromSet.dom.replaceChild(slot, originalCard); - 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; - return Async.sequence( - Async.wait(10), - Async.apply(function() { - card.dom.style.left = 0; - card.dom.style.top = 0; - }), - Async.wait(1000), - Async.apply(function() { - fromSet.dom.removeChild(slot); - card.dom.classList.remove('moving'); - }) - ); -} - -function insertCard(toSet, card) { - if(toSet.dom != undefined) { - toSet.card[card.name] = card; - toSet.dom.appendChild(card.dom); - } else { - insertCard(toSet[card.value.family.class], card); - } -} - -function play(move) { - Messaging.send({ - tag: "Play", - move: move, - onGame: status.game - }); -} - -function matchingInRiver(card) { - return Fun.mapFilter( - Fun.of(sets.river.card), - Fun.defined - )(Hanafuda.sameMonth(card).map(Fun.proj('name'))); -} - function setStatus(game) { - Dom.clear(status.dom); - status.game = game; - status.step = game.public.step.tag; - if(game.public.month != status.month) { - status.month = game.public.month; + Dom.clear(dom.status); + state.game = game; + state.step = game.public.step.tag; + if(game.public.month != state.month) { + state.month = game.public.month; } - status.dom.appendChild( - Dom.make('li', {textContent: I18n.get('monthFlower')(I18n.get(status.month))}) + dom.status.appendChild( + Dom.make('li', {textContent: I18n.get('monthFlower')(I18n.get(state.month))}) ); var turn = null; - status.playing = Session.is(game.public.playing); - if(status.playing) { - sets.you.hand.dom.classList.toggle("yourTurn", status.step == "ToPlay"); + state.playing = Session.is(game.public.playing); + if(state.playing) { + sets.you.hand.dom.classList.toggle("yourTurn", state.step == "ToPlay"); turn = I18n.get("yourTurn"); } else { sets.you.hand.dom.classList.remove("yourTurn"); turn = I18n.get('playing')(players.get(game.public.playing)); } - status.dom.appendChild(Dom.make('li', {textContent: turn})); + dom.status.appendChild(Dom.make('li', {textContent: turn})); } function setCaptures(game) { @@ -296,7 +210,7 @@ function setCaptures(game) { Dom.clear(byClass[family.class]); }); game.public.players[key].meld.forEach(function(cardName) { - var card = new Card(cardName); + var card = Card.make(cardName); byClass[card.value.family.class].appendChild(card.dom); }); } @@ -307,7 +221,7 @@ function setCardSet(set, cardNames, constructor) { set.card = {}; Dom.clear(set.dom); cardNames.forEach(function(cardName) { - var card = new constructor(cardName); + var card = constructor(cardName); set.card[cardName] = card; set.dom.appendChild(card.dom); }); @@ -315,7 +229,7 @@ function setCardSet(set, cardNames, constructor) { function setTheirCards(game) { var turnsTheyPlayed = Math.floor( - (game.public.turns + (Session.is(game.public.oyake) ? 0 : 1)) / 2 + (game.public.gameState.turns + (Session.is(game.public.oyake) ? 0 : 1)) / 2 ); Dom.clear(sets.them.hand.dom); for(var i = 0; i < 8 - turnsTheyPlayed; i++) { @@ -324,20 +238,8 @@ function setTheirCards(game) { } function setTurned(cardName) { - turnedCard = new TurnedCard(cardName); - if(status.playing) { - selected = turnedCard; - showCandidates(Hanafuda.Card[cardName], true); + state.turnedCard = TurnedCard.make(cardName); + if(state.playing) { + select(turnedCard); } } - -function showCandidates(card, yes) { - matchingInRiver(card).forEach(function(riverCard) {riverCard.setCandidate(yes);}); -} - -function setSelected(yes) { - selected = yes ? this : null; - this.dom.classList.toggle('selected', yes); - showCandidates(this.value, yes); -} - diff --git a/js/GUI/Screen/Game/Animation.js b/js/GUI/Screen/Game/Animation.js new file mode 100644 index 0000000..3611ed3 --- /dev/null +++ b/js/GUI/Screen/Game/Animation.js @@ -0,0 +1,106 @@ +import * as Card from GUI.Card; +import * as State from GUI.Screen.Game.State; +import * as Async from UnitJS.Async; +import * as Dom from UnitJS.Dom; + +var queue = []; + +return { + animate: animate, + catchUp: catchUp, + delay: delay, + 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.name] = State.state.turnedCard || TurnedCard.make(card.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; + Async.run.apply(null, queue.concat( + Async.apply(function() { + queue = queue.slice(length); + catchUp(); + }) + )); + } +} + +function delay(animation) { + queue.push(animation); +} + +function getQueue() { + return queue; +} + +function insertCard(toSet, card) { + if(toSet.dom != undefined) { + toSet.card[card.name] = card; + toSet.dom.appendChild(card.dom); + } else { + insertCard(toSet[card.value.family.class], card); + } +} + +function moveCard(fromSet, toSet, card) { + var from, originalCard; + var slot = Dom.make('li', {class: ['card', 'slot']}); + if (fromSet.card[card.name] != undefined) { + originalCard = fromSet.card[card.name].dom; + delete fromSet.card[card.name]; + } else { + originalCard = fromSet.dom.children[fromSet.dom.children.length - 1]; + } + from = originalCard.getBoundingClientRect(); + fromSet.dom.replaceChild(slot, originalCard); + 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; + return Async.sequence( + Async.wait(10), + Async.apply(function() { + card.dom.style.left = 0; + card.dom.style.top = 0; + }), + Async.wait(1000), + Async.apply(function() { + fromSet.dom.removeChild(slot); + card.dom.classList.remove('moving'); + }) + ); +} diff --git a/js/GUI/Screen/Game/State.js b/js/GUI/Screen/Game/State.js new file mode 100644 index 0000000..2b06147 --- /dev/null +++ b/js/GUI/Screen/Game/State.js @@ -0,0 +1,84 @@ +import Messaging; + +var dom = { + deck: document.getElementById('deck'), + rest: document.getElementById('rest'), + status: document.getElementById('status') +}; +var selected; +var sets = {}; +var state = { + game: null, + playing: false, + step: null, + month: null, + turnedCard: null, +}; + +return { + dom: dom, + getSelected: getSelected, + init: init, + matchingInRiver: matchingInRiver, + play: play, + select: select, + sets: sets, + state: state +}; + +function init() { + buildSets(); +} + +function buildSets() { + ['river', 'you', 'them'].forEach(function(id) { + var setDom = document.getElementById(id); + if(setDom.tagName.toLowerCase() == 'ul') { + sets[id] = {card: null, dom: setDom}; + } else { + sets[id] = {}; + for(var i = 0; i < setDom.children.length; i++) { + if(setDom.children[i].tagName.toLowerCase() == 'ul') { + var child = setDom.children[i].className; + sets[id][child] = {card: {}, dom: setDom.children[i]}; + } + } + } + }); +} + +function getSelected() { + return selected; +} + +function matchingInRiver(card) { + return Fun.mapFilter( + Fun.of(sets.river.card), + Fun.defined + )(Hanafuda.sameMonth(card).map(Fun.proj('name'))); +} + +function play(move) { + Messaging.send({ + tag: "Play", + move: move, + onGame: state.game + }); +} + +function showCandidates(card, yes) { + matchingInRiver(card).forEach( + function(riverCard) {riverCard.setCandidate(yes);} + ); +} + +function select(cardOrFalse) { + if(typeof cardOrFalse == 'object') { + selected = cardOrFalse; + } + selected.dom.classList.toggle('selected', !!cardOrFalse); + showCandidates(selected.value, !!cardOrFalse); + if(cardOrFalse === false) { + selected = null; + } +} diff --git a/js/Hanafuda.js b/js/Hanafuda.js index c312afa..f3eca26 100644 --- a/js/Hanafuda.js +++ b/js/Hanafuda.js @@ -41,6 +41,7 @@ var Card = Object.freeze( id: i, family: findFamily(name, i), flower: Math.floor(i / 4), + monthOffset: i % 4, name: name }; return o; @@ -51,7 +52,6 @@ return { Flower: Flower, Family: Family, Card: Card, - getValue: getValue, sameMonth: sameMonth }; @@ -67,11 +67,6 @@ function findFamily(name, i) { } } -function getValue(card) { - var first = 4 * card.flower; - return card.id - first; -} - function sameMonth(card) { var first = 4 * card.flower; return [0,1,2,3].map(function(i) {return Card[CardNames[first + i]];}); diff --git a/js/Main.js b/js/Main.js index 778b6e6..3c97537 100644 --- a/js/Main.js +++ b/js/Main.js @@ -1,24 +1,11 @@ -import I18n; -import * as Screen from GUI.Screen; import * as Login from GUI.Screen.Login; import * as Hall from GUI.Screen.Hall; import * as Game from GUI.Screen.Game; -import Save; var gamePath = window.location.pathname.match(/\/game\/([0-9A-Fa-f]+)/); if(gamePath) { - var gameState = Save.get('games.state.Game#' + gamePath[1]); - if(gameState != undefined) { - Game.init(gameState); - } else { - Screen.dialog({ - text: I18n.get('gameNotFound'), - answers: [ - {label: 'backToMain', action: function() {window.location = '..';}} - ] - }); - } + Game.init(gamePath[1]) } else { Login.init(); Hall.init(); diff --git a/js/Messaging.js b/js/Messaging.js index e61becd..ac7d049 100644 --- a/js/Messaging.js +++ b/js/Messaging.js @@ -9,8 +9,9 @@ var s = 1000; /* ms */ var keepAlivePeriod = 20; var reconnectDelay = 1; var routes = {callbacks: [], children: {}}; +var messagesQueue = []; var wsHandlers = { - open: [function() {on = true; reconnectDelay = 1}, ping], + open: [function() {on = true; reconnectDelay = 1}, catchUp, ping], close: [function() {on = false;}, reconnect] }; @@ -79,9 +80,19 @@ function messageListener(event) { }; function send(o) { - ws.send(JSON.stringify(o)); - o.direction = 'client > server'; - log(o); + if(isOn()) { + ws.send(JSON.stringify(o)); + o.direction = 'client > server'; + log(o); + } else { + messagesQueue.push(o); + } +} + +function catchUp() { + var messages = messagesQueue; + messagesQueue = []; + messagesQueue.forEach(send); } function log(message) { diff --git a/js/Translations.js b/js/Translations.js index 35e0021..4791679 100644 --- a/js/Translations.js +++ b/js/Translations.js @@ -28,6 +28,7 @@ return { return "This month's flower is the " + flower; }, noGames: "No games being played", + noNameToPlay: "Your name isn't set, how about choosing one first ?", notFound: "No one goes by that name", notYet: "Not yet", ok: "Ok", @@ -88,6 +89,7 @@ return { return "C'est le mois des " + flower; }, noGames: "Aucune partie en cours", + noNameToPlay: "Et si on commençait par aller choisir un nom pour jouer", notFound: "Personne ne s'appelle comme ça", notYet: "Pas pour l'instant", ok: "Ok", diff --git a/skin/game.css b/skin/game.css index d3fe2ff..96ba7f2 100644 --- a/skin/game.css +++ b/skin/game.css @@ -20,7 +20,7 @@ } #game .card { - background: url("/cards.jpg") no-repeat; + background: url("../skin/cards.jpg") no-repeat; background-size: 400% 1300%; display: inline-block; border-radius: 0.5em;