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