1 function team_symbol(team) {
8 function undisplay(element) {
9 element.style.display="none";
12 function add_message(severity, message) {
13 message = `<div class="message ${severity}" onclick="undisplay(this)">
14 <span class="hide-button" onclick="undisplay(this.parentElement)">×</span>
17 const message_area = document.getElementById('message-area');
18 message_area.insertAdjacentHTML('beforeend', message);
21 /*********************************************************
22 * Handling server-sent event stream *
23 *********************************************************/
25 const events = new EventSource("events");
27 events.onerror = function(event) {
28 if (event.target.readyState === EventSource.CLOSED) {
30 add_message("danger", "Connection to server lost.");
35 events.addEventListener("game-info", event => {
36 const info = JSON.parse(event.data);
38 window.game.set_game_info(info);
41 events.addEventListener("player-info", event => {
42 const info = JSON.parse(event.data);
44 window.game.set_player_info(info);
47 events.addEventListener("player-enter", event => {
48 const info = JSON.parse(event.data);
50 window.game.set_other_player_info(info);
53 events.addEventListener("player-update", event => {
54 const info = JSON.parse(event.data);
56 if (info.id === window.game.state.player_info.id)
57 window.game.set_player_info(info);
59 window.game.set_other_player_info(info);
62 events.addEventListener("move", event => {
63 const move = JSON.parse(event.data);
65 window.game.receive_move(move);
68 events.addEventListener("game-state", event => {
69 const state = JSON.parse(event.data);
71 window.game.reset_board();
73 for (let square of state.moves) {
74 window.game.receive_move(square);
78 /*********************************************************
79 * Game and supporting classes *
80 *********************************************************/
82 const scribe_glyphs = [
199 function copy_to_clipboard(id)
201 const tmp = document.createElement("input");
202 tmp.setAttribute("value", document.getElementById(id).innerHTML);
203 document.body.appendChild(tmp);
205 document.execCommand("copy");
206 document.body.removeChild(tmp);
209 function GameInfo(props) {
214 <div className="game-info">
215 <span className="game-id">{props.id}</span>
217 Share this link to invite a friend:{" "}
218 <span id="game-share-url">{props.url}</span>
222 onClick={() => copy_to_clipboard('game-share-url')}
228 function TeamButton(props) {
229 return <button className="inline"
230 onClick={() => props.game.join_team(props.team)}>
235 function TeamChoices(props) {
237 if (props.player.team === "+")
242 if (props.player.team === "") {
243 if (props.first_move) {
247 <TeamButton key="+" game={props.game} team="+" label="Join 🞥" />,
249 <TeamButton key="o" game={props.game} team="o" label="Join 🞇" />
253 return <TeamButton game={props.game} team={other_team} label="Switch" />;
257 function PlayerInfo(props) {
258 if (! props.player.id)
261 const choices = <TeamChoices
263 first_move={props.first_move}
264 player={props.player}
268 <div className="player-info">
269 <span className="players-header">Players: </span>
271 {props.player.team ? ` (${props.player.team})` : ""}
272 {props.first_move ? "" : " "}
274 {props.other_players.map(other => (
275 <span key={other.id}>
278 {other.team ? ` (${other.team})` : ""}
285 function Glyph(props) {
287 const glyph_dots = [];
290 for (let i = 0; i < 9; i++) {
291 if (props.squares[i])
295 const height = Math.floor(20 * (Math.floor(last_square / 3) + 1));
297 const viewbox=`0 0 60 ${height}`;
299 for (let row = 0; row < 3; row++) {
300 for (let col = 0; col < 3; col++) {
301 if (props.squares[3 * row + col]) {
302 let cy = 10 + 20 * row;
303 let cx = 10 + 20 * col;
316 return (<div className="glyph-and-name">
318 <div className="glyph">
319 <svg viewBox={viewbox}>
329 function Square(props) {
330 let className = "square";
333 className += " occupied";
334 } else if (props.active) {
335 className += " open";
338 if (props.last_move) {
339 className += " last-move";
342 const onClick = props.active ? props.onClick : null;
345 <div className={className}
352 function MiniGrid(props) {
353 function grid_square(j) {
354 const value = props.squares[j];
355 const last_move = props.last_moves.includes(j);
359 active={props.active}
360 last_move={last_move}
361 onClick={() => props.onClick(j)}
366 /* Even if my parent thinks I'm active because of the last move, I
367 * might not _really_ be active if I'm full. */
369 props.squares.forEach(element => {
374 let class_name = "mini-grid";
375 if (props.active && occupied < 9)
376 class_name += " active";
379 <div className={class_name}>
393 class Board extends React.Component {
395 /* This mini grid is active only if both:
397 * 1. It is our turn (this.props.active === true)
399 * 2. One of the following conditions is met:
401 * a. This is this players first turn (last_two_moves[0] === null)
402 * b. This mini grid corresponds to this players last turn
403 * c. The mini grid that corresponds to the players last turn is full
405 let grid_active = false;
406 if (this.props.active) {
408 if (this.props.last_two_moves.length > 1) {
409 /* First index (0) gives us our last move, (that is, of the
410 * last two moves, it's the first one, so two moves ago).
412 * Second index (1) gives us the second number from that move,
413 * (that is, the index within the mini-grid that we last
416 const target = this.props.last_two_moves[0][1];
418 this.props.squares[target].forEach(element => {
422 /* If the target mini-grid isn't full then this grid is
423 * only active if it is that target. */
425 grid_active = (i === target);
429 /* We want to highlight each of the last two moves (both "+" and
430 * "o"). So we filter the last two moves that have a first index
431 * that matches this mini_grid and pass down their second index
434 const last_moves = this.props.last_two_moves.filter(move => move[0] === i)
435 .map(move => move[1]);
437 const squares = this.props.squares[i];
442 last_moves={last_moves}
443 onClick={(j) => this.props.onClick(i,j)}
450 <div className="board-container">
451 <div className="board">
467 function fetch_method_json(method, api = '', data = {}) {
468 const response = fetch(api, {
471 'Content-Type': 'application/json'
473 body: JSON.stringify(data)
478 function fetch_post_json(api = '', data = {}) {
479 return fetch_method_json('POST', api, data);
482 async function fetch_put_json(api = '', data = {}) {
483 return fetch_method_json('PUT', api, data);
486 class Game extends React.Component {
493 squares: [...Array(9)].map(() => Array(9).fill(null)),
499 set_game_info(info) {
505 set_player_info(info) {
511 set_other_player_info(info) {
512 const other_players_copy = [...this.state.other_players];
513 const idx = other_players_copy.findIndex(o => o.id === info.id);
515 other_players_copy[idx] = info;
517 other_players_copy.push(info);
520 other_players: other_players_copy
531 if (this.state.moves.length === 81) {
534 const symbol = team_symbol(this.state.next_to_play);
535 const new_squares = this.state.squares.map(arr => arr.slice());
536 new_squares[move[0]][move[1]] = symbol;
537 const new_moves = [...this.state.moves, move];
539 if (this.state.next_to_play === "+")
544 squares: new_squares,
546 next_to_play: next_to_play
550 async handle_click(i, j, first_move) {
555 move.assert_first_move = true;
557 const response = await fetch_post_json("move", move);
558 if (response.status == 200) {
559 const result = await response.json();
561 add_message("danger", result.message);
563 add_message("danger", `Error occurred sending move`);
568 fetch_put_json("player", {team: team});
572 const state = this.state;
573 const first_move = state.moves.length === 0;
574 const my_team = state.player_info.team;
578 if (this.state.moves.length === 81)
580 status = "Game over";
581 board_active = false;
585 if (state.other_players.length == 0) {
586 status = "You can move or wait for another player to join.";
589 if (state.other_players.length == 1) {
590 qualifier = "Either";
594 status = `${qualifier} player can make the first move.`;
598 else if (my_team === "")
600 status = "You're just watching the game.";
601 board_active = false;
603 else if (my_team === state.next_to_play)
605 status = "Your turn. Make a move.";
610 status = "Waiting for another player to ";
611 if (state.other_players.length == 0) {
616 board_active = false;
622 id={state.game_info.id}
623 url={state.game_info.url}
628 first_move={first_move}
629 player={state.player_info}
630 other_players={state.other_players}
632 <div key="game" className="game">
634 <div className="game-board">
636 active={board_active}
637 squares={state.squares}
638 last_two_moves={state.moves.slice(-2)}
639 onClick={(i,j) => this.handle_click(i, j, first_move)}
643 <div key="glyphs" className="glyphs">
645 scribe_glyphs.map(glyph => {
650 squares={glyph.squares}
660 ReactDOM.render(<Game
661 ref={(me) => window.game = me}
662 />, document.getElementById("scribe"));