--- /dev/null
+function team_symbol(team) {
+ if (team === "+")
+ return "🞥";
+ else
+ return "🞇";
+}
+
+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)">×</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) {
+ add_message("danger", "Connection to server lost.");
+ }
+};
+
+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);
+});
+
+events.addEventListener("move", event => {
+ const move = JSON.parse(event.data);
+
+ window.game.receive_move(move);
+});
+
+events.addEventListener("game-state", event => {
+ const state = JSON.parse(event.data);
+
+ window.game.reset_board();
+
+ for (let square of state.moves) {
+ window.game.receive_move(square);
+ }
+});
+
+/*********************************************************
+ * Game and supporting classes *
+ *********************************************************/
+
+function GameInfo(props) {
+ if (! props.id)
+ return null;
+
+ return (
+ <div className="game-info">
+ <h2>{props.id}</h2>
+ Invite a friend to play by sending this URL: {props.url}
+ </div>
+ );
+}
+
+function TeamButton(props) {
+ return <button className="inline"
+ onClick={() => props.game.join_team(props.team)}>
+ {props.label}
+ </button>;
+}
+
+function TeamChoices(props) {
+ let other_team;
+ if (props.player.team === "+")
+ other_team = "o";
+ else
+ other_team = "+";
+
+ if (props.player.team === "") {
+ if (props.first_move) {
+ return null;
+ } else {
+ return [
+ <TeamButton key="+" game={props.game} team="+" label="Join 🞥" />,
+ " ",
+ <TeamButton key="o" game={props.game} team="o" label="Join 🞇" />
+ ];
+ }
+ } else {
+ return <TeamButton game={props.game} team={other_team} label="Switch" />;
+ }
+}
+
+function PlayerInfo(props) {
+ if (! props.player.id)
+ return null;
+
+ const choices = <TeamChoices
+ game={props.game}
+ first_move={props.first_move}
+ player={props.player}
+ />;
+
+ return (
+ <div className="player-info">
+ <h2>Players</h2>
+ {props.player.name}
+ {props.player.team ? ` (${props.player.team})` : ""}
+ {props.first_move ? "" : " "}
+ {choices}
+ {props.other_players.map(other => (
+ <span key={other.id}>
+ {", "}
+ {other.name}
+ {other.team ? ` (${other.team})` : ""}
+ </span>
+ ))}
+ </div>
+ );
+}
+
+function Square(props) {
+ let className = "square";
+
+ if (props.value) {
+ className += " occupied";
+ } else if (props.active) {
+ className += " open";
+ }
+
+ const onClick = props.active ? props.onClick : null;
+
+ return (
+ <div className={className}
+ onClick={onClick}>
+ {props.value}
+ </div>
+ );
+}
+
+class Board extends React.Component {
+ render_square(i,j) {
+ const value = this.props.squares[i][j];
+ return (
+ <Square
+ value={value}
+ active={this.props.active && ! value}
+ onClick={() => this.props.onClick(i,j)}
+ />
+ );
+ }
+
+ render() {
+ return (
+ <div>
+ <div className="board-row">
+ {this.render_square(0,0)}
+ {this.render_square(0,1)}
+ {this.render_square(0,2)}
+ {" "}
+ {this.render_square(1,0)}
+ {this.render_square(1,1)}
+ {this.render_square(1,2)}
+ {" "}
+ {this.render_square(2,0)}
+ {this.render_square(2,1)}
+ {this.render_square(2,2)}
+ </div>
+ <div className="board-row">
+ {this.render_square(0,3)}
+ {this.render_square(0,4)}
+ {this.render_square(0,5)}
+ {" "}
+ {this.render_square(1,3)}
+ {this.render_square(1,4)}
+ {this.render_square(1,5)}
+ {" "}
+ {this.render_square(2,3)}
+ {this.render_square(2,4)}
+ {this.render_square(2,5)}
+ </div>
+ <div className="board-row">
+ {this.render_square(0,6)}
+ {this.render_square(0,7)}
+ {this.render_square(0,8)}
+ {" "}
+ {this.render_square(1,6)}
+ {this.render_square(1,7)}
+ {this.render_square(1,8)}
+ {" "}
+ {this.render_square(2,6)}
+ {this.render_square(2,7)}
+ {this.render_square(2,8)}
+ </div>
+
+ <div className="board-row">
+ </div>
+
+ <div className="board-row">
+ {this.render_square(3,0)}
+ {this.render_square(3,1)}
+ {this.render_square(3,2)}
+ {" "}
+ {this.render_square(4,0)}
+ {this.render_square(4,1)}
+ {this.render_square(4,2)}
+ {" "}
+ {this.render_square(5,0)}
+ {this.render_square(5,1)}
+ {this.render_square(5,2)}
+ </div>
+ <div className="board-row">
+ {this.render_square(3,3)}
+ {this.render_square(3,4)}
+ {this.render_square(3,5)}
+ {" "}
+ {this.render_square(4,3)}
+ {this.render_square(4,4)}
+ {this.render_square(4,5)}
+ {" "}
+ {this.render_square(5,3)}
+ {this.render_square(5,4)}
+ {this.render_square(5,5)}
+ </div>
+ <div className="board-row">
+ {this.render_square(3,6)}
+ {this.render_square(3,7)}
+ {this.render_square(3,8)}
+ {" "}
+ {this.render_square(4,6)}
+ {this.render_square(4,7)}
+ {this.render_square(4,8)}
+ {" "}
+ {this.render_square(5,6)}
+ {this.render_square(5,7)}
+ {this.render_square(5,8)}
+ </div>
+
+ <div className="board-row">
+ </div>
+
+ <div className="board-row">
+ {this.render_square(6,0)}
+ {this.render_square(6,1)}
+ {this.render_square(6,2)}
+ {" "}
+ {this.render_square(7,0)}
+ {this.render_square(7,1)}
+ {this.render_square(7,2)}
+ {" "}
+ {this.render_square(8,0)}
+ {this.render_square(8,1)}
+ {this.render_square(8,2)}
+ </div>
+ <div className="board-row">
+ {this.render_square(6,3)}
+ {this.render_square(6,4)}
+ {this.render_square(6,5)}
+ {" "}
+ {this.render_square(7,3)}
+ {this.render_square(7,4)}
+ {this.render_square(7,5)}
+ {" "}
+ {this.render_square(8,3)}
+ {this.render_square(8,4)}
+ {this.render_square(8,5)}
+ </div>
+ <div className="board-row">
+ {this.render_square(6,6)}
+ {this.render_square(6,7)}
+ {this.render_square(6,8)}
+ {" "}
+ {this.render_square(7,6)}
+ {this.render_square(7,7)}
+ {this.render_square(7,8)}
+ {" "}
+ {this.render_square(8,6)}
+ {this.render_square(8,7)}
+ {this.render_square(8,8)}
+ </div>
+
+ </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);
+}
+
+class Game extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ game_info: {},
+ player_info: {},
+ other_players: [],
+ squares: Array(9).fill(null).map(() => Array(9).fill(null)),
+ moves: 0,
+ next_to_play: "+"
+ };
+ }
+
+ 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
+ });
+ }
+
+ reset_board() {
+ this.setState({
+ next_to_play: "+"
+ });
+ }
+
+ receive_move(move) {
+ if (this.state.moves === 81) {
+ return;
+ }
+ const symbol = team_symbol(this.state.next_to_play);
+ const new_squares = this.state.squares.map(arr => arr.slice());
+ new_squares[move[0]][move[1]] = symbol;
+ let next_to_play;
+ if (this.state.next_to_play === "+")
+ next_to_play = "o";
+ else
+ next_to_play = "+";
+ this.setState({
+ squares: new_squares,
+ moves: this.state.moves + 1,
+ next_to_play: next_to_play
+ });
+ }
+
+ async handle_click(i, j, first_move) {
+ let move = {
+ move: [i, j]
+ };
+ if (first_move) {
+ move.assert_first_move = true;
+ }
+ const response = await fetch_post_json("move", move);
+ if (response.status == 200) {
+ const result = await response.json();
+ if (! result.legal)
+ add_message("danger", result.message);
+ } else {
+ add_message("danger", `Error occurred sending move`);
+ }
+ }
+
+ join_team(team) {
+ fetch_put_json("player", {team: team});
+ }
+
+ render() {
+ const state = this.state;
+ const first_move = state.moves === 0;
+ const my_team = state.player_info.team;
+ var board_active;
+
+ let status;
+ if (this.state.moves.length === 81)
+ {
+ status = "Game over";
+ board_active = false;
+ }
+ else if (first_move)
+ {
+ if (state.other_players.length == 0) {
+ status = "You can move or wait for another player to join.";
+ } else {
+ let qualifier;
+ if (state.other_players.length == 1) {
+ qualifier = "Either";
+ } else {
+ qualifier = "Any";
+ }
+ status = `${qualifier} player can make the first move.`;
+ }
+ board_active = true;
+ }
+ else if (my_team === "")
+ {
+ status = "You're just watching the game.";
+ board_active = false;
+ }
+ else if (my_team === state.next_to_play)
+ {
+ status = "Your turn. Make a move.";
+ board_active = true;
+ }
+ else
+ {
+ status = "Waiting for another player to ";
+ if (state.other_players.length == 0) {
+ status += "join.";
+ } else {
+ status += "move.";
+ }
+ board_active = false;
+ }
+
+ return [
+ <GameInfo
+ key="game-info"
+ id={state.game_info.id}
+ url={state.game_info.url}
+ />,
+ <PlayerInfo
+ key="player-info"
+ game={this}
+ first_move={first_move}
+ player={state.player_info}
+ other_players={state.other_players}
+ />,
+ <div key="game" className="game">
+ <div>{status}</div>
+ <div className="game-board">
+ <Board
+ active={board_active}
+ squares={state.squares}
+ onClick={(i,j) => this.handle_click(i, j, first_move)}
+ />
+ </div>
+ </div>
+ ];
+ }
+}
+
+ReactDOM.render(<Game
+ ref={(me) => window.game = me}
+ />, document.getElementById("scribe"));