Initial framework for an Empathy game
authorCarl Worth <cworth@cworth.org>
Sun, 7 Jun 2020 18:56:35 +0000 (11:56 -0700)
committerCarl Worth <cworth@cworth.org>
Sun, 7 Jun 2020 18:56:35 +0000 (11:56 -0700)
This starts with the generic code, (grabbed from Scribe), for
displaying the game INFO, (with a button for copying the link), and
the list of players that are in the game.

It also provides a form for submitting a category, but that is still
inert for now.

empathy/.gitignore [new file with mode: 0644]
empathy/Makefile [new file with mode: 0644]
empathy/empathy.css [new file with mode: 0644]
empathy/empathy.jsx [new file with mode: 0644]
empathy/index.html [new file with mode: 0644]

diff --git a/empathy/.gitignore b/empathy/.gitignore
new file mode 100644 (file)
index 0000000..053da87
--- /dev/null
@@ -0,0 +1 @@
+empathy.js
diff --git a/empathy/Makefile b/empathy/Makefile
new file mode 100644 (file)
index 0000000..9f07401
--- /dev/null
@@ -0,0 +1,12 @@
+# Defer all targets up to the upper-level
+#
+# This requires two recipes. The first to cover the case of no
+# explicit target specifed (so when invoked as "make" we call "make"
+# at the upper-level) and then a .DEFAULT recipe to pass any explicit
+# target up as well, (so that an invocation of "make foo" results in a
+# call to "make foo" above.
+all:
+       $(MAKE) -C ..
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/empathy/empathy.css b/empathy/empathy.css
new file mode 100644 (file)
index 0000000..e8a317e
--- /dev/null
@@ -0,0 +1 @@
+/* Nothing to see here yet. */
diff --git a/empathy/empathy.jsx b/empathy/empathy.jsx
new file mode 100644 (file)
index 0000000..9ebe9f3
--- /dev/null
@@ -0,0 +1,217 @@
+function undisplay(element) {
+  element.style.display="none";
+}
+
+function add_message(severity, message) {
+  message = `<div class="message ${severity}" onclick="undisplay(this)">
+<span class="hide-button" onclick="undisplay(this.parentElement)">&times;</span>
+${message}
+</div>`;
+  const message_area = document.getElementById('message-area');
+  message_area.insertAdjacentHTML('beforeend', message);
+}
+
+/*********************************************************
+ * Handling server-sent event stream                     *
+ *********************************************************/
+
+const events = new EventSource("events");
+
+events.onerror = function(event) {
+  if (event.target.readyState === EventSource.CLOSED) {
+    setTimeout(() => {
+      add_message("danger", "Connection to server lost.");
+    }, 1000);
+  }
+};
+
+events.addEventListener("game-info", event => {
+  const info = JSON.parse(event.data);
+
+  window.game.set_game_info(info);
+});
+
+events.addEventListener("player-info", event => {
+  const info = JSON.parse(event.data);
+
+  window.game.set_player_info(info);
+});
+
+events.addEventListener("player-enter", event => {
+  const info = JSON.parse(event.data);
+
+  window.game.set_other_player_info(info);
+});
+
+events.addEventListener("player-update", event => {
+  const info = JSON.parse(event.data);
+
+  if (info.id === window.game.state.player_info.id)
+    window.game.set_player_info(info);
+  else
+    window.game.set_other_player_info(info);
+});
+
+/*********************************************************
+ * Game and supporting classes                           *
+ *********************************************************/
+
+function copy_to_clipboard(id)
+{
+  const tmp = document.createElement("input");
+  tmp.setAttribute("value", document.getElementById(id).innerHTML);
+  document.body.appendChild(tmp);
+  tmp.select();
+  document.execCommand("copy");
+  document.body.removeChild(tmp);
+}
+
+function GameInfo(props) {
+  if (! props.id)
+    return null;
+
+  return (
+    <div className="game-info">
+      <span className="game-id">{props.id}</span>
+      {" "}
+      Share this link to invite friends:{" "}
+      <span id="game-share-url">{props.url}</span>
+      {" "}
+      <button
+        className="inline"
+        onClick={() => copy_to_clipboard('game-share-url')}
+      >Copy Link</button>
+    </div>
+  );
+}
+
+function PlayerInfo(props) {
+  if (! props.player.id)
+    return null;
+
+  return (
+    <div className="player-info">
+      <span className="players-header">Players: </span>
+      {props.player.name}
+      {props.other_players.map(other => (
+        <span key={other.id}>
+          {", "}
+          {other.name}
+        </span>
+      ))}
+    </div>
+  );
+}
+
+function fetch_method_json(method, api = '', data = {}) {
+  const response = fetch(api, {
+    method: method,
+    headers: {
+      'Content-Type': 'application/json'
+    },
+    body: JSON.stringify(data)
+  });
+  return response;
+}
+
+function fetch_post_json(api = '', data = {}) {
+  return fetch_method_json('POST', api, data);
+}
+
+async function fetch_put_json(api = '', data = {}) {
+  return fetch_method_json('PUT', api, data);
+}
+
+function CategoryRequest(props) {
+  return (
+    <div className="category-request">
+      <h2>Submit a Category</h2>
+      <p>
+          Suggest a category to play with your friends. Don't forget to
+          include the number of items for each person to submit.
+      </p>
+
+      <form>
+        <div className="form-field large">
+          <input
+            type="text"
+            id="category"
+            placeholder="6 things at the beach"
+            required pattern=".*[0-9]+.*"
+            title="Category must contain a number"
+          >
+          </input>
+        </div>
+
+        <div className="form-field large">
+          <button type="submit">
+            Send
+          </button>
+        </div>
+
+      </form>
+    </div>
+  );
+}
+
+class Game extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      game_info: {},
+      player_info: {},
+      other_players: [],
+    };
+  }
+
+  set_game_info(info) {
+    this.setState({
+      game_info: info
+    });
+  }
+
+  set_player_info(info) {
+    this.setState({
+      player_info: info
+    });
+  }
+
+  set_other_player_info(info) {
+    const other_players_copy = [...this.state.other_players];
+    const idx = other_players_copy.findIndex(o => o.id === info.id);
+    if (idx >= 0) {
+      other_players_copy[idx] = info;
+    } else {
+      other_players_copy.push(info);
+    }
+    this.setState({
+      other_players: other_players_copy
+    });
+  }
+
+  render() {
+    const state = this.state;
+
+    return [
+      <GameInfo
+        key="game-info"
+        id={state.game_info.id}
+        url={state.game_info.url}
+      />,
+      <PlayerInfo
+        key="player-info"
+        game={this}
+        player={state.player_info}
+        other_players={state.other_players}
+      />,
+      <p key="spacer"></p>,
+      <CategoryRequest
+        key="category-request"
+      />
+    ];
+  }
+}
+
+ReactDOM.render(<Game
+                  ref={(me) => window.game = me}
+                />, document.getElementById("empathy"));
diff --git a/empathy/index.html b/empathy/index.html
new file mode 100644 (file)
index 0000000..d250dc8
--- /dev/null
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.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="/lmno.js"></script>
+
+    <div id="page">
+
+      <h1>The Game of Empathy</h1>
+
+      <p>
+        You don't need to be right, you just need to agree with your
+        friends.
+      </p>
+
+      <div id="message-area">
+      </div>
+
+      <form onsubmit="lmno_new('empathy'); return false;">
+        <button type="submit">
+          Host a new game
+        </button>
+      </form>
+
+    </div>
+  </body>
+</html>