const Team = { X: 0, O: 1, properties: { 0: {name: "X"}, 1: {name: "O"} } }; function undisplay(element) { element.style.display="none"; } function add_message(severity, message) { message = `
× ${message}
`; 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-update", event => { const info = JSON.parse(event.data); if (info.id === window.game.state.player_info.id) window.game.set_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 (

{props.id}

Invite a friend to play by sending this URL: {props.url}
); } function PlayerInfo(props) { if (! props.id) return null; return (

Player

{props.name}, ID: {props.id}, on team: {props.team}
); } 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 (
{props.value}
); } class Board extends React.Component { render_square(i) { const value = this.props.squares[i]; return ( this.props.onClick(i)} /> ); } render() { return (
{this.render_square(0)} {this.render_square(1)} {this.render_square(2)}
{this.render_square(3)} {this.render_square(4)} {this.render_square(5)}
{this.render_square(6)} {this.render_square(7)} {this.render_square(8)}
); } } 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: {}, history: [ { squares: Array(9).fill(null) } ], step_number: 0, next_to_play: Team.X }; } set_game_info(info) { this.setState({ game_info: info }); } set_player_info(info) { this.setState({ player_info: info }); } send_move(i) { return fetch_post_json("move", { move: i }); } reset_board() { this.setState({ history: [ { squares: Array(9).fill(null) } ], step_number: 0, next_to_play: Team.X }); } receive_move(i) { const history = this.state.history.slice(0, this.state.step_number + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculate_winner(squares) || squares[i]) { return; } squares[i] = Team.properties[this.state.next_to_play].name; let next_to_play; if (this.state.next_to_play === Team.X) next_to_play = Team.O; else next_to_play = Team.X; this.setState({ history: history.concat([ { squares: squares } ]), step_number: history.length, next_to_play: next_to_play }); } async handle_click(i) { const response = await this.send_move(i); 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 history = this.state.history; const current = history[this.state.step_number]; const winner = calculate_winner(current.squares); let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (Team.properties[this.state.next_to_play].name); } return [ , ,
 
{status}
this.handle_click(i)} />
]; } } ReactDOM.render( window.game = me} />, document.getElementById("tictactoe")); function calculate_winner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }