]> git.cworth.org Git - lmno.games/blob - tictactoe/tictactoe.jsx
42c32ef29041345a03d2ca2ddb7ae5cbea5fd2c2
[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 const events = new EventSource("events");
24
25 events.onerror = function(event) {
26   if (event.target.readyState === EventSource.CLOSED) {
27       add_message("danger", "Connection to server lost.");
28   }
29 };
30
31 events.addEventListener("move", event => {
32   const move = JSON.parse(event.data);
33
34   window.game.receiveMove(move);
35 });
36
37 events.addEventListener("game-state", event => {
38   const state = JSON.parse(event.data);
39
40   window.game.resetState();
41
42   for (let square of state.moves) {
43     window.game.receiveMove(square);
44   }
45 });
46
47 function Square(props) {
48   let className = "square";
49
50   if (props.value) {
51     className += " occupied";
52   } else if (props.active) {
53     className += " open";
54   }
55
56   const onClick = props.active ? props.onClick : null;
57
58   return (
59     <div className={className}
60          onClick={onClick}>
61       {props.value}
62     </div>
63   );
64 }
65
66 class Board extends React.Component {
67   renderSquare(i) {
68     const value = this.props.squares[i];
69     return (
70       <Square
71         value={value}
72         active={! this.props.gameOver && ! value}
73         onClick={() => this.props.onClick(i)}
74       />
75     );
76   }
77
78   render() {
79     return (
80       <div>
81         <div className="board-row">
82           {this.renderSquare(0)}
83           {this.renderSquare(1)}
84           {this.renderSquare(2)}
85         </div>
86         <div className="board-row">
87           {this.renderSquare(3)}
88           {this.renderSquare(4)}
89           {this.renderSquare(5)}
90         </div>
91         <div className="board-row">
92           {this.renderSquare(6)}
93           {this.renderSquare(7)}
94           {this.renderSquare(8)}
95         </div>
96       </div>
97     );
98   }
99 }
100
101 function fetch_post_json(api = '', data = {}) {
102   const response = fetch(api, {
103     method: 'POST',
104     headers: {
105       'Content-Type': 'application/json'
106     },
107     body: JSON.stringify(data)
108   });
109   return response;
110 }
111
112 class Game extends React.Component {
113   constructor(props) {
114     super(props);
115     this.state = {
116       history: [
117         {
118           squares: Array(9).fill(null)
119         }
120       ],
121       stepNumber: 0,
122       next_to_play: Team.X
123     };
124   }
125
126   sendMove(i) {
127     return fetch_post_json("move", { move: i });
128   }
129
130   resetState() {
131     this.setState({
132       history: [
133         {
134           squares: Array(9).fill(null)
135         }
136       ],
137       stepNumber: 0,
138       next_to_play: Team.X
139     });
140   }
141
142   receiveMove(i) {
143     const history = this.state.history.slice(0, this.state.stepNumber + 1);
144     const current = history[history.length - 1];
145     const squares = current.squares.slice();
146     if (calculateWinner(squares) || squares[i]) {
147       return;
148     }
149     squares[i] = Team.properties[this.state.next_to_play].name;
150     let next_to_play;
151     if (this.state.next_to_play === Team.X)
152       next_to_play = Team.O;
153     else
154       next_to_play = Team.X;
155     this.setState({
156       history: history.concat([
157         {
158           squares: squares
159         }
160       ]),
161       stepNumber: history.length,
162       next_to_play: next_to_play
163     });
164   }
165
166   async handleClick(i) {
167     const response = await this.sendMove(i);
168     if (response.status == 200) {
169       const result = await response.json();
170       if (! result.legal)
171         add_message("danger", result.message);
172     } else {
173       add_message("danger", `Error occurred sending move`);
174     }
175   }
176
177   jumpTo(step) {
178     this.setState({
179       stepNumber: step,
180       next_to_play: (step % 2) === 0
181     });
182   }
183
184   render() {
185     const history = this.state.history;
186     const current = history[this.state.stepNumber];
187     const winner = calculateWinner(current.squares);
188
189     let status;
190     if (winner) {
191       status = "Winner: " + winner;
192     } else {
193       status = "Next player: " + (Team.properties[this.state.next_to_play].name);
194     }
195
196     return (
197       <div className="game">
198         <div className="game-info">
199           <div>{status}</div>
200         </div>
201         <div className="game-board">
202           <Board
203             gameOver={winner}
204             squares={current.squares}
205             onClick={i => this.handleClick(i)}
206           />
207         </div>
208       </div>
209     );
210   }
211 }
212
213 // ========================================
214
215 ReactDOM.render(<Game
216                   ref={(me) => window.game = me}
217                 />, document.getElementById("tictactoe"));
218
219 function calculateWinner(squares) {
220   const lines = [
221     [0, 1, 2],
222     [3, 4, 5],
223     [6, 7, 8],
224     [0, 3, 6],
225     [1, 4, 7],
226     [2, 5, 8],
227     [0, 4, 8],
228     [2, 4, 6]
229   ];
230   for (let i = 0; i < lines.length; i++) {
231     const [a, b, c] = lines[i];
232     if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
233       return squares[a];
234     }
235   }
236   return null;
237 }