]> git.cworth.org Git - lmno.games/blob - tictactoe/tictactoe.jsx
71690b91e2ecf7692790eeb5517080aa35ef5653
[lmno.games] / tictactoe / tictactoe.jsx
1 const Team = {
2   X: 0,
3   O: 1,
4   properties: {
5     0: {name: "X"},
6     1: {name: "O"}
7   }
8 };
9
10 function undisplay(element) {
11   element.style.display="none";
12 }
13
14 function add_message(severity, message) {
15   message = `<div class="message ${severity}" onclick="undisplay(this)">
16 <span class="hide-button" onclick="undisplay(this.parentElement)">&times;</span>
17 ${message}
18 </div>`;
19   const message_area = document.getElementById('message-area');
20   message_area.insertAdjacentHTML('beforeend', message);
21 }
22
23 /*********************************************************
24  * Handling server-sent event stream                     *
25  *********************************************************/
26
27 const events = new EventSource("events");
28
29 events.onerror = function(event) {
30   if (event.target.readyState === EventSource.CLOSED) {
31       add_message("danger", "Connection to server lost.");
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-update", event => {
48   const info = JSON.parse(event.data);
49
50   if (info.id === window.game.state.player_info.id)
51     window.game.set_player_info(info);
52 });
53
54 events.addEventListener("move", event => {
55   const move = JSON.parse(event.data);
56
57   window.game.receive_move(move);
58 });
59
60 events.addEventListener("game-state", event => {
61   const state = JSON.parse(event.data);
62
63   window.game.reset_board();
64
65   for (let square of state.moves) {
66     window.game.receive_move(square);
67   }
68 });
69
70 /*********************************************************
71  * Game and supporting classes                           *
72  *********************************************************/
73
74 function GameInfo(props) {
75   return (
76     <div className="game-info">
77       <h2>{props.id}</h2>
78       Invite a friend to play by sending this URL: {props.url}
79     </div>
80   );
81 }
82
83 function PlayerInfo(props) {
84   return (
85     <div className="player-info">
86       <h2>Player</h2>
87       {props.name}, ID: {props.id}, on team: {props.team}
88     </div>
89   );
90 }
91
92 function Square(props) {
93   let className = "square";
94
95   if (props.value) {
96     className += " occupied";
97   } else if (props.active) {
98     className += " open";
99   }
100
101   const onClick = props.active ? props.onClick : null;
102
103   return (
104     <div className={className}
105          onClick={onClick}>
106       {props.value}
107     </div>
108   );
109 }
110
111 class Board extends React.Component {
112   render_square(i) {
113     const value = this.props.squares[i];
114     return (
115       <Square
116         value={value}
117         active={! this.props.game_over && ! value}
118         onClick={() => this.props.onClick(i)}
119       />
120     );
121   }
122
123   render() {
124     return (
125       <div>
126         <div className="board-row">
127           {this.render_square(0)}
128           {this.render_square(1)}
129           {this.render_square(2)}
130         </div>
131         <div className="board-row">
132           {this.render_square(3)}
133           {this.render_square(4)}
134           {this.render_square(5)}
135         </div>
136         <div className="board-row">
137           {this.render_square(6)}
138           {this.render_square(7)}
139           {this.render_square(8)}
140         </div>
141       </div>
142     );
143   }
144 }
145
146 function fetch_method_json(method, api = '', data = {}) {
147   const response = fetch(api, {
148     method: method,
149     headers: {
150       'Content-Type': 'application/json'
151     },
152     body: JSON.stringify(data)
153   });
154   return response;
155 }
156
157 function fetch_post_json(api = '', data = {}) {
158   return fetch_method_json('POST', api, data);
159 }
160
161 async function fetch_put_json(api = '', data = {}) {
162   return fetch_method_json('PUT', api, data);
163 }
164
165 class Game extends React.Component {
166   constructor(props) {
167     super(props);
168     this.state = {
169       game_info: {},
170       player_info: {},
171       history: [
172         {
173           squares: Array(9).fill(null)
174         }
175       ],
176       step_number: 0,
177       next_to_play: Team.X
178     };
179   }
180
181   set_game_info(info) {
182     this.setState({
183       game_info: info
184     });
185   }
186
187   set_player_info(info) {
188     this.setState({
189       player_info: info
190     });
191   }
192
193   send_move(i) {
194     return fetch_post_json("move", { move: i });
195   }
196
197   reset_board() {
198     this.setState({
199       history: [
200         {
201           squares: Array(9).fill(null)
202         }
203       ],
204       step_number: 0,
205       next_to_play: Team.X
206     });
207   }
208
209   receive_move(i) {
210     const history = this.state.history.slice(0, this.state.step_number + 1);
211     const current = history[history.length - 1];
212     const squares = current.squares.slice();
213     if (calculate_winner(squares) || squares[i]) {
214       return;
215     }
216     squares[i] = Team.properties[this.state.next_to_play].name;
217     let next_to_play;
218     if (this.state.next_to_play === Team.X)
219       next_to_play = Team.O;
220     else
221       next_to_play = Team.X;
222     this.setState({
223       history: history.concat([
224         {
225           squares: squares
226         }
227       ]),
228       step_number: history.length,
229       next_to_play: next_to_play
230     });
231   }
232
233   async handle_click(i) {
234     const response = await this.send_move(i);
235     if (response.status == 200) {
236       const result = await response.json();
237       if (! result.legal)
238         add_message("danger", result.message);
239     } else {
240       add_message("danger", `Error occurred sending move`);
241     }
242   }
243
244   render() {
245     const history = this.state.history;
246     const current = history[this.state.step_number];
247     const winner = calculate_winner(current.squares);
248
249     let status;
250     if (winner) {
251       status = "Winner: " + winner;
252     } else {
253       status = "Next player: " + (Team.properties[this.state.next_to_play].name);
254     }
255
256     return [
257       <GameInfo
258         key="game-info"
259         id={this.state.game_info.id}
260         url={this.state.game_info.url}
261       />,
262       <PlayerInfo
263         key="player-info"
264         id={this.state.player_info.id}
265         name={this.state.player_info.name}
266         team={this.state.player_info.team}
267       />,
268       <div key="game" className="game">
269         <div>{status}</div>
270         <div className="game-board">
271           <Board
272             game_over={winner}
273             squares={current.squares}
274             onClick={i => this.handle_click(i)}
275           />
276         </div>
277       </div>
278     ];
279   }
280 }
281
282 ReactDOM.render(<Game
283                   ref={(me) => window.game = me}
284                 />, document.getElementById("tictactoe"));
285
286 function calculate_winner(squares) {
287   const lines = [
288     [0, 1, 2],
289     [3, 4, 5],
290     [6, 7, 8],
291     [0, 3, 6],
292     [1, 4, 7],
293     [2, 5, 8],
294     [0, 4, 8],
295     [2, 4, 6]
296   ];
297   for (let i = 0; i < lines.length; i++) {
298     const [a, b, c] = lines[i];
299     if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
300       return squares[a];
301     }
302   }
303   return null;
304 }