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