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-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 (

{props.id}

Invite a friend to play by sending this URL: {props.url}
); } function TeamButton(props) { return ; } function TeamChoices(props) { let other_team; if (props.player.team === "X") other_team = "O"; else other_team = "X"; if (props.player.team === "") { if (props.first_move) { return null; } else { return [ , " ", ]; } } else { return ; } } function PlayerInfo(props) { if (! props.player.id) return null; const choices = ; return (

Players

{props.player.name} {props.player.team ? ` (${props.player.team})` : ""} {props.first_move ? "" : " "} {choices} {props.other_players.map(other => ( {", "} {other.name} {other.team ? ` (${other.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: {}, other_players: [], 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 }); } 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({ 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, first_move) { let move = { move: i }; 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 history = state.history; const current = history[state.step_number]; const winner = calculate_winner(current.squares); const first_move = state.step_number === 0; const my_team = state.player_info.team; var board_active; let status; if (winner) { status = winner + " wins!"; if (state.player_info.team != "") { if (my_team === winner) status += " Congratulations!"; else status += " Better luck next time."; } 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 === Team.properties[state.next_to_play].name) { 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 [ , ,
{status}
this.handle_click(i, first_move)} />
]; } } 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; }