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