]> git.cworth.org Git - lmno.games/blob - scribe/scribe.jsx
Scribe: Style the board so there are clear gaps between the mini grids
[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 function MiniGrid(props) {
169   function grid_square(j) {
170     const value = props.squares[j];
171     return (
172       <Square
173         value={value}
174         active={props.active}
175         onClick={() => props.onClick(j)}
176       />
177     );
178   }
179
180   return (
181     <div className="mini-grid">
182       {grid_square(0)}
183       {grid_square(1)}
184       {grid_square(2)}
185       {grid_square(3)}
186       {grid_square(4)}
187       {grid_square(5)}
188       {grid_square(6)}
189       {grid_square(7)}
190       {grid_square(8)}
191     </div>
192   );
193 }
194
195 class Board extends React.Component {
196   mini_grid(i) {
197     const squares = this.props.squares[i];
198     return (
199       <MiniGrid
200         squares={squares}
201         active={this.props.active}
202         onClick={(j) => this.props.onClick(i,j)}
203       />
204     );
205   }
206
207   render() {
208     return (
209       <div className="board-container">
210         <div className="board">
211           {this.mini_grid(0)}
212           {this.mini_grid(1)}
213           {this.mini_grid(2)}
214           {this.mini_grid(3)}
215           {this.mini_grid(4)}
216           {this.mini_grid(5)}
217           {this.mini_grid(6)}
218           {this.mini_grid(7)}
219           {this.mini_grid(8)}
220         </div>
221       </div>
222     );
223   }
224 }
225
226 function fetch_method_json(method, api = '', data = {}) {
227   const response = fetch(api, {
228     method: method,
229     headers: {
230       'Content-Type': 'application/json'
231     },
232     body: JSON.stringify(data)
233   });
234   return response;
235 }
236
237 function fetch_post_json(api = '', data = {}) {
238   return fetch_method_json('POST', api, data);
239 }
240
241 async function fetch_put_json(api = '', data = {}) {
242   return fetch_method_json('PUT', api, data);
243 }
244
245 class Game extends React.Component {
246   constructor(props) {
247     super(props);
248     this.state = {
249       game_info: {},
250       player_info: {},
251       other_players: [],
252       squares: Array(9).fill(null).map(() => Array(9).fill(null)),
253       moves: 0,
254       next_to_play: "+"
255     };
256   }
257
258   set_game_info(info) {
259     this.setState({
260       game_info: info
261     });
262   }
263
264   set_player_info(info) {
265     this.setState({
266       player_info: info
267     });
268   }
269
270   set_other_player_info(info) {
271     const other_players_copy = [...this.state.other_players];
272     const idx = other_players_copy.findIndex(o => o.id === info.id);
273     if (idx >= 0) {
274       other_players_copy[idx] = info;
275     } else {
276       other_players_copy.push(info);
277     }
278     this.setState({
279       other_players: other_players_copy
280     });
281   }
282
283   reset_board() {
284     this.setState({
285       next_to_play: "+"
286     });
287   }
288
289   receive_move(move) {
290     if (this.state.moves === 81) {
291       return;
292     }
293     const symbol = team_symbol(this.state.next_to_play);
294     const new_squares = this.state.squares.map(arr => arr.slice());
295     new_squares[move[0]][move[1]] = symbol;
296     let next_to_play;
297     if (this.state.next_to_play === "+")
298       next_to_play = "o";
299     else
300       next_to_play = "+";
301     this.setState({
302       squares: new_squares,
303       moves: this.state.moves + 1,
304       next_to_play: next_to_play
305     });
306   }
307
308   async handle_click(i, j, first_move) {
309     let move = {
310       move: [i, j]
311     };
312     if (first_move) {
313       move.assert_first_move = true;
314     }
315     const response = await fetch_post_json("move", move);
316     if (response.status == 200) {
317       const result = await response.json();
318       if (! result.legal)
319         add_message("danger", result.message);
320     } else {
321       add_message("danger", `Error occurred sending move`);
322     }
323   }
324
325   join_team(team) {
326     fetch_put_json("player", {team: team});
327   }
328
329   render() {
330     const state = this.state;
331     const first_move = state.moves === 0;
332     const my_team = state.player_info.team;
333     var board_active;
334
335     let status;
336     if (this.state.moves.length === 81)
337     {
338       status = "Game over";
339       board_active = false;
340     }
341     else if (first_move)
342     {
343       if (state.other_players.length == 0) {
344         status = "You can move or wait for another player to join.";
345       } else {
346         let qualifier;
347         if (state.other_players.length == 1) {
348           qualifier = "Either";
349         } else {
350           qualifier = "Any";
351         }
352         status = `${qualifier} player can make the first move.`;
353       }
354       board_active = true;
355     }
356     else if (my_team === "")
357     {
358       status = "You're just watching the game.";
359       board_active = false;
360     }
361     else if (my_team === state.next_to_play)
362     {
363       status = "Your turn. Make a move.";
364       board_active = true;
365     }
366     else
367     {
368       status = "Waiting for another player to ";
369       if (state.other_players.length == 0) {
370         status += "join.";
371       } else {
372         status += "move.";
373       }
374       board_active = false;
375     }
376
377     return [
378       <GameInfo
379         key="game-info"
380         id={state.game_info.id}
381         url={state.game_info.url}
382       />,
383       <PlayerInfo
384         key="player-info"
385         game={this}
386         first_move={first_move}
387         player={state.player_info}
388         other_players={state.other_players}
389       />,
390       <div key="game" className="game">
391         <div>{status}</div>
392         <div className="game-board">
393           <Board
394             active={board_active}
395             squares={state.squares}
396             onClick={(i,j) => this.handle_click(i, j, first_move)}
397           />
398         </div>
399       </div>
400     ];
401   }
402 }
403
404 ReactDOM.render(<Game
405                   ref={(me) => window.game = me}
406                 />, document.getElementById("scribe"));