]> git.cworth.org Git - lmno.games/blob - scribe/scribe.jsx
Delay the "Connection lost" message by a second
[lmno.games] / scribe / scribe.jsx
1 function team_symbol(team) {
2   if (team === "+")
3     return "+";
4   else
5     return "o";
6 }
7
8 function undisplay(element) {
9   element.style.display="none";
10 }
11
12 function add_message(severity, message) {
13   message = `<div class="message ${severity}" onclick="undisplay(this)">
14 <span class="hide-button" onclick="undisplay(this.parentElement)">&times;</span>
15 ${message}
16 </div>`;
17   const message_area = document.getElementById('message-area');
18   message_area.insertAdjacentHTML('beforeend', message);
19 }
20
21 /*********************************************************
22  * Handling server-sent event stream                     *
23  *********************************************************/
24
25 const events = new EventSource("events");
26
27 events.onerror = function(event) {
28   if (event.target.readyState === EventSource.CLOSED) {
29     setTimeout(() => {
30       add_message("danger", "Connection to server lost.");
31     }, 1000);
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_other_player_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_other_player_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 TeamButton(props) {
95   return <button className="inline"
96                  onClick={() => props.game.join_team(props.team)}>
97            {props.label}
98          </button>;
99 }
100
101 function TeamChoices(props) {
102   let other_team;
103   if (props.player.team === "+")
104     other_team = "o";
105   else
106     other_team = "+";
107
108   if (props.player.team === "") {
109     if (props.first_move) {
110       return null;
111     } else {
112       return [
113         <TeamButton key="+" game={props.game} team="+" label="Join ðŸž¥" />,
114         " ",
115         <TeamButton key="o" game={props.game} team="o" label="Join ðŸž‡" />
116       ];
117     }
118   } else {
119     return <TeamButton game={props.game} team={other_team} label="Switch" />;
120   }
121 }
122
123 function PlayerInfo(props) {
124   if (! props.player.id)
125     return null;
126
127   const choices = <TeamChoices
128                     game={props.game}
129                     first_move={props.first_move}
130                     player={props.player}
131                   />;
132
133   return (
134     <div className="player-info">
135       <h2>Players</h2>
136       {props.player.name}
137       {props.player.team ? ` (${props.player.team})` : ""}
138       {props.first_move ? "" : " "}
139       {choices}
140       {props.other_players.map(other => (
141         <span key={other.id}>
142           {", "}
143           {other.name}
144           {other.team ? ` (${other.team})` : ""}
145         </span>
146       ))}
147     </div>
148   );
149 }
150
151 function Square(props) {
152   let className = "square";
153
154   if (props.value) {
155     className += " occupied";
156   } else if (props.active) {
157     className += " open";
158   }
159
160   const onClick = props.active ? props.onClick : null;
161
162   return (
163     <div className={className}
164          onClick={onClick}>
165       {props.value}
166     </div>
167   );
168 }
169
170 function MiniGrid(props) {
171   function grid_square(j) {
172     const value = props.squares[j];
173     return (
174       <Square
175         value={value}
176         active={props.active}
177         onClick={() => props.onClick(j)}
178       />
179     );
180   }
181
182   return (
183     <div className="mini-grid">
184       {grid_square(0)}
185       {grid_square(1)}
186       {grid_square(2)}
187       {grid_square(3)}
188       {grid_square(4)}
189       {grid_square(5)}
190       {grid_square(6)}
191       {grid_square(7)}
192       {grid_square(8)}
193     </div>
194   );
195 }
196
197 class Board extends React.Component {
198   mini_grid(i) {
199     const squares = this.props.squares[i];
200     return (
201       <MiniGrid
202         squares={squares}
203         active={this.props.active}
204         onClick={(j) => this.props.onClick(i,j)}
205       />
206     );
207   }
208
209   render() {
210     return (
211       <div className="board-container">
212         <div className="board">
213           {this.mini_grid(0)}
214           {this.mini_grid(1)}
215           {this.mini_grid(2)}
216           {this.mini_grid(3)}
217           {this.mini_grid(4)}
218           {this.mini_grid(5)}
219           {this.mini_grid(6)}
220           {this.mini_grid(7)}
221           {this.mini_grid(8)}
222         </div>
223       </div>
224     );
225   }
226 }
227
228 function fetch_method_json(method, api = '', data = {}) {
229   const response = fetch(api, {
230     method: method,
231     headers: {
232       'Content-Type': 'application/json'
233     },
234     body: JSON.stringify(data)
235   });
236   return response;
237 }
238
239 function fetch_post_json(api = '', data = {}) {
240   return fetch_method_json('POST', api, data);
241 }
242
243 async function fetch_put_json(api = '', data = {}) {
244   return fetch_method_json('PUT', api, data);
245 }
246
247 class Game extends React.Component {
248   constructor(props) {
249     super(props);
250     this.state = {
251       game_info: {},
252       player_info: {},
253       other_players: [],
254       squares: Array(9).fill(null).map(() => Array(9).fill(null)),
255       moves: 0,
256       next_to_play: "+"
257     };
258   }
259
260   set_game_info(info) {
261     this.setState({
262       game_info: info
263     });
264   }
265
266   set_player_info(info) {
267     this.setState({
268       player_info: info
269     });
270   }
271
272   set_other_player_info(info) {
273     const other_players_copy = [...this.state.other_players];
274     const idx = other_players_copy.findIndex(o => o.id === info.id);
275     if (idx >= 0) {
276       other_players_copy[idx] = info;
277     } else {
278       other_players_copy.push(info);
279     }
280     this.setState({
281       other_players: other_players_copy
282     });
283   }
284
285   reset_board() {
286     this.setState({
287       next_to_play: "+"
288     });
289   }
290
291   receive_move(move) {
292     if (this.state.moves === 81) {
293       return;
294     }
295     const symbol = team_symbol(this.state.next_to_play);
296     const new_squares = this.state.squares.map(arr => arr.slice());
297     new_squares[move[0]][move[1]] = symbol;
298     let next_to_play;
299     if (this.state.next_to_play === "+")
300       next_to_play = "o";
301     else
302       next_to_play = "+";
303     this.setState({
304       squares: new_squares,
305       moves: this.state.moves + 1,
306       next_to_play: next_to_play
307     });
308   }
309
310   async handle_click(i, j, first_move) {
311     let move = {
312       move: [i, j]
313     };
314     if (first_move) {
315       move.assert_first_move = true;
316     }
317     const response = await fetch_post_json("move", move);
318     if (response.status == 200) {
319       const result = await response.json();
320       if (! result.legal)
321         add_message("danger", result.message);
322     } else {
323       add_message("danger", `Error occurred sending move`);
324     }
325   }
326
327   join_team(team) {
328     fetch_put_json("player", {team: team});
329   }
330
331   render() {
332     const state = this.state;
333     const first_move = state.moves === 0;
334     const my_team = state.player_info.team;
335     var board_active;
336
337     let status;
338     if (this.state.moves.length === 81)
339     {
340       status = "Game over";
341       board_active = false;
342     }
343     else if (first_move)
344     {
345       if (state.other_players.length == 0) {
346         status = "You can move or wait for another player to join.";
347       } else {
348         let qualifier;
349         if (state.other_players.length == 1) {
350           qualifier = "Either";
351         } else {
352           qualifier = "Any";
353         }
354         status = `${qualifier} player can make the first move.`;
355       }
356       board_active = true;
357     }
358     else if (my_team === "")
359     {
360       status = "You're just watching the game.";
361       board_active = false;
362     }
363     else if (my_team === state.next_to_play)
364     {
365       status = "Your turn. Make a move.";
366       board_active = true;
367     }
368     else
369     {
370       status = "Waiting for another player to ";
371       if (state.other_players.length == 0) {
372         status += "join.";
373       } else {
374         status += "move.";
375       }
376       board_active = false;
377     }
378
379     return [
380       <GameInfo
381         key="game-info"
382         id={state.game_info.id}
383         url={state.game_info.url}
384       />,
385       <PlayerInfo
386         key="player-info"
387         game={this}
388         first_move={first_move}
389         player={state.player_info}
390         other_players={state.other_players}
391       />,
392       <div key="game" className="game">
393         <div>{status}</div>
394         <div className="game-board">
395           <Board
396             active={board_active}
397             squares={state.squares}
398             onClick={(i,j) => this.handle_click(i, j, first_move)}
399           />
400         </div>
401       </div>
402     ];
403   }
404 }
405
406 ReactDOM.render(<Game
407                   ref={(me) => window.game = me}
408                 />, document.getElementById("scribe"));