]> git.cworth.org Git - lmno.games/blob - tictactoe/tictactoe.jsx
tictactoe: Add a simple game-info div
[lmno.games] / tictactoe / tictactoe.jsx
1 const Team = {
2   X: 0,
3   O: 1,
4   properties: {
5     0: {name: "X"},
6     1: {name: "O"}
7   }
8 };
9
10 function undisplay(element) {
11   element.style.display="none";
12 }
13
14 function add_message(severity, message) {
15   message = `<div class="message ${severity}" onclick="undisplay(this)">
16 <span class="hide-button" onclick="undisplay(this.parentElement)">&times;</span>
17 ${message}
18 </div>`;
19   const message_area = document.getElementById('message-area');
20   message_area.insertAdjacentHTML('beforeend', message);
21 }
22
23 /*********************************************************
24  * Handling server-sent event stream                     *
25  *********************************************************/
26
27 const events = new EventSource("events");
28
29 events.onerror = function(event) {
30   if (event.target.readyState === EventSource.CLOSED) {
31       add_message("danger", "Connection to server lost.");
32   }
33 };
34
35 events.addEventListener("game-info", event => {
36   const info = JSON.parse(event.data);
37
38   window.game.set_game_info(info);
39 });
40
41 events.addEventListener("move", event => {
42   const move = JSON.parse(event.data);
43
44   window.game.receive_move(move);
45 });
46
47 events.addEventListener("game-state", event => {
48   const state = JSON.parse(event.data);
49
50   window.game.reset_board();
51
52   for (let square of state.moves) {
53     window.game.receive_move(square);
54   }
55 });
56
57 /*********************************************************
58  * Game and supporting classes                           *
59  *********************************************************/
60
61 function GameInfo(props) {
62   return (
63     <div className="game-info">
64       <h2>{props.id}</h2>
65       Invite a friend to play by sending this URL: {props.url}
66     </div>
67   );
68 }
69
70 function Square(props) {
71   let className = "square";
72
73   if (props.value) {
74     className += " occupied";
75   } else if (props.active) {
76     className += " open";
77   }
78
79   const onClick = props.active ? props.onClick : null;
80
81   return (
82     <div className={className}
83          onClick={onClick}>
84       {props.value}
85     </div>
86   );
87 }
88
89 class Board extends React.Component {
90   render_square(i) {
91     const value = this.props.squares[i];
92     return (
93       <Square
94         value={value}
95         active={! this.props.game_over && ! value}
96         onClick={() => this.props.onClick(i)}
97       />
98     );
99   }
100
101   render() {
102     return (
103       <div>
104         <div className="board-row">
105           {this.render_square(0)}
106           {this.render_square(1)}
107           {this.render_square(2)}
108         </div>
109         <div className="board-row">
110           {this.render_square(3)}
111           {this.render_square(4)}
112           {this.render_square(5)}
113         </div>
114         <div className="board-row">
115           {this.render_square(6)}
116           {this.render_square(7)}
117           {this.render_square(8)}
118         </div>
119       </div>
120     );
121   }
122 }
123
124 function fetch_post_json(api = '', data = {}) {
125   const response = fetch(api, {
126     method: 'POST',
127     headers: {
128       'Content-Type': 'application/json'
129     },
130     body: JSON.stringify(data)
131   });
132   return response;
133 }
134
135 class Game extends React.Component {
136   constructor(props) {
137     super(props);
138     this.state = {
139       game_info: {},
140       history: [
141         {
142           squares: Array(9).fill(null)
143         }
144       ],
145       step_number: 0,
146       next_to_play: Team.X
147     };
148   }
149
150   set_game_info(info) {
151     this.setState({
152       game_info: info
153     });
154   }
155
156   send_move(i) {
157     return fetch_post_json("move", { move: i });
158   }
159
160   reset_board() {
161     this.setState({
162       history: [
163         {
164           squares: Array(9).fill(null)
165         }
166       ],
167       step_number: 0,
168       next_to_play: Team.X
169     });
170   }
171
172   receive_move(i) {
173     const history = this.state.history.slice(0, this.state.step_number + 1);
174     const current = history[history.length - 1];
175     const squares = current.squares.slice();
176     if (calculate_winner(squares) || squares[i]) {
177       return;
178     }
179     squares[i] = Team.properties[this.state.next_to_play].name;
180     let next_to_play;
181     if (this.state.next_to_play === Team.X)
182       next_to_play = Team.O;
183     else
184       next_to_play = Team.X;
185     this.setState({
186       history: history.concat([
187         {
188           squares: squares
189         }
190       ]),
191       step_number: history.length,
192       next_to_play: next_to_play
193     });
194   }
195
196   async handle_click(i) {
197     const response = await this.send_move(i);
198     if (response.status == 200) {
199       const result = await response.json();
200       if (! result.legal)
201         add_message("danger", result.message);
202     } else {
203       add_message("danger", `Error occurred sending move`);
204     }
205   }
206
207   render() {
208     const history = this.state.history;
209     const current = history[this.state.step_number];
210     const winner = calculate_winner(current.squares);
211
212     let status;
213     if (winner) {
214       status = "Winner: " + winner;
215     } else {
216       status = "Next player: " + (Team.properties[this.state.next_to_play].name);
217     }
218
219     return [
220       <GameInfo
221         id={this.state.game_info.id}
222         url={this.state.game_info.url}
223       />,
224       <div className="game">
225         <div>{status}</div>
226         <div className="game-board">
227           <Board
228             game_over={winner}
229             squares={current.squares}
230             onClick={i => this.handle_click(i)}
231           />
232         </div>
233       </div>
234     ];
235   }
236 }
237
238 ReactDOM.render(<Game
239                   ref={(me) => window.game = me}
240                 />, document.getElementById("tictactoe"));
241
242 function calculate_winner(squares) {
243   const lines = [
244     [0, 1, 2],
245     [3, 4, 5],
246     [6, 7, 8],
247     [0, 3, 6],
248     [1, 4, 7],
249     [2, 5, 8],
250     [0, 4, 8],
251     [2, 4, 6]
252   ];
253   for (let i = 0; i < lines.length; i++) {
254     const [a, b, c] = lines[i];
255     if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
256       return squares[a];
257     }
258   }
259   return null;
260 }