commit f625b9954f17759ee33b227c274230d80683aebc Author: Tissevert Date: Fri Jan 10 08:36:57 2020 +0100 Import code from hanafuda-webapp and adapt it for SJW diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3096c2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +game.js +index.js +skin.css diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5095c05 --- /dev/null +++ b/Makefile @@ -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) diff --git a/game.html b/game.html new file mode 100644 index 0000000..fc22982 --- /dev/null +++ b/game.html @@ -0,0 +1,39 @@ + + + + + KoiKoi + + + + +
+
+
    +
      +
        +
          +
            + +
            +
            +
              +
            • +
            +
              +
                +
                +
                +
                  +
                    +
                      +
                        +
                          + +
                          +
                          +
                          +
                          +

                          + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..ab74edd --- /dev/null +++ b/index.html @@ -0,0 +1,41 @@ + + + + + KoiKoi + + + + +
                          +

                          KoiKoi

                          +
                          + +

                          + + +

                          +
                          +
                          +
                          +
                          + +

                          + + +

                          +
                          + +
                            +
                            +
                            +
                            + +
                              +
                              +
                              +
                              +
                              +

                              + + diff --git a/js/GUI/ConnectedForm.js b/js/GUI/ConnectedForm.js new file mode 100644 index 0000000..0636338 --- /dev/null +++ b/js/GUI/ConnectedForm.js @@ -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(); + } +} diff --git a/js/GUI/ListSelector.js b/js/GUI/ListSelector.js new file mode 100644 index 0000000..829eede --- /dev/null +++ b/js/GUI/ListSelector.js @@ -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 +}; diff --git a/js/GUI/Screen.js b/js/GUI/Screen.js new file mode 100644 index 0000000..a1f24c6 --- /dev/null +++ b/js/GUI/Screen.js @@ -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"; +} diff --git a/js/GUI/Screen/Hall.js b/js/GUI/Screen/Hall.js new file mode 100644 index 0000000..efaf06c --- /dev/null +++ b/js/GUI/Screen/Hall.js @@ -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'); + } +} diff --git a/js/GUI/Screen/Login.js b/js/GUI/Screen/Login.js new file mode 100644 index 0000000..397576c --- /dev/null +++ b/js/GUI/Screen/Login.js @@ -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 != ""); +} diff --git a/js/I18n.js b/js/I18n.js new file mode 100644 index 0000000..6f70c13 --- /dev/null +++ b/js/I18n.js @@ -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+'" !!'); +} diff --git a/js/Messaging.js b/js/Messaging.js new file mode 100644 index 0000000..e61becd --- /dev/null +++ b/js/Messaging.js @@ -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); +} diff --git a/js/Room.js b/js/Room.js new file mode 100644 index 0000000..b385421 --- /dev/null +++ b/js/Room.js @@ -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); + }); +} diff --git a/js/Save.js b/js/Save.js new file mode 100644 index 0000000..49519d5 --- /dev/null +++ b/js/Save.js @@ -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)); +} diff --git a/js/Session.js b/js/Session.js new file mode 100644 index 0000000..acfea24 --- /dev/null +++ b/js/Session.js @@ -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); +} diff --git a/js/StatusHandler.js b/js/StatusHandler.js new file mode 100644 index 0000000..de36bef --- /dev/null +++ b/js/StatusHandler.js @@ -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; + } +} diff --git a/js/Table.js b/js/Table.js new file mode 100644 index 0000000..fb7e5d1 --- /dev/null +++ b/js/Table.js @@ -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 +} diff --git a/js/Translations.js b/js/Translations.js new file mode 100644 index 0000000..58bab12 --- /dev/null +++ b/js/Translations.js @@ -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 ?" + } +} diff --git a/skin/cards.jpg b/skin/cards.jpg new file mode 100644 index 0000000..48e142d Binary files /dev/null and b/skin/cards.jpg differ diff --git a/skin/game.css b/skin/game.css new file mode 100644 index 0000000..d3fe2ff --- /dev/null +++ b/skin/game.css @@ -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; +} diff --git a/skin/hall.css b/skin/hall.css new file mode 100644 index 0000000..ccb0f8f --- /dev/null +++ b/skin/hall.css @@ -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; +} diff --git a/skin/screen.css b/skin/screen.css new file mode 100644 index 0000000..9f239d8 --- /dev/null +++ b/skin/screen.css @@ -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; +}