10 function undisplay(element) {
11 element.style.display="none";
14 function add_message(severity, message) {
15 message = `<div class="message ${severity}" onclick="undisplay(this)">
16 <span class="hide-button" onclick="undisplay(this.parentElement)">×</span>
19 const message_area = document.getElementById('message-area');
20 message_area.insertAdjacentHTML('beforeend', message);
23 /*********************************************************
24 * Handling server-sent event stream *
25 *********************************************************/
27 const events = new EventSource("events");
29 events.onerror = function(event) {
30 if (event.target.readyState === EventSource.CLOSED) {
31 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 function GameInfo(props) {
87 <div className="game-info">
89 Invite a friend to play by sending this URL: {props.url}
94 function TeamButton(props) {
95 return <button className="inline"
96 onClick={() => props.game.join_team(props.team)}>
101 function TeamChoices(props) {
103 if (props.player.team === "X")
108 if (props.player.team === "") {
109 if (props.first_move) {
113 <TeamButton key="X" game={props.game} team="X" label="Join X" />,
115 <TeamButton key="O" game={props.game} team="O" label="Join O" />
119 return <TeamButton game={props.game} team={other_team} label="Switch" />;
123 function PlayerInfo(props) {
124 if (! props.player.id)
127 const choices = <TeamChoices
129 first_move={props.first_move}
130 player={props.player}
134 <div className="player-info">
137 {props.player.team ? ` (${props.player.team})` : ""}
138 {props.first_move ? "" : " "}
140 {props.other_players.map(other => (
141 <span key={other.id}>
144 {other.team ? ` (${other.team})` : ""}
151 function Square(props) {
152 let className = "square";
155 className += " occupied";
156 } else if (props.active) {
157 className += " open";
160 const onClick = props.active ? props.onClick : null;
163 <div className={className}
170 class Board extends React.Component {
172 const value = this.props.squares[i];
176 active={this.props.active && ! value}
177 onClick={() => this.props.onClick(i)}
185 <div className="board-row">
186 {this.render_square(0)}
187 {this.render_square(1)}
188 {this.render_square(2)}
190 <div className="board-row">
191 {this.render_square(3)}
192 {this.render_square(4)}
193 {this.render_square(5)}
195 <div className="board-row">
196 {this.render_square(6)}
197 {this.render_square(7)}
198 {this.render_square(8)}
205 function fetch_method_json(method, api = '', data = {}) {
206 const response = fetch(api, {
209 'Content-Type': 'application/json'
211 body: JSON.stringify(data)
216 function fetch_post_json(api = '', data = {}) {
217 return fetch_method_json('POST', api, data);
220 async function fetch_put_json(api = '', data = {}) {
221 return fetch_method_json('PUT', api, data);
224 class Game extends React.Component {
233 squares: Array(9).fill(null)
241 set_game_info(info) {
247 set_player_info(info) {
253 set_other_player_info(info) {
254 const other_players_copy = [...this.state.other_players];
255 const idx = other_players_copy.findIndex(o => o.id === info.id);
257 other_players_copy[idx] = info;
259 other_players_copy.push(info);
262 other_players: other_players_copy
270 squares: Array(9).fill(null)
279 const history = this.state.history.slice(0, this.state.step_number + 1);
280 const current = history[history.length - 1];
281 const squares = current.squares.slice();
282 if (calculate_winner(squares) || squares[i]) {
285 squares[i] = Team.properties[this.state.next_to_play].name;
287 if (this.state.next_to_play === Team.X)
288 next_to_play = Team.O;
290 next_to_play = Team.X;
292 history: history.concat([
297 step_number: history.length,
298 next_to_play: next_to_play
302 async handle_click(i, first_move) {
303 let move = { move: i };
305 move.assert_first_move = true;
307 const response = await fetch_post_json("move", move);
308 if (response.status == 200) {
309 const result = await response.json();
311 add_message("danger", result.message);
313 add_message("danger", `Error occurred sending move`);
318 fetch_put_json("player", {team: team});
322 const state = this.state;
323 const history = state.history;
324 const current = history[state.step_number];
325 const winner = calculate_winner(current.squares);
326 const first_move = state.step_number === 0;
327 const my_team = state.player_info.team;
333 status = winner + " wins!";
334 if (state.player_info.team != "")
336 if (my_team === winner)
337 status += " Congratulations!";
339 status += " Better luck next time.";
341 board_active = false;
345 if (state.other_players.length == 0) {
346 status = "You can move or wait for another player to join.";
349 if (state.other_players.length == 1) {
350 qualifier = "Either";
354 status = `${qualifier} player can make the first move.`;
358 else if (my_team === "")
360 status = "You're just watching the game.";
361 board_active = false;
363 else if (my_team === Team.properties[state.next_to_play].name)
365 status = "Your turn. Make a move.";
370 status = "Waiting for another player to ";
371 if (state.other_players.length == 0) {
376 board_active = false;
382 id={state.game_info.id}
383 url={state.game_info.url}
388 first_move={first_move}
389 player={state.player_info}
390 other_players={state.other_players}
392 <div key="game" className="game">
394 <div className="game-board">
396 active={board_active}
397 squares={current.squares}
398 onClick={i => this.handle_click(i, first_move)}
406 ReactDOM.render(<Game
407 ref={(me) => window.game = me}
408 />, document.getElementById("tictactoe"));
410 function calculate_winner(squares) {
421 for (let i = 0; i < lines.length; i++) {
422 const [a, b, c] = lines[i];
423 if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {