]> git.cworth.org Git - lmno.games/blob - tictactoe/tictactoe.jsx
Use an actual space not an   entity.
[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("player-info", event => {
42   const info = JSON.parse(event.data);
43
44   window.game.set_player_info(info);
45 });
46
47 events.addEventListener("player-update", event => {
48   const info = JSON.parse(event.data);
49
50   if (info.id === window.game.state.player_info.id)
51     window.game.set_player_info(info);
52 });
53
54 events.addEventListener("move", event => {
55   const move = JSON.parse(event.data);
56
57   window.game.receive_move(move);
58 });
59
60 events.addEventListener("game-state", event => {
61   const state = JSON.parse(event.data);
62
63   window.game.reset_board();
64
65   for (let square of state.moves) {
66     window.game.receive_move(square);
67   }
68 });
69
70 /*********************************************************
71  * Game and supporting classes                           *
72  *********************************************************/
73
74 function GameInfo(props) {
75   if (! props.id)
76     return null;
77
78   return (
79     <div className="game-info">
80       <h2>{props.id}</h2>
81       Invite a friend to play by sending this URL: {props.url}
82     </div>
83   );
84 }
85
86 function PlayerInfo(props) {
87   if (! props.id)
88     return null;
89
90   return (
91     <div className="player-info">
92       <h2>Player</h2>
93       {props.name}, ID: {props.id},
94       {props.team ? ` on team ${props.team}` : " not on a team"}
95     </div>
96   );
97 }
98
99 function Square(props) {
100   let className = "square";
101
102   if (props.value) {
103     className += " occupied";
104   } else if (props.active) {
105     className += " open";
106   }
107
108   const onClick = props.active ? props.onClick : null;
109
110   return (
111     <div className={className}
112          onClick={onClick}>
113       {props.value}
114     </div>
115   );
116 }
117
118 class Board extends React.Component {
119   render_square(i) {
120     const value = this.props.squares[i];
121     return (
122       <Square
123         value={value}
124         active={this.props.active && ! value}
125         onClick={() => this.props.onClick(i)}
126       />
127     );
128   }
129
130   render() {
131     return (
132       <div>
133         <div className="board-row">
134           {this.render_square(0)}
135           {this.render_square(1)}
136           {this.render_square(2)}
137         </div>
138         <div className="board-row">
139           {this.render_square(3)}
140           {this.render_square(4)}
141           {this.render_square(5)}
142         </div>
143         <div className="board-row">
144           {this.render_square(6)}
145           {this.render_square(7)}
146           {this.render_square(8)}
147         </div>
148       </div>
149     );
150   }
151 }
152
153 function fetch_method_json(method, api = '', data = {}) {
154   const response = fetch(api, {
155     method: method,
156     headers: {
157       'Content-Type': 'application/json'
158     },
159     body: JSON.stringify(data)
160   });
161   return response;
162 }
163
164 function fetch_post_json(api = '', data = {}) {
165   return fetch_method_json('POST', api, data);
166 }
167
168 async function fetch_put_json(api = '', data = {}) {
169   return fetch_method_json('PUT', api, data);
170 }
171
172 class Game extends React.Component {
173   constructor(props) {
174     super(props);
175     this.state = {
176       game_info: {},
177       player_info: {},
178       history: [
179         {
180           squares: Array(9).fill(null)
181         }
182       ],
183       step_number: 0,
184       next_to_play: Team.X
185     };
186   }
187
188   set_game_info(info) {
189     this.setState({
190       game_info: info
191     });
192   }
193
194   set_player_info(info) {
195     this.setState({
196       player_info: info
197     });
198   }
199
200   reset_board() {
201     this.setState({
202       history: [
203         {
204           squares: Array(9).fill(null)
205         }
206       ],
207       step_number: 0,
208       next_to_play: Team.X
209     });
210   }
211
212   receive_move(i) {
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]) {
217       return;
218     }
219     squares[i] = Team.properties[this.state.next_to_play].name;
220     let next_to_play;
221     if (this.state.next_to_play === Team.X)
222       next_to_play = Team.O;
223     else
224       next_to_play = Team.X;
225     this.setState({
226       history: history.concat([
227         {
228           squares: squares
229         }
230       ]),
231       step_number: history.length,
232       next_to_play: next_to_play
233     });
234   }
235
236   async handle_click(i, first_move) {
237     let move = { move: i };
238     if (first_move) {
239       move.assert_first_move = true;
240     }
241     const response = await fetch_post_json("move", move);
242     if (response.status == 200) {
243       const result = await response.json();
244       if (! result.legal)
245         add_message("danger", result.message);
246     } else {
247       add_message("danger", `Error occurred sending move`);
248     }
249   }
250
251   join_team(team) {
252     fetch_put_json("player", {team: team});
253   }
254
255   render() {
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;
262     var board_active;
263
264     let status;
265     if (winner)
266     {
267       status = winner + " wins!";
268       if (state.player_info.team != "")
269       {
270         if (my_team === winner)
271           status += " Congratulations!";
272         else
273           status += " Better luck next time.";
274       }
275       board_active = false;
276     }
277     else if (first_move)
278     {
279       status = "Either player can make the first move.";
280       board_active = true;
281     }
282     else if (my_team === "")
283     {
284       status = "You're just watching the game.";
285       board_active = false;
286     }
287     else if (my_team === Team.properties[state.next_to_play].name)
288     {
289       status = "Your turn. Make a move.";
290       board_active = true;
291     }
292     else
293     {
294       status = "Waiting for your opponent to move.";
295       board_active = false;
296     }
297
298     return [
299       <GameInfo
300         key="game-info"
301         id={state.game_info.id}
302         url={state.game_info.url}
303       />,
304       <PlayerInfo
305         key="player-info"
306         id={state.player_info.id}
307         name={state.player_info.name}
308         team={state.player_info.team}
309       />,
310       <div key="game" className="game">
311         <button className="inline"
312                 onClick={() => this.join_team('X')}>Join Team X</button>
313         {" "}
314         <button className="inline"
315                 onClick={() => this.join_team('O')}>Join Team O</button>
316         <div>{status}</div>
317         <div className="game-board">
318           <Board
319             active={board_active}
320             squares={current.squares}
321             onClick={i => this.handle_click(i, first_move)}
322           />
323         </div>
324       </div>
325     ];
326   }
327 }
328
329 ReactDOM.render(<Game
330                   ref={(me) => window.game = me}
331                 />, document.getElementById("tictactoe"));
332
333 function calculate_winner(squares) {
334   const lines = [
335     [0, 1, 2],
336     [3, 4, 5],
337     [6, 7, 8],
338     [0, 3, 6],
339     [1, 4, 7],
340     [2, 5, 8],
341     [0, 4, 8],
342     [2, 4, 6]
343   ];
344   for (let i = 0; i < lines.length; i++) {
345     const [a, b, c] = lines[i];
346     if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
347       return squares[a];
348     }
349   }
350   return null;
351 }