--- /dev/null
+PUBLISH_HOST=cworth.org
+PUBLISH_DIR=/srv/families.cworth.org/www
+
+DO_NOT_PUBLISH="Makefile"
+
+deploy:
+ rm -rf .deploy-source
+ git clone . .deploy-source
+ rm -rf .deploy-source/.git
+ (cd .deploy-source; rm $(DO_NOT_PUBLISH); rsync -avz ./ --delete --delete-after $(PUBLISH_HOST):$(PUBLISH_DIR) )
+ rm -rf .deploy-source
--- /dev/null
+const API = "https://families.cworth.org/api/";
+
+function undisplay(element) {
+ element.style.display="none";
+}
+
+function add_message(severity, message) {
+ message = `<div class="message ${severity}" onclick="undisplay(this)">
+${message}
+<span class="hide-button" onclick="undisplay(this.parentElement)">×</span>
+</div>`;
+ const message_area = document.getElementById('message-area');
+ message_area.insertAdjacentHTML('beforeend', message);
+}
+
+function register(form) {
+ var request = new XMLHttpRequest();
+
+ request.open("POST", API + "register");
+ request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
+ var data = {
+ "name": form.name.value,
+ "character": form.character.value
+ };
+ request.send(JSON.stringify(data));
+
+ form.reset();
+}
+
+function toggle_host_tools() {
+ const host_tools = document.getElementById("host-tools");
+
+ console.log("Toggling, host_tools.style.display is '" + host_tools.style.display + "'");
+
+ if (host_tools.style.display === "block")
+ host_tools.style.display = "none";
+ else
+ host_tools.style.display = "block";
+}
+
+function post_reveal() {
+ const request = new XMLHttpRequest();
+
+ request.open("POST", API + "reveal");
+ request.send();
+}
+
+function post_start() {
+ const request = new XMLHttpRequest();
+
+ request.open("POST", API + "start");
+ request.send();
+}
+
+function post_reset() {
+ const request = new XMLHttpRequest();
+
+ request.open("POST", API + "reset");
+ request.send();
+}
+
+const events = new EventSource(API + "events");
+
+events.onerror = function(event) {
+ if (event.target.readyState === EventSource.CLOSED) {
+ add_message("danger", "Connection to server lost.");
+ }
+};
+
+events.addEventListener("players", function(event) {
+ const players_div = document.getElementById("players-div");
+ const players_element = document.getElementById("players");
+ const players = JSON.parse(event.data);
+
+ players_element.innerHTML = '';
+ for (const player of players) {
+ var li = document.createElement('li');
+ li.id = "player-" + player.id;
+ li.innerText = player.name;
+ players_element.appendChild(li);
+ }
+
+ /* Force players list to be visible. */
+ players_div.style.display = "block";
+});
+
+events.addEventListener("player-join", function(event) {
+ const players_div = document.getElementById("players-div");
+ const players = document.getElementById("players");
+ const player = JSON.parse(event.data);
+
+ const li = document.createElement('li');
+ li.id = "player-" + player.id;
+ li.innerText = player.name;
+ players.appendChild(li);
+
+ /* Force players list to be visible. */
+ players_div.style.display = "block";
+});
+
+events.addEventListener("player-leave", function(event) {
+ const players = document.getElementById("players");
+ const id = JSON.parse(event.data);
+ const player = document.getElementById("player-" + id.id);
+
+ add_message("info", player.innerText + " has left the game.");
+ players.removeChild(player);
+});
+
+events.addEventListener("game-state", function(event) {
+ const data = JSON.parse(event.data);
+ const old_state = data.old_state;
+ const new_state = data.new_state;
+
+ const hide_selector = ".show-state-" +old_state+ ",.hide-state-" +new_state;
+ const show_selector = ".hide-state-" +old_state+ ",.show-state-" +new_state;
+
+ /* Hide all elements based on the state transition. */
+ var elts = document.querySelectorAll(hide_selector);
+ for (const elt of elts) {
+ elt.style.display = "none";
+ }
+
+ /* And show all elements based on the same state transition. */
+ elts = document.querySelectorAll(show_selector);
+ for (const elt of elts) {
+ elt.style.display = "block";
+ }
+});
+
+events.addEventListener("character-reveal", function(event) {
+ const data = JSON.parse(event.data);
+ const character_name = data.character;
+
+ const character = document.getElementById("character-reveal");
+
+ character.innerText = character_name;
+});
+
+events.addEventListener("capture", function(event_string) {
+ const players = document.getElementById("players");
+ const event = JSON.parse(event_string.data);
+ const player = document.getElementById("player-" + event.captee);
+
+ players.removeChild(player);
+});
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;" />
+
+ <title>The Game of Empires</title>
+
+ <link rel="stylesheet" href="reset.css" type="text/css" />
+ <link rel="stylesheet" href="style.css" type="text/css" />
+ </head>
+<body>
+
+ <script src="empires-client.js"></script>
+
+<div id="page">
+
+ <span style="float:right;cursor:pointer" onclick="toggle_host_tools()">
+ ⚙
+ </span>
+
+ <div id="host-tools">
+ <button onclick="post_reveal()">
+ Reveal Characters
+ </button>
+ <button onclick="post_start()">
+ Start Game
+ </button>
+ <button onclick="post_reset()">
+ Reset Game
+ </button>
+ </div>
+
+ <div id="message-area">
+ </div>
+
+ <div id="loading" class="show-state-none">
+ <h1>The Game of Empires</h1>
+
+ <p>
+ Contacting server. Please wait...
+ </p>
+ </div>
+
+ <div id="pre-game" class="show-state-join">
+
+ <h1>The Game of Empires</h1>
+
+ <p>
+ To join the game, type your own name below. Also, choose the name
+ of a character that you want to play as. This can be anyone (real
+ or fictional) that everyone playing the game would be likely to
+ know, (for example "Albert Einstein" or "Harry Potter").
+ </p>
+
+ <p>
+ Note: After you have joined the game, another player can use this
+ same device to join the game as well.
+ </p>
+
+ <!-- The return false prevents the page from being reloaded -->
+ <form id="register-form" onsubmit="register(this); return false">
+ <div class="form-row medium left">
+ <label for="name">Your name</label>
+ <input type="text" id="name" required>
+ </div>
+
+ <div class="form-row medium right">
+ <label for="character">Character name</label>
+ <input type="text" id="character" autocomplete="off" required>
+ </div>
+
+ <div class="form-row large">
+ <button type="submit">
+ Join game
+ </button>
+ </div>
+ </form>
+
+ </div>
+
+ <div class="show-state-reveal">
+ <h1>Watch and memorize each character!</h1>
+
+ <span id="character-reveal"></span>
+ </div>
+
+ <div class="hide-state-reveal" id="players-div">
+ <h1>Players in the game</h1>
+
+ <ul id="players">
+ </ul>
+
+ </div>
+
+</div>
+</body>
+</html>
--- /dev/null
+/* http://meyerweb.com/eric/tools/css/reset/
+ v2.0 | 20110126
+ License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+body {
+ line-height: 1;
+}
+ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
--- /dev/null
+body {
+ background-color: #333738;
+ font-family: sans-serif;
+ line-height: 1.5;
+}
+
+#page {
+ margin-top: 36px;
+ margin-bottom: 36px;
+ padding-top: 30px;
+ padding-bottom: 30px;
+ padding-left: 10px;
+ padding-right: 10px;
+ background-color: white;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* For a wide screen, just use the center of the window for content. */
+@media screen and (min-width: 500px) {
+ #page {
+ max-width: 720px;
+ padding-left: 50px;
+ padding-right: 50px;
+ }
+}
+
+/* And when the screen is narrow, let's kill the wasted vertical space
+ above it too. */
+@media screen and (max-width: 499px) {
+ body {
+ background-color: white;
+ }
+ #page {
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+}
+
+h1 {
+ color: #333738;
+ font-size: 150%;
+ font-weight: bold;
+}
+
+h2 {
+ color: #333738;
+ font-size: 110%;
+ font-weight: bold;
+}
+
+p,dl,dd {
+ margin-bottom: 1em;
+}
+
+/* Default message severity is "info" but can be overriden. */
+.message {
+ padding: 1em;
+ background-color: #44c7ef;
+ color: white;
+ transition: 0.3s;
+ margin-bottom: 0.5em;
+ font-weight: bold;
+}
+
+.success {
+ background-color: #44c7ef;
+}
+
+.warning {
+ background-color: #ffa92a;
+}
+
+.danger {
+ background-color: #f56257
+}
+
+.hide-button {
+ color: white;
+ font-weight: bold;
+ float: right;
+ font-size: 150%;
+ cursor: pointer;
+}
+
+.hide-button:hover {
+ color: black;
+}
+
+.show-state-join {
+ display:none;
+}
+
+.show-state-reveal {
+ display:none;
+}
+
+.show-state-capture {
+ display:none;
+}
+
+.hide-state-join {
+ display:block;
+}
+
+.hide-state-reveal {
+ display:block;
+}
+
+.hide-state-capture {
+ display:block;
+}
+
+/* Players list starts out hidden (until a player is added). */
+#players-div {
+ display:none;
+}
+
+/* Host tools start out hidden. */
+#host-tools {
+ display: none;
+}
+
+#register-form {
+ max-width: 100%;
+ display: grid;
+ grid-template-columns: 49% 49%;
+ grid-column-gap: 2%;
+}
+
+.form-row {
+ max-width: 100%;
+ padding-bottom: 0.25em;
+}
+
+.form-row.medium.left {
+ grid-column-start: 1;
+}
+
+.form-row.medium.right {
+ grid-column-start: 2;
+}
+
+.form-row.large {
+ grid-column-start: 1;
+ grid-column-end: span 2;
+}
+
+/* For a narrow screen, use a single-column for medium form rows. */
+@media screen and (max-width: 600px) {
+ .form-row.medium.left,.form-row.medium.right {
+ grid-column-start: 1;
+ grid-column-end: span 2
+ }
+}
+
+input {
+ width: 100%;
+}
+
+input {
+ box-sizing: border-box;
+
+ height: 40px;
+
+ padding: 10px 12px;
+
+ border: 1px solid #287789;
+ border-radius: 4px;
+}
+
+input:focus {
+ border: 2px solid #44c7ef;
+}
+
+button {
+ display: inline-block;
+ border-radius: 4px;
+ background-color: #287789;
+ border: none;
+ color: white;
+ text-align: center;
+ font-size: 125%;
+ margin-top: .25em;
+ padding-top: 0.25em;
+ padding-bottom: 0.25em;
+ width: 200px;
+ transition: all 150ms ease;
+}
+
+button:hover {
+ transform: translateY(-1px);
+ background-color: #44c7ef;
+}
+
+:focus {
+ outline: none;
+}
+::-moz-focus-inner {
+ border: 0;
+}
+
+#character-reveal {
+ color: #333738;
+ font-size: 300%;
+ font-weight: bold;
+}