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