]> git.cworth.org Git - lmno.games/blob - scribe/scribe.jsx
Add a reference diagram of the 19 glyphs of Scribe
[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 Glyph(props) {
169
170   const glyph_dots = [];
171
172   for (let row = 0; row < 3; row++) {
173     for (let col = 0; col < 3; col++) {
174       if (props.squares[3 * row + col]) {
175         let cy = 10 + 20 * row;
176         let cx = 10 + 20 * col;
177         glyph_dots.push(
178           <circle
179             cx={cx}
180             cy={cy}
181             r="8"
182           />
183         );
184       }
185     }
186   }
187
188   return (<div className="glyph-and-name">
189             {props.name}
190             <div className="glyph">
191               <svg viewBox="0 0 60 60">
192                 <g fill="#287789">
193                   {glyph_dots}
194                 </g>
195               </svg>
196             </div>
197           </div>
198          );
199 }
200
201 function Square(props) {
202   let className = "square";
203
204   if (props.value) {
205     className += " occupied";
206   } else if (props.active) {
207     className += " open";
208   }
209
210   const onClick = props.active ? props.onClick : null;
211
212   return (
213     <div className={className}
214          onClick={onClick}>
215       {props.value}
216     </div>
217   );
218 }
219
220 function MiniGrid(props) {
221   function grid_square(j) {
222     const value = props.squares[j];
223     return (
224       <Square
225         value={value}
226         active={props.active}
227         onClick={() => props.onClick(j)}
228       />
229     );
230   }
231
232   return (
233     <div className="mini-grid">
234       {grid_square(0)}
235       {grid_square(1)}
236       {grid_square(2)}
237       {grid_square(3)}
238       {grid_square(4)}
239       {grid_square(5)}
240       {grid_square(6)}
241       {grid_square(7)}
242       {grid_square(8)}
243     </div>
244   );
245 }
246
247 class Board extends React.Component {
248   mini_grid(i) {
249     const squares = this.props.squares[i];
250     return (
251       <MiniGrid
252         squares={squares}
253         active={this.props.active}
254         onClick={(j) => this.props.onClick(i,j)}
255       />
256     );
257   }
258
259   render() {
260     return (
261       <div className="board-container">
262         <div className="board">
263           {this.mini_grid(0)}
264           {this.mini_grid(1)}
265           {this.mini_grid(2)}
266           {this.mini_grid(3)}
267           {this.mini_grid(4)}
268           {this.mini_grid(5)}
269           {this.mini_grid(6)}
270           {this.mini_grid(7)}
271           {this.mini_grid(8)}
272         </div>
273       </div>
274     );
275   }
276 }
277
278 function fetch_method_json(method, api = '', data = {}) {
279   const response = fetch(api, {
280     method: method,
281     headers: {
282       'Content-Type': 'application/json'
283     },
284     body: JSON.stringify(data)
285   });
286   return response;
287 }
288
289 function fetch_post_json(api = '', data = {}) {
290   return fetch_method_json('POST', api, data);
291 }
292
293 async function fetch_put_json(api = '', data = {}) {
294   return fetch_method_json('PUT', api, data);
295 }
296
297 class Game extends React.Component {
298   constructor(props) {
299     super(props);
300     this.state = {
301       game_info: {},
302       player_info: {},
303       other_players: [],
304       squares: [...Array(9)].map(() => Array(9).fill(null)),
305       moves: 0,
306       next_to_play: "+"
307     };
308   }
309
310   set_game_info(info) {
311     this.setState({
312       game_info: info
313     });
314   }
315
316   set_player_info(info) {
317     this.setState({
318       player_info: info
319     });
320   }
321
322   set_other_player_info(info) {
323     const other_players_copy = [...this.state.other_players];
324     const idx = other_players_copy.findIndex(o => o.id === info.id);
325     if (idx >= 0) {
326       other_players_copy[idx] = info;
327     } else {
328       other_players_copy.push(info);
329     }
330     this.setState({
331       other_players: other_players_copy
332     });
333   }
334
335   reset_board() {
336     this.setState({
337       next_to_play: "+"
338     });
339   }
340
341   receive_move(move) {
342     if (this.state.moves === 81) {
343       return;
344     }
345     const symbol = team_symbol(this.state.next_to_play);
346     const new_squares = this.state.squares.map(arr => arr.slice());
347     new_squares[move[0]][move[1]] = symbol;
348     let next_to_play;
349     if (this.state.next_to_play === "+")
350       next_to_play = "o";
351     else
352       next_to_play = "+";
353     this.setState({
354       squares: new_squares,
355       moves: this.state.moves + 1,
356       next_to_play: next_to_play
357     });
358   }
359
360   async handle_click(i, j, first_move) {
361     let move = {
362       move: [i, j]
363     };
364     if (first_move) {
365       move.assert_first_move = true;
366     }
367     const response = await fetch_post_json("move", move);
368     if (response.status == 200) {
369       const result = await response.json();
370       if (! result.legal)
371         add_message("danger", result.message);
372     } else {
373       add_message("danger", `Error occurred sending move`);
374     }
375   }
376
377   join_team(team) {
378     fetch_put_json("player", {team: team});
379   }
380
381   render() {
382     const state = this.state;
383     const first_move = state.moves === 0;
384     const my_team = state.player_info.team;
385     var board_active;
386
387     let status;
388     if (this.state.moves.length === 81)
389     {
390       status = "Game over";
391       board_active = false;
392     }
393     else if (first_move)
394     {
395       if (state.other_players.length == 0) {
396         status = "You can move or wait for another player to join.";
397       } else {
398         let qualifier;
399         if (state.other_players.length == 1) {
400           qualifier = "Either";
401         } else {
402           qualifier = "Any";
403         }
404         status = `${qualifier} player can make the first move.`;
405       }
406       board_active = true;
407     }
408     else if (my_team === "")
409     {
410       status = "You're just watching the game.";
411       board_active = false;
412     }
413     else if (my_team === state.next_to_play)
414     {
415       status = "Your turn. Make a move.";
416       board_active = true;
417     }
418     else
419     {
420       status = "Waiting for another player to ";
421       if (state.other_players.length == 0) {
422         status += "join.";
423       } else {
424         status += "move.";
425       }
426       board_active = false;
427     }
428
429     return [
430       <GameInfo
431         key="game-info"
432         id={state.game_info.id}
433         url={state.game_info.url}
434       />,
435       <PlayerInfo
436         key="player-info"
437         game={this}
438         first_move={first_move}
439         player={state.player_info}
440         other_players={state.other_players}
441       />,
442       <div key="game" className="game">
443         <div>{status}</div>
444         <div className="game-board">
445           <Board
446             active={board_active}
447             squares={state.squares}
448             onClick={(i,j) => this.handle_click(i, j, first_move)}
449           />
450         </div>
451       </div>,
452       <div key="glyphs" className="glyphs">
453         <Glyph
454           name="Single"
455           squares={[1,0,0,
456                     0,0,0,
457                     0,0,0]}
458         />
459         <Glyph
460           name="Double"
461           squares={[1,1,0,
462                     0,0,0,
463                     0,0,0]}
464         />
465         <Glyph
466           name="Line"
467           squares={[1,1,1,
468                     0,0,0,
469                     0,0,0]}
470         />
471         <Glyph
472           name="Pipe"
473           squares={[0,0,1,
474                     1,1,1,
475                     0,0,0]}
476         />
477         <Glyph
478           name="Squat-T"
479           squares={[1,1,1,
480                     0,1,0,
481                     0,0,0]}
482         />
483         <Glyph
484           name="4-block"
485           squares={[1,1,0,
486                     1,1,0,
487                     0,0,0]}
488         />
489         <Glyph
490           name="T"
491           squares={[1,1,1,
492                     0,1,0,
493                     0,1,0]}
494                 />
495         <Glyph
496           name="Cross"
497           squares={[0,1,0,
498                     1,1,1,
499                     0,1,0]}
500         />
501         <Glyph
502           name="6-block"
503           squares={[1,1,1,
504                     1,1,1,
505                     0,0,0]}
506         />
507         <Glyph
508           name="Bomber"
509           squares={[1,1,1,
510                     0,1,1,
511                     0,0,1]}
512         />
513         <Glyph
514           name="Chair"
515           squares={[0,0,1,
516                     1,1,1,
517                     1,0,1]}
518         />
519         <Glyph
520           name="J"
521           squares={[0,0,1,
522                     1,0,1,
523                     1,1,1]}
524         />
525         <Glyph
526           name="Earring"
527           squares={[0,1,1,
528                     1,0,1,
529                     1,1,1]}
530         />
531         <Glyph
532           name="House"
533           squares={[0,1,0,
534                     1,1,1,
535                     1,1,1]}
536         />
537         <Glyph
538           name="H"
539           squares={[1,0,1,
540                     1,1,1,
541                     1,0,1]}
542         />
543         <Glyph
544           name="U"
545           squares={[1,0,1,
546                     1,0,1,
547                     1,1,1]}
548         />
549         <Glyph
550           name="Ottoman"
551           squares={[1,1,1,
552                     1,1,1,
553                     1,0,1]}
554         />
555         <Glyph
556           name="O"
557           squares={[1,1,1,
558                     1,0,1,
559                     1,1,1]}
560         />
561         <Glyph
562           name="9-block"
563           squares={[1,1,1,
564                     1,1,1,
565                     1,1,1]}
566         />
567       </div>
568     ];
569   }
570 }
571
572 ReactDOM.render(<Game
573                   ref={(me) => window.game = me}
574                 />, document.getElementById("scribe"));