+ Select any categories below that you'd like to play.
+ You can choose as many as you'd like.
+
+ {props.prompts.map(
+ prompt =>
+ )}
+
+ );
+});
+
+const LetsPlay = React.memo(props => {
+
+ const quorum = Math.max(0, props.num_players - props.prompts.length);
+ const max_votes = props.prompts.reduce(
+ (max_so_far, v) => Math.max(max_so_far, v.votes.length), 0);
+
+ if (max_votes < quorum) {
+ let text = `Before we play, we should collect a bit
+ more information about what category would
+ be interesting for this group. So, either
+ type a new category option above, or else`;
+ if (props.prompts.length) {
+ if (props.prompts.length > 1)
+ text += " vote on some of the categories below.";
+ else
+ text += " vote on the category below.";
+ } else {
+ text += " wait for others to submit, and then vote on them below.";
+ }
+
+ return (
+
+ Click/tap on each pair of answers that should be scored as equivalent,
+ (or click a word twice to split it out from a group). Remember,
+ what goes around comes around, so it's best to be generous when
+ judging.
+
+
+ Also, for an especially fun or witty answer, you can give kudos
+ by clicking the star on the right. You may only do this for one
+ word/group.
+
+ The following players have submitted their answers:{' '}
+ {[...this.props.players_answered].join(', ')}
+
+ {still_waiting}
+ {move_on_button}
+
+
+ );
+ }
+
+ return (
+
+
The Game of Empathy
+
+ Remember, you're trying to match your answers with
+ what the other players submit.
+ Give {this.props.prompt.items} answer
+ {this.props.prompt.items > 1 ? 's' : ''} for the following prompt:
+
+
{this.props.prompt.prompt}
+
+
+ );
+ }
+}
+
+class Game extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ game_info: {},
+ player_info: {},
+ other_players: [],
+ prompts: [],
+ active_prompt: null,
+ players_answered: new Set(),
+ players_answering: {},
+ answering_idle: false,
+ end_answers_votes: new Set(),
+ ambiguities: null,
+ players_judged: new Set(),
+ players_judging: {},
+ judging_idle: false,
+ end_judging_votes: new Set(),
+ scores: null,
+ new_game_votes: new Set(),
+ ready: false
+ };
+ }
+
+ 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
+ });
+ }
+
+ disable_player(info) {
+ const idx = this.state.other_players.findIndex(o => o.id === info.id);
+ if (idx < 0)
+ return;
+
+ const other_players_copy = [...this.state.other_players];
+ other_players_copy[idx].active = false;
+
+ this.setState({
+ other_players: other_players_copy
+ });
+ }
+
+ reset_game_state() {
+ this.setState({
+ prompts: [],
+ active_prompt: null,
+ players_answered: new Set(),
+ players_answering: {},
+ answering_idle: false,
+ end_answers_votes: new Set(),
+ ambiguities: null,
+ players_judged: new Set(),
+ players_judging: {},
+ judging_idle: false,
+ end_judging_votes: new Set(),
+ scores: null,
+ new_game_votes: new Set(),
+ ready: false
+ });
+ }
+
+ set_prompts(prompts) {
+ this.setState({
+ prompts: prompts
+ });
+ }
+
+ add_or_update_prompt(prompt) {
+ const prompts_copy = [...this.state.prompts];
+ const idx = prompts_copy.findIndex(p => p.id === prompt.id);
+ if (idx >= 0) {
+ prompts_copy[idx] = prompt;
+ } else {
+ prompts_copy.push(prompt);
+ }
+ this.setState({
+ prompts: prompts_copy
+ });
+ }
+
+ set_active_prompt(prompt) {
+ this.setState({
+ active_prompt: prompt
+ });
+ }
+
+ set_players_answered(players) {
+ this.setState({
+ players_answered: new Set(players)
+ });
+ }
+
+ set_player_answered(player) {
+ const new_players_answering = {...this.state.players_answering};
+ delete new_players_answering[player];
+
+ this.setState({
+ players_answered: new Set([...this.state.players_answered, player]),
+ players_answering: new_players_answering
+ });
+ }
+
+ set_players_answering(players) {
+ const players_answering = {};
+ for (let player of players) {
+ players_answering[player] = {active: false};
+ }
+ this.setState({
+ players_answering: players_answering
+ });
+ }
+
+ set_player_answering(player) {
+ /* Set the player as actively answering now. */
+ this.setState({
+ players_answering: {
+ ...this.state.players_answering,
+ [player]: {active: true}
+ }
+ });
+ /* And arrange to have them marked idle very shortly.
+ *
+ * Note: This timeout is intentionally very, very short. We only
+ * need it long enough that the browser has latched onto the state
+ * change to "active" above. We actually use a CSS transition
+ * delay to control the user-perceptible length of time after
+ * which an active player appears inactive.
+ */
+ setTimeout(() => {
+ this.setState({
+ players_answering: {
+ ...this.state.players_answering,
+ [player]: {active: false}
+ }
+ });
+ }, 100);
+ }
+
+ set_answering_idle(value) {
+ this.setState({
+ answering_idle: value
+ });
+ }
+
+ set_end_answers(players) {
+ this.setState({
+ end_answers_votes: new Set(players)
+ });
+ }
+
+ set_player_vote_end_answers(player) {
+ this.setState({
+ end_answers_votes: new Set([...this.state.end_answers_votes, player])
+ });
+ }
+
+ set_player_unvote_end_answers(player) {
+ this.setState({
+ end_answers_votes: new Set([...this.state.end_answers_votes].filter(p => p !== player))
+ });
+ }
+
+ set_ambiguities(ambiguities) {
+ this.setState({
+ ambiguities: ambiguities
+ });
+ }
+
+ set_players_judged(players) {
+ this.setState({
+ players_judged: new Set(players)
+ });
+ }
+
+ set_player_judged(player) {
+ const new_players_judging = {...this.state.players_judging};
+ delete new_players_judging[player];
+
+ this.setState({
+ players_judged: new Set([...this.state.players_judged, player]),
+ players_judging: new_players_judging
+ });
+ }
+
+ set_players_judging(players) {
+ const players_judging = {};
+ for (let player of players) {
+ players_judging[player] = {active: false};
+ }
+ this.setState({
+ players_judging: players_judging
+ });
+ }
+
+ set_player_judging(player) {
+ /* Set the player as actively judging now. */
+ this.setState({
+ players_judging: {
+ ...this.state.players_judging,
+ [player]: {active: true}
+ }
+ });
+ /* And arrange to have them marked idle very shortly.
+ *
+ * Note: This timeout is intentionally very, very short. We only
+ * need it long enough that the browser has latched onto the state
+ * change to "active" above. We actually use a CSS transition
+ * delay to control the user-perceptible length of time after
+ * which an active player appears inactive.
+ */
+ setTimeout(() => {
+ this.setState({
+ players_judging: {
+ ...this.state.players_judging,
+ [player]: {active: false}
+ }
+ });
+ }, 100);
+
+ }
+
+ set_judging_idle(value) {
+ this.setState({
+ judging_idle: value
+ });
+ }
+
+ set_end_judging(players) {
+ this.setState({
+ end_judging_votes: new Set(players)
+ });
+ }
+
+ set_player_vote_end_judging(player) {
+ this.setState({
+ end_judging_votes: new Set([...this.state.end_judging_votes, player])
+ });
+ }
+
+ set_player_unvote_end_judging(player) {
+ this.setState({
+ end_judging_votes: new Set([...this.state.end_judging_votes].filter(p => p !== player))
+ });
+ }
+
+ set_scores(scores) {
+ this.setState({
+ scores: scores
+ });
+ }
+
+ set_new_game_votes(players) {
+ this.setState({
+ new_game_votes: new Set(players)
+ });
+ }
+
+ set_player_vote_new_game(player) {
+ this.setState({
+ new_game_votes: new Set([...this.state.new_game_votes, player])
+ });
+ }
+
+ set_player_unvote_new_game(player) {
+ this.setState({
+ new_game_votes: new Set([...this.state.new_game_votes].filter(p => p !== player))
+ });
+ }
+
+ state_ready() {
+ this.setState({
+ ready: true
+ });
+ }
+
+ render() {
+ const state = this.state;
+
+ if (state.scores) {
+
+ const players_total = state.players_answered.size;
+
+ let perfect_score = 0;
+ for (let i = 0;
+ i < state.active_prompt.items &&
+ i < state.scores.words.length;
+ i++)
+ {
+ perfect_score += state.scores.words[i].players.length;
+ }
+
+ return (
+
+ You don't need to be right, you just need to agree with your
+ friends.
+
+
+
+
+
+
+
+
+
+
diff --git a/empires/game.css b/empires/game.css
index 57cd206..bc3340b 100644
--- a/empires/game.css
+++ b/empires/game.css
@@ -1,28 +1,28 @@
/* By default, hide things that are not to be shown
- * until a particular game state is reached. */
-.show-state-join {
+ * until a particular game phase is reached. */
+.show-phase-join {
display:none;
}
-.show-state-reveal {
+.show-phase-reveal {
display:none;
}
-.show-state-capture {
+.show-phase-capture {
display:none;
}
/* And by default, show things that will be hidden
- * when a particular game state is reached. */
-.hide-state-join {
+ * when a particular game phase is reached. */
+.hide-phase-join {
display:block;
}
-.hide-state-reveal {
+.hide-phase-reveal {
display:block;
}
-.hide-state-capture {
+.hide-phase-capture {
display:block;
}
diff --git a/empires/game.js b/empires/game.js
index 8043d32..05b801e 100644
--- a/empires/game.js
+++ b/empires/game.js
@@ -75,8 +75,6 @@ function register(form) {
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
@@ -195,28 +193,28 @@ function spectator_on_load() {
state.spectator_id = JSON.parse(this.response);
}
-events.addEventListener("game-state", function(event) {
+events.addEventListener("game-phase", function(event) {
const data = JSON.parse(event.data);
- const old_state = data.old_state;
- const new_state = data.new_state;
+ const old_phase = data.old_phase;
+ const new_phase = data.new_phase;
- const hide_selector = ".show-state-" +old_state+ ",.hide-state-" +new_state;
- const show_selector = ".hide-state-" +old_state+ ",.show-state-" +new_state;
+ const hide_selector = ".show-phase-" +old_phase+ ",.hide-phase-" +new_phase;
+ const show_selector = ".hide-phase-" +old_phase+ ",.show-phase-" +new_phase;
- /* Hide all elements based on the state transition. */
+ /* Hide all elements based on the phase 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. */
+ /* And show all elements based on the same phase transition. */
elts = document.querySelectorAll(show_selector);
for (const elt of elts) {
elt.style.display = "block";
}
- /* Whenever the game enters the "join" state, add ourselves as a spectator. */
- if (new_state === "join") {
+ /* Whenever the game enters the "join" phase, add ourselves as a spectator. */
+ if (new_phase === "join") {
const request = new XMLHttpRequest();
request.addEventListener("load", spectator_on_load);
diff --git a/index.html b/index.html
index 261234a..ec4fe03 100644
--- a/index.html
+++ b/index.html
@@ -58,6 +58,9 @@
diff --git a/scribe/.gitignore b/scribe/.gitignore
new file mode 100644
index 0000000..953891d
--- /dev/null
+++ b/scribe/.gitignore
@@ -0,0 +1 @@
+scribe.js
diff --git a/scribe/Makefile b/scribe/Makefile
new file mode 100644
index 0000000..9f07401
--- /dev/null
+++ b/scribe/Makefile
@@ -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/scribe/index.html b/scribe/index.html
new file mode 100644
index 0000000..7638db9
--- /dev/null
+++ b/scribe/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+ Scribe
+
+
+
+
+
+
+
+
+
+
+
Scribe
+
+
+ A game
+ by Mark
+ Steere, implemented by permission.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scribe/scribe.css b/scribe/scribe.css
new file mode 100644
index 0000000..aba532e
--- /dev/null
+++ b/scribe/scribe.css
@@ -0,0 +1,103 @@
+/* We want our board to be the largest square that can
+ * fit. Unfortunately, it requires a bit of CSS magic to make that
+ * happen. We can set a width easily enough, but what we can't easily
+ * do is to set the height to be exactly the same as the width.
+ *
+ * So here's the magic to get that to happen. On the board container
+ * we set the height to 0 and the bottom padding to 100% (which just
+ * happens to be defined as relative to the _width_). So, now we have
+ * a square element. Hurrah!
+ *
+ * The problem is that this element has a nominal height of 0, so if
+ * any child sas "height: 100%" that will result in a 0-height child
+ * and won't be what we want.
+ *
+ * So the last piece of the magic is to use absolute placement of the
+ * board (which requires position:relative on its parent) and set all
+ * of its edges (top, left, bottom, right) to the extents of the
+ * container.
+ *
+ * Ta-da! Now our board element is square and does not have any
+ * dimensions of 0 so child elements can compute their sizes
+ * naturally.
+ */
+.board-container {
+ position: relative;
+ width: 100%;
+ height: 0;
+ padding-bottom: 100%;
+}
+
+.board {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-template-rows: 1fr 1fr 1fr;
+ grid-gap: 1em;
+}
+
+.mini-grid {
+ width: 100%;
+ height: 100%;
+
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-template-rows: 1fr 1fr 1fr;
+
+ border-radius: 6px;
+ border: 3px solid #999;
+}
+
+.mini-grid.active {
+ border: 3px solid var(--accent-color-bright);
+}
+
+.square {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: calc(min(8vw, .08 * var(--page-max-width)));
+ line-height: 0;
+ font-weight: bold;
+ border-bottom: 1px solid #999;
+ border-right: 1px solid #999;
+}
+
+.square.open {
+ cursor: pointer;
+}
+
+.square.occupied {
+ cursor: default;
+}
+
+.square.open:hover {
+ background-color: var(--accent-color-bright);
+}
+
+.square.last-move {
+ color: var(--accent-color-bright);
+}
+
+.glyphs {
+ padding-top: 0.5em;
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
+ grid-column-gap: 0.5em;
+ grid-row-gap: 0.25em;
+}
+
+.glyph-and-name {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.glyph {
+ width: 8vw;
+}
diff --git a/scribe/scribe.jsx b/scribe/scribe.jsx
new file mode 100644
index 0000000..ae05690
--- /dev/null
+++ b/scribe/scribe.jsx
@@ -0,0 +1,667 @@
+function team_symbol(team) {
+ if (team === "+")
+ return "+";
+ else
+ return "o";
+}
+
+function undisplay(element) {
+ element.style.display="none";
+}
+
+function add_message(severity, message) {
+ message = `
+ );
+}
+
+function MiniGrid(props) {
+ function grid_square(j) {
+ const value = props.squares[j];
+ const last_move = props.last_moves.includes(j);
+
+ /* Even if the grid is active, the square is only active if
+ * unoccupied. */
+ const square_active = (props.active && (value === null));
+
+ return (
+ props.onClick(j)}
+ />
+ );
+ }
+
+ /* Even if my parent thinks I'm active because of the last move, I
+ * might not _really_ be active if I'm full. */
+ let occupied = 0;
+ props.squares.forEach(element => {
+ if (element)
+ occupied++;
+ });
+
+ let class_name = "mini-grid";
+ if (props.active && occupied < 9)
+ class_name += " active";
+
+ return (
+
+ );
+}
+
+class Board extends React.Component {
+ mini_grid(i) {
+ /* This mini grid is active only if both:
+ *
+ * 1. It is our turn (this.props.active === true)
+ *
+ * 2. One of the following conditions is met:
+ *
+ * a. This is this players first turn (last_two_moves[0] === null)
+ * b. This mini grid corresponds to this players last turn
+ * c. The mini grid that corresponds to the players last turn is full
+ */
+ let grid_active = false;
+ if (this.props.active) {
+ grid_active = true;
+ if (this.props.last_two_moves.length > 1) {
+ /* First index (0) gives us our last move, (that is, of the
+ * last two moves, it's the first one, so two moves ago).
+ *
+ * Second index (1) gives us the second number from that move,
+ * (that is, the index within the mini-grid that we last
+ * played).
+ */
+ const target = this.props.last_two_moves[0][1];
+ let occupied = 0;
+ this.props.squares[target].forEach(element => {
+ if (element)
+ occupied++;
+ });
+ /* If the target mini-grid isn't full then this grid is
+ * only active if it is that target. */
+ if (occupied < 9)
+ grid_active = (i === target);
+ }
+ }
+
+ /* We want to highlight each of the last two moves (both "+" and
+ * "o"). So we filter the last two moves that have a first index
+ * that matches this mini_grid and pass down their second index
+ * be highlighted.
+ */
+ const last_moves = this.props.last_two_moves.filter(move => move[0] === i)
+ .map(move => move[1]);
+
+ const squares = this.props.squares[i];
+ return (
+ this.props.onClick(i,j)}
+ />
+ );
+ }
+
+ render() {
+ return (
+