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-update", event => {
48 const info = JSON.parse(event.data);
50 if (info.id === window.game.state.player_info.id)
51 window.game.set_player_info(info);
54 events.addEventListener("move", event => {
55 const move = JSON.parse(event.data);
57 window.game.receive_move(move);
60 events.addEventListener("game-state", event => {
61 const state = JSON.parse(event.data);
63 window.game.reset_board();
65 for (let square of state.moves) {
66 window.game.receive_move(square);
70 /*********************************************************
71 * Game and supporting classes *
72 *********************************************************/
74 function GameInfo(props) {
79 <div className="game-info">
81 Invite a friend to play by sending this URL: {props.url}
86 function PlayerInfo(props) {
87 if (! props.player.id)
91 <div className="player-info">
94 {props.player.team ? ` (${props.player.team})` : ""}
99 function Square(props) {
100 let className = "square";
103 className += " occupied";
104 } else if (props.active) {
105 className += " open";
108 const onClick = props.active ? props.onClick : null;
111 <div className={className}
118 class Board extends React.Component {
120 const value = this.props.squares[i];
124 active={this.props.active && ! value}
125 onClick={() => this.props.onClick(i)}
133 <div className="board-row">
134 {this.render_square(0)}
135 {this.render_square(1)}
136 {this.render_square(2)}
138 <div className="board-row">
139 {this.render_square(3)}
140 {this.render_square(4)}
141 {this.render_square(5)}
143 <div className="board-row">
144 {this.render_square(6)}
145 {this.render_square(7)}
146 {this.render_square(8)}
153 function fetch_method_json(method, api = '', data = {}) {
154 const response = fetch(api, {
157 'Content-Type': 'application/json'
159 body: JSON.stringify(data)
164 function fetch_post_json(api = '', data = {}) {
165 return fetch_method_json('POST', api, data);
168 async function fetch_put_json(api = '', data = {}) {
169 return fetch_method_json('PUT', api, data);
172 class Game extends React.Component {
180 squares: Array(9).fill(null)
188 set_game_info(info) {
194 set_player_info(info) {
204 squares: Array(9).fill(null)
213 const history = this.state.history.slice(0, this.state.step_number + 1);
214 const current = history[history.length - 1];
215 const squares = current.squares.slice();
216 if (calculate_winner(squares) || squares[i]) {
219 squares[i] = Team.properties[this.state.next_to_play].name;
221 if (this.state.next_to_play === Team.X)
222 next_to_play = Team.O;
224 next_to_play = Team.X;
226 history: history.concat([
231 step_number: history.length,
232 next_to_play: next_to_play
236 async handle_click(i, first_move) {
237 let move = { move: i };
239 move.assert_first_move = true;
241 const response = await fetch_post_json("move", move);
242 if (response.status == 200) {
243 const result = await response.json();
245 add_message("danger", result.message);
247 add_message("danger", `Error occurred sending move`);
252 fetch_put_json("player", {team: team});
256 const state = this.state;
257 const history = state.history;
258 const current = history[state.step_number];
259 const winner = calculate_winner(current.squares);
260 const first_move = state.step_number === 0;
261 const my_team = state.player_info.team;
267 status = winner + " wins!";
268 if (state.player_info.team != "")
270 if (my_team === winner)
271 status += " Congratulations!";
273 status += " Better luck next time.";
275 board_active = false;
279 status = "Either player can make the first move.";
282 else if (my_team === "")
284 status = "You're just watching the game.";
285 board_active = false;
287 else if (my_team === Team.properties[state.next_to_play].name)
289 status = "Your turn. Make a move.";
294 status = "Waiting for your opponent to move.";
295 board_active = false;
301 id={state.game_info.id}
302 url={state.game_info.url}
306 player={state.player_info}
308 <div key="game" className="game">
309 <button className="inline"
310 onClick={() => this.join_team('X')}>Join Team X</button>
312 <button className="inline"
313 onClick={() => this.join_team('O')}>Join Team O</button>
315 <div className="game-board">
317 active={board_active}
318 squares={current.squares}
319 onClick={i => this.handle_click(i, first_move)}
327 ReactDOM.render(<Game
328 ref={(me) => window.game = me}
329 />, document.getElementById("tictactoe"));
331 function calculate_winner(squares) {
342 for (let i = 0; i < lines.length; i++) {
343 const [a, b, c] = lines[i];
344 if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {