]> git.cworth.org Git - lmno.games/blob - scribe/scribe.jsx
Initial implementation of Scribe
[lmno.games] / scribe / scribe.jsx
1 function team_symbol(team) {
2   if (team === "+")
3     return "🞥";
4   else
5     return "🞇";
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       add_message("danger", "Connection to server lost.");
30   }
31 };
32
33 events.addEventListener("game-info", event => {
34   const info = JSON.parse(event.data);
35
36   window.game.set_game_info(info);
37 });
38
39 events.addEventListener("player-info", event => {
40   const info = JSON.parse(event.data);
41
42   window.game.set_player_info(info);
43 });
44
45 events.addEventListener("player-enter", event => {
46   const info = JSON.parse(event.data);
47
48   window.game.set_other_player_info(info);
49 });
50
51 events.addEventListener("player-update", event => {
52   const info = JSON.parse(event.data);
53
54   if (info.id === window.game.state.player_info.id)
55     window.game.set_player_info(info);
56   else
57     window.game.set_other_player_info(info);
58 });
59
60 events.addEventListener("move", event => {
61   const move = JSON.parse(event.data);
62
63   window.game.receive_move(move);
64 });
65
66 events.addEventListener("game-state", event => {
67   const state = JSON.parse(event.data);
68
69   window.game.reset_board();
70
71   for (let square of state.moves) {
72     window.game.receive_move(square);
73   }
74 });
75
76 /*********************************************************
77  * Game and supporting classes                           *
78  *********************************************************/
79
80 function GameInfo(props) {
81   if (! props.id)
82     return null;
83
84   return (
85     <div className="game-info">
86       <h2>{props.id}</h2>
87       Invite a friend to play by sending this URL: {props.url}
88     </div>
89   );
90 }
91
92 function TeamButton(props) {
93   return <button className="inline"
94                  onClick={() => props.game.join_team(props.team)}>
95            {props.label}
96          </button>;
97 }
98
99 function TeamChoices(props) {
100   let other_team;
101   if (props.player.team === "+")
102     other_team = "o";
103   else
104     other_team = "+";
105
106   if (props.player.team === "") {
107     if (props.first_move) {
108       return null;
109     } else {
110       return [
111         <TeamButton key="+" game={props.game} team="+" label="Join 🞥" />,
112         " ",
113         <TeamButton key="o" game={props.game} team="o" label="Join 🞇" />
114       ];
115     }
116   } else {
117     return <TeamButton game={props.game} team={other_team} label="Switch" />;
118   }
119 }
120
121 function PlayerInfo(props) {
122   if (! props.player.id)
123     return null;
124
125   const choices = <TeamChoices
126                     game={props.game}
127                     first_move={props.first_move}
128                     player={props.player}
129                   />;
130
131   return (
132     <div className="player-info">
133       <h2>Players</h2>
134       {props.player.name}
135       {props.player.team ? ` (${props.player.team})` : ""}
136       {props.first_move ? "" : " "}
137       {choices}
138       {props.other_players.map(other => (
139         <span key={other.id}>
140           {", "}
141           {other.name}
142           {other.team ? ` (${other.team})` : ""}
143         </span>
144       ))}
145     </div>
146   );
147 }
148
149 function Square(props) {
150   let className = "square";
151
152   if (props.value) {
153     className += " occupied";
154   } else if (props.active) {
155     className += " open";
156   }
157
158   const onClick = props.active ? props.onClick : null;
159
160   return (
161     <div className={className}
162          onClick={onClick}>
163       {props.value}
164     </div>
165   );
166 }
167
168 class Board extends React.Component {
169   render_square(i,j) {
170     const value = this.props.squares[i][j];
171     return (
172       <Square
173         value={value}
174         active={this.props.active && ! value}
175         onClick={() => this.props.onClick(i,j)}
176       />
177     );
178   }
179
180   render() {
181     return (
182       <div>
183         <div className="board-row">
184           {this.render_square(0,0)}
185           {this.render_square(0,1)}
186           {this.render_square(0,2)}
187           {" "}
188           {this.render_square(1,0)}
189           {this.render_square(1,1)}
190           {this.render_square(1,2)}
191           {" "}
192           {this.render_square(2,0)}
193           {this.render_square(2,1)}
194           {this.render_square(2,2)}
195         </div>
196         <div className="board-row">
197           {this.render_square(0,3)}
198           {this.render_square(0,4)}
199           {this.render_square(0,5)}
200           {" "}
201           {this.render_square(1,3)}
202           {this.render_square(1,4)}
203           {this.render_square(1,5)}
204           {" "}
205           {this.render_square(2,3)}
206           {this.render_square(2,4)}
207           {this.render_square(2,5)}
208         </div>
209         <div className="board-row">
210           {this.render_square(0,6)}
211           {this.render_square(0,7)}
212           {this.render_square(0,8)}
213           {" "}
214           {this.render_square(1,6)}
215           {this.render_square(1,7)}
216           {this.render_square(1,8)}
217           {" "}
218           {this.render_square(2,6)}
219           {this.render_square(2,7)}
220           {this.render_square(2,8)}
221         </div>
222
223         <div className="board-row">
224         </div>
225
226         <div className="board-row">
227           {this.render_square(3,0)}
228           {this.render_square(3,1)}
229           {this.render_square(3,2)}
230           {" "}
231           {this.render_square(4,0)}
232           {this.render_square(4,1)}
233           {this.render_square(4,2)}
234           {" "}
235           {this.render_square(5,0)}
236           {this.render_square(5,1)}
237           {this.render_square(5,2)}
238         </div>
239         <div className="board-row">
240           {this.render_square(3,3)}
241           {this.render_square(3,4)}
242           {this.render_square(3,5)}
243           {" "}
244           {this.render_square(4,3)}
245           {this.render_square(4,4)}
246           {this.render_square(4,5)}
247           {" "}
248           {this.render_square(5,3)}
249           {this.render_square(5,4)}
250           {this.render_square(5,5)}
251         </div>
252         <div className="board-row">
253           {this.render_square(3,6)}
254           {this.render_square(3,7)}
255           {this.render_square(3,8)}
256           {" "}
257           {this.render_square(4,6)}
258           {this.render_square(4,7)}
259           {this.render_square(4,8)}
260           {" "}
261           {this.render_square(5,6)}
262           {this.render_square(5,7)}
263           {this.render_square(5,8)}
264         </div>
265
266         <div className="board-row">
267         </div>
268
269         <div className="board-row">
270           {this.render_square(6,0)}
271           {this.render_square(6,1)}
272           {this.render_square(6,2)}
273           {" "}
274           {this.render_square(7,0)}
275           {this.render_square(7,1)}
276           {this.render_square(7,2)}
277           {" "}
278           {this.render_square(8,0)}
279           {this.render_square(8,1)}
280           {this.render_square(8,2)}
281         </div>
282         <div className="board-row">
283           {this.render_square(6,3)}
284           {this.render_square(6,4)}
285           {this.render_square(6,5)}
286           {" "}
287           {this.render_square(7,3)}
288           {this.render_square(7,4)}
289           {this.render_square(7,5)}
290           {" "}
291           {this.render_square(8,3)}
292           {this.render_square(8,4)}
293           {this.render_square(8,5)}
294         </div>
295         <div className="board-row">
296           {this.render_square(6,6)}
297           {this.render_square(6,7)}
298           {this.render_square(6,8)}
299           {" "}
300           {this.render_square(7,6)}
301           {this.render_square(7,7)}
302           {this.render_square(7,8)}
303           {" "}
304           {this.render_square(8,6)}
305           {this.render_square(8,7)}
306           {this.render_square(8,8)}
307         </div>
308
309       </div>
310     );
311   }
312 }
313
314 function fetch_method_json(method, api = '', data = {}) {
315   const response = fetch(api, {
316     method: method,
317     headers: {
318       'Content-Type': 'application/json'
319     },
320     body: JSON.stringify(data)
321   });
322   return response;
323 }
324
325 function fetch_post_json(api = '', data = {}) {
326   return fetch_method_json('POST', api, data);
327 }
328
329 async function fetch_put_json(api = '', data = {}) {
330   return fetch_method_json('PUT', api, data);
331 }
332
333 class Game extends React.Component {
334   constructor(props) {
335     super(props);
336     this.state = {
337       game_info: {},
338       player_info: {},
339       other_players: [],
340       squares: Array(9).fill(null).map(() => Array(9).fill(null)),
341       moves: 0,
342       next_to_play: "+"
343     };
344   }
345
346   set_game_info(info) {
347     this.setState({
348       game_info: info
349     });
350   }
351
352   set_player_info(info) {
353     this.setState({
354       player_info: info
355     });
356   }
357
358   set_other_player_info(info) {
359     const other_players_copy = [...this.state.other_players];
360     const idx = other_players_copy.findIndex(o => o.id === info.id);
361     if (idx >= 0) {
362       other_players_copy[idx] = info;
363     } else {
364       other_players_copy.push(info);
365     }
366     this.setState({
367       other_players: other_players_copy
368     });
369   }
370
371   reset_board() {
372     this.setState({
373       next_to_play: "+"
374     });
375   }
376
377   receive_move(move) {
378     if (this.state.moves === 81) {
379       return;
380     }
381     const symbol = team_symbol(this.state.next_to_play);
382     const new_squares = this.state.squares.map(arr => arr.slice());
383     new_squares[move[0]][move[1]] = symbol;
384     let next_to_play;
385     if (this.state.next_to_play === "+")
386       next_to_play = "o";
387     else
388       next_to_play = "+";
389     this.setState({
390       squares: new_squares,
391       moves: this.state.moves + 1,
392       next_to_play: next_to_play
393     });
394   }
395
396   async handle_click(i, j, first_move) {
397     let move = {
398       move: [i, j]
399     };
400     if (first_move) {
401       move.assert_first_move = true;
402     }
403     const response = await fetch_post_json("move", move);
404     if (response.status == 200) {
405       const result = await response.json();
406       if (! result.legal)
407         add_message("danger", result.message);
408     } else {
409       add_message("danger", `Error occurred sending move`);
410     }
411   }
412
413   join_team(team) {
414     fetch_put_json("player", {team: team});
415   }
416
417   render() {
418     const state = this.state;
419     const first_move = state.moves === 0;
420     const my_team = state.player_info.team;
421     var board_active;
422
423     let status;
424     if (this.state.moves.length === 81)
425     {
426       status = "Game over";
427       board_active = false;
428     }
429     else if (first_move)
430     {
431       if (state.other_players.length == 0) {
432         status = "You can move or wait for another player to join.";
433       } else {
434         let qualifier;
435         if (state.other_players.length == 1) {
436           qualifier = "Either";
437         } else {
438           qualifier = "Any";
439         }
440         status = `${qualifier} player can make the first move.`;
441       }
442       board_active = true;
443     }
444     else if (my_team === "")
445     {
446       status = "You're just watching the game.";
447       board_active = false;
448     }
449     else if (my_team === state.next_to_play)
450     {
451       status = "Your turn. Make a move.";
452       board_active = true;
453     }
454     else
455     {
456       status = "Waiting for another player to ";
457       if (state.other_players.length == 0) {
458         status += "join.";
459       } else {
460         status += "move.";
461       }
462       board_active = false;
463     }
464
465     return [
466       <GameInfo
467         key="game-info"
468         id={state.game_info.id}
469         url={state.game_info.url}
470       />,
471       <PlayerInfo
472         key="player-info"
473         game={this}
474         first_move={first_move}
475         player={state.player_info}
476         other_players={state.other_players}
477       />,
478       <div key="game" className="game">
479         <div>{status}</div>
480         <div className="game-board">
481           <Board
482             active={board_active}
483             squares={state.squares}
484             onClick={(i,j) => this.handle_click(i, j, first_move)}
485           />
486         </div>
487       </div>
488     ];
489   }
490 }
491
492 ReactDOM.render(<Game
493                   ref={(me) => window.game = me}
494                 />, document.getElementById("scribe"));