]> git.cworth.org Git - lmno.games/blob - tictactoe/tictactoe.jsx
Expect defailed error message from the server when rejecting a 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   return (
40     <button className="square" onClick={props.onClick}>
41       {props.value}
42     </button>
43   );
44 }
45
46 class Board extends React.Component {
47   renderSquare(i) {
48     return (
49       <Square
50         value={this.props.squares[i]}
51         onClick={() => this.props.onClick(i)}
52       />
53     );
54   }
55
56   render() {
57     return (
58       <div>
59         <div className="board-row">
60           {this.renderSquare(0)}
61           {this.renderSquare(1)}
62           {this.renderSquare(2)}
63         </div>
64         <div className="board-row">
65           {this.renderSquare(3)}
66           {this.renderSquare(4)}
67           {this.renderSquare(5)}
68         </div>
69         <div className="board-row">
70           {this.renderSquare(6)}
71           {this.renderSquare(7)}
72           {this.renderSquare(8)}
73         </div>
74       </div>
75     );
76   }
77 }
78
79 function fetch_post_json(api = '', data = {}) {
80   const response = fetch(api, {
81     method: 'POST',
82     headers: {
83       'Content-Type': 'application/json'
84     },
85     body: JSON.stringify(data)
86   });
87   return response;
88 }
89
90 class Game extends React.Component {
91   constructor(props) {
92     super(props);
93     this.state = {
94       history: [
95         {
96           squares: Array(9).fill(null)
97         }
98       ],
99       stepNumber: 0,
100       xIsNext: true
101     };
102   }
103
104   sendMove(i) {
105     return fetch_post_json("move", { move: i });
106   }
107
108   resetState() {
109     this.setState({
110       history: [
111         {
112           squares: Array(9).fill(null)
113         }
114       ],
115       stepNumber: 0,
116       xIsNext: true
117     });
118   }
119
120   receiveMove(i) {
121     const history = this.state.history.slice(0, this.state.stepNumber + 1);
122     const current = history[history.length - 1];
123     const squares = current.squares.slice();
124     if (calculateWinner(squares) || squares[i]) {
125       return;
126     }
127     squares[i] = this.state.xIsNext ? "X" : "O";
128     this.setState({
129       history: history.concat([
130         {
131           squares: squares
132         }
133       ]),
134       stepNumber: history.length,
135       xIsNext: !this.state.xIsNext
136     });
137   }
138
139   async handleClick(i) {
140     const response = await this.sendMove(i);
141     if (response.status == 200) {
142       const result = await response.json();
143       if (! result.legal)
144         add_message("danger", result.message);
145     } else {
146       add_message("danger", `Error occurred sending move`);
147     }
148   }
149
150   jumpTo(step) {
151     this.setState({
152       stepNumber: step,
153       xIsNext: (step % 2) === 0
154     });
155   }
156
157   render() {
158     const history = this.state.history;
159     const current = history[this.state.stepNumber];
160     const winner = calculateWinner(current.squares);
161
162     let status;
163     if (winner) {
164       status = "Winner: " + winner;
165     } else {
166       status = "Next player: " + (this.state.xIsNext ? "X" : "O");
167     }
168
169     return (
170       <div className="game">
171         <div className="game-info">
172           <div>{status}</div>
173         </div>
174         <div className="game-board">
175           <Board
176             squares={current.squares}
177             onClick={i => this.handleClick(i)}
178           />
179         </div>
180       </div>
181     );
182   }
183 }
184
185 // ========================================
186
187 ReactDOM.render(<Game
188                   ref={(me) => window.game = me}
189                 />, document.getElementById("tictactoe"));
190
191 function calculateWinner(squares) {
192   const lines = [
193     [0, 1, 2],
194     [3, 4, 5],
195     [6, 7, 8],
196     [0, 3, 6],
197     [1, 4, 7],
198     [2, 5, 8],
199     [0, 4, 8],
200     [2, 4, 6]
201   ];
202   for (let i = 0; i < lines.length; i++) {
203     const [a, b, c] = lines[i];
204     if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
205       return squares[a];
206     }
207   }
208   return null;
209 }