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) {
29 add_message("danger", "Connection to server lost.");
33 events.addEventListener("game-info", event => {
34 const info = JSON.parse(event.data);
36 window.game.set_game_info(info);
39 events.addEventListener("player-info", event => {
40 const info = JSON.parse(event.data);
42 window.game.set_player_info(info);
45 events.addEventListener("player-enter", event => {
46 const info = JSON.parse(event.data);
48 window.game.set_other_player_info(info);
51 events.addEventListener("player-update", event => {
52 const info = JSON.parse(event.data);
54 if (info.id === window.game.state.player_info.id)
55 window.game.set_player_info(info);
57 window.game.set_other_player_info(info);
60 events.addEventListener("move", event => {
61 const move = JSON.parse(event.data);
63 window.game.receive_move(move);
66 events.addEventListener("game-state", event => {
67 const state = JSON.parse(event.data);
69 window.game.reset_board();
71 for (let square of state.moves) {
72 window.game.receive_move(square);
76 /*********************************************************
77 * Game and supporting classes *
78 *********************************************************/
80 function GameInfo(props) {
85 <div className="game-info">
87 Invite a friend to play by sending this URL: {props.url}
92 function TeamButton(props) {
93 return <button className="inline"
94 onClick={() => props.game.join_team(props.team)}>
99 function TeamChoices(props) {
101 if (props.player.team === "+")
106 if (props.player.team === "") {
107 if (props.first_move) {
111 <TeamButton key="+" game={props.game} team="+" label="Join 🞥" />,
113 <TeamButton key="o" game={props.game} team="o" label="Join 🞇" />
117 return <TeamButton game={props.game} team={other_team} label="Switch" />;
121 function PlayerInfo(props) {
122 if (! props.player.id)
125 const choices = <TeamChoices
127 first_move={props.first_move}
128 player={props.player}
132 <div className="player-info">
135 {props.player.team ? ` (${props.player.team})` : ""}
136 {props.first_move ? "" : " "}
138 {props.other_players.map(other => (
139 <span key={other.id}>
142 {other.team ? ` (${other.team})` : ""}
149 function Square(props) {
150 let className = "square";
153 className += " occupied";
154 } else if (props.active) {
155 className += " open";
158 const onClick = props.active ? props.onClick : null;
161 <div className={className}
168 class Board extends React.Component {
170 const value = this.props.squares[i][j];
174 active={this.props.active && ! value}
175 onClick={() => this.props.onClick(i,j)}
183 <div className="board-row">
184 {this.render_square(0,0)}
185 {this.render_square(0,1)}
186 {this.render_square(0,2)}
188 {this.render_square(1,0)}
189 {this.render_square(1,1)}
190 {this.render_square(1,2)}
192 {this.render_square(2,0)}
193 {this.render_square(2,1)}
194 {this.render_square(2,2)}
196 <div className="board-row">
197 {this.render_square(0,3)}
198 {this.render_square(0,4)}
199 {this.render_square(0,5)}
201 {this.render_square(1,3)}
202 {this.render_square(1,4)}
203 {this.render_square(1,5)}
205 {this.render_square(2,3)}
206 {this.render_square(2,4)}
207 {this.render_square(2,5)}
209 <div className="board-row">
210 {this.render_square(0,6)}
211 {this.render_square(0,7)}
212 {this.render_square(0,8)}
214 {this.render_square(1,6)}
215 {this.render_square(1,7)}
216 {this.render_square(1,8)}
218 {this.render_square(2,6)}
219 {this.render_square(2,7)}
220 {this.render_square(2,8)}
223 <div className="board-row">
226 <div className="board-row">
227 {this.render_square(3,0)}
228 {this.render_square(3,1)}
229 {this.render_square(3,2)}
231 {this.render_square(4,0)}
232 {this.render_square(4,1)}
233 {this.render_square(4,2)}
235 {this.render_square(5,0)}
236 {this.render_square(5,1)}
237 {this.render_square(5,2)}
239 <div className="board-row">
240 {this.render_square(3,3)}
241 {this.render_square(3,4)}
242 {this.render_square(3,5)}
244 {this.render_square(4,3)}
245 {this.render_square(4,4)}
246 {this.render_square(4,5)}
248 {this.render_square(5,3)}
249 {this.render_square(5,4)}
250 {this.render_square(5,5)}
252 <div className="board-row">
253 {this.render_square(3,6)}
254 {this.render_square(3,7)}
255 {this.render_square(3,8)}
257 {this.render_square(4,6)}
258 {this.render_square(4,7)}
259 {this.render_square(4,8)}
261 {this.render_square(5,6)}
262 {this.render_square(5,7)}
263 {this.render_square(5,8)}
266 <div className="board-row">
269 <div className="board-row">
270 {this.render_square(6,0)}
271 {this.render_square(6,1)}
272 {this.render_square(6,2)}
274 {this.render_square(7,0)}
275 {this.render_square(7,1)}
276 {this.render_square(7,2)}
278 {this.render_square(8,0)}
279 {this.render_square(8,1)}
280 {this.render_square(8,2)}
282 <div className="board-row">
283 {this.render_square(6,3)}
284 {this.render_square(6,4)}
285 {this.render_square(6,5)}
287 {this.render_square(7,3)}
288 {this.render_square(7,4)}
289 {this.render_square(7,5)}
291 {this.render_square(8,3)}
292 {this.render_square(8,4)}
293 {this.render_square(8,5)}
295 <div className="board-row">
296 {this.render_square(6,6)}
297 {this.render_square(6,7)}
298 {this.render_square(6,8)}
300 {this.render_square(7,6)}
301 {this.render_square(7,7)}
302 {this.render_square(7,8)}
304 {this.render_square(8,6)}
305 {this.render_square(8,7)}
306 {this.render_square(8,8)}
314 function fetch_method_json(method, api = '', data = {}) {
315 const response = fetch(api, {
318 'Content-Type': 'application/json'
320 body: JSON.stringify(data)
325 function fetch_post_json(api = '', data = {}) {
326 return fetch_method_json('POST', api, data);
329 async function fetch_put_json(api = '', data = {}) {
330 return fetch_method_json('PUT', api, data);
333 class Game extends React.Component {
340 squares: Array(9).fill(null).map(() => Array(9).fill(null)),
346 set_game_info(info) {
352 set_player_info(info) {
358 set_other_player_info(info) {
359 const other_players_copy = [...this.state.other_players];
360 const idx = other_players_copy.findIndex(o => o.id === info.id);
362 other_players_copy[idx] = info;
364 other_players_copy.push(info);
367 other_players: other_players_copy
378 if (this.state.moves === 81) {
381 const symbol = team_symbol(this.state.next_to_play);
382 const new_squares = this.state.squares.map(arr => arr.slice());
383 new_squares[move[0]][move[1]] = symbol;
385 if (this.state.next_to_play === "+")
390 squares: new_squares,
391 moves: this.state.moves + 1,
392 next_to_play: next_to_play
396 async handle_click(i, j, first_move) {
401 move.assert_first_move = true;
403 const response = await fetch_post_json("move", move);
404 if (response.status == 200) {
405 const result = await response.json();
407 add_message("danger", result.message);
409 add_message("danger", `Error occurred sending move`);
414 fetch_put_json("player", {team: team});
418 const state = this.state;
419 const first_move = state.moves === 0;
420 const my_team = state.player_info.team;
424 if (this.state.moves.length === 81)
426 status = "Game over";
427 board_active = false;
431 if (state.other_players.length == 0) {
432 status = "You can move or wait for another player to join.";
435 if (state.other_players.length == 1) {
436 qualifier = "Either";
440 status = `${qualifier} player can make the first move.`;
444 else if (my_team === "")
446 status = "You're just watching the game.";
447 board_active = false;
449 else if (my_team === state.next_to_play)
451 status = "Your turn. Make a move.";
456 status = "Waiting for another player to ";
457 if (state.other_players.length == 0) {
462 board_active = false;
468 id={state.game_info.id}
469 url={state.game_info.url}
474 first_move={first_move}
475 player={state.player_info}
476 other_players={state.other_players}
478 <div key="game" className="game">
480 <div className="game-board">
482 active={board_active}
483 squares={state.squares}
484 onClick={(i,j) => this.handle_click(i, j, first_move)}
492 ReactDOM.render(<Game
493 ref={(me) => window.game = me}
494 />, document.getElementById("scribe"));