]> git.cworth.org Git - lmno.games/blob - tictactoe/tictactoe.jsx
Return null from GameInfo and PlayerInfo if they have no populated props
[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}, on team: {props.team}
94     </div>
95   );
96 }
97
98 function Square(props) {
99   let className = "square";
100
101   if (props.value) {
102     className += " occupied";
103   } else if (props.active) {
104     className += " open";
105   }
106
107   const onClick = props.active ? props.onClick : null;
108
109   return (
110     <div className={className}
111          onClick={onClick}>
112       {props.value}
113     </div>
114   );
115 }
116
117 class Board extends React.Component {
118   render_square(i) {
119     const value = this.props.squares[i];
120     return (
121       <Square
122         value={value}
123         active={! this.props.game_over && ! value}
124         onClick={() => this.props.onClick(i)}
125       />
126     );
127   }
128
129   render() {
130     return (
131       <div>
132         <div className="board-row">
133           {this.render_square(0)}
134           {this.render_square(1)}
135           {this.render_square(2)}
136         </div>
137         <div className="board-row">
138           {this.render_square(3)}
139           {this.render_square(4)}
140           {this.render_square(5)}
141         </div>
142         <div className="board-row">
143           {this.render_square(6)}
144           {this.render_square(7)}
145           {this.render_square(8)}
146         </div>
147       </div>
148     );
149   }
150 }
151
152 function fetch_method_json(method, api = '', data = {}) {
153   const response = fetch(api, {
154     method: method,
155     headers: {
156       'Content-Type': 'application/json'
157     },
158     body: JSON.stringify(data)
159   });
160   return response;
161 }
162
163 function fetch_post_json(api = '', data = {}) {
164   return fetch_method_json('POST', api, data);
165 }
166
167 async function fetch_put_json(api = '', data = {}) {
168   return fetch_method_json('PUT', api, data);
169 }
170
171 class Game extends React.Component {
172   constructor(props) {
173     super(props);
174     this.state = {
175       game_info: {},
176       player_info: {},
177       history: [
178         {
179           squares: Array(9).fill(null)
180         }
181       ],
182       step_number: 0,
183       next_to_play: Team.X
184     };
185   }
186
187   set_game_info(info) {
188     this.setState({
189       game_info: info
190     });
191   }
192
193   set_player_info(info) {
194     this.setState({
195       player_info: info
196     });
197   }
198
199   send_move(i) {
200     return fetch_post_json("move", { move: i });
201   }
202
203   reset_board() {
204     this.setState({
205       history: [
206         {
207           squares: Array(9).fill(null)
208         }
209       ],
210       step_number: 0,
211       next_to_play: Team.X
212     });
213   }
214
215   receive_move(i) {
216     const history = this.state.history.slice(0, this.state.step_number + 1);
217     const current = history[history.length - 1];
218     const squares = current.squares.slice();
219     if (calculate_winner(squares) || squares[i]) {
220       return;
221     }
222     squares[i] = Team.properties[this.state.next_to_play].name;
223     let next_to_play;
224     if (this.state.next_to_play === Team.X)
225       next_to_play = Team.O;
226     else
227       next_to_play = Team.X;
228     this.setState({
229       history: history.concat([
230         {
231           squares: squares
232         }
233       ]),
234       step_number: history.length,
235       next_to_play: next_to_play
236     });
237   }
238
239   async handle_click(i) {
240     const response = await this.send_move(i);
241     if (response.status == 200) {
242       const result = await response.json();
243       if (! result.legal)
244         add_message("danger", result.message);
245     } else {
246       add_message("danger", `Error occurred sending move`);
247     }
248   }
249
250   join_team(team) {
251     fetch_put_json("player", {team: team});
252   }
253
254   render() {
255     const history = this.state.history;
256     const current = history[this.state.step_number];
257     const winner = calculate_winner(current.squares);
258
259     let status;
260     if (winner) {
261       status = "Winner: " + winner;
262     } else {
263       status = "Next player: " + (Team.properties[this.state.next_to_play].name);
264     }
265
266     return [
267       <GameInfo
268         key="game-info"
269         id={this.state.game_info.id}
270         url={this.state.game_info.url}
271       />,
272       <PlayerInfo
273         key="player-info"
274         id={this.state.player_info.id}
275         name={this.state.player_info.name}
276         team={this.state.player_info.team}
277       />,
278       <div key="game" className="game">
279         <button className="inline"
280                 onClick={() => this.join_team('X')}>Join Team X</button>
281         &nbsp;
282         <button className="inline"
283                 onClick={() => this.join_team('O')}>Join Team O</button>
284         <div>{status}</div>
285         <div className="game-board">
286           <Board
287             game_over={winner}
288             squares={current.squares}
289             onClick={i => this.handle_click(i)}
290           />
291         </div>
292       </div>
293     ];
294   }
295 }
296
297 ReactDOM.render(<Game
298                   ref={(me) => window.game = me}
299                 />, document.getElementById("tictactoe"));
300
301 function calculate_winner(squares) {
302   const lines = [
303     [0, 1, 2],
304     [3, 4, 5],
305     [6, 7, 8],
306     [0, 3, 6],
307     [1, 4, 7],
308     [2, 5, 8],
309     [0, 4, 8],
310     [2, 4, 6]
311   ];
312   for (let i = 0; i < lines.length; i++) {
313     const [a, b, c] = lines[i];
314     if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
315       return squares[a];
316     }
317   }
318   return null;
319 }