Skip to content

Commit f44779f

Browse files
committed
added tic tac toe example
1 parent 54f306c commit f44779f

File tree

6 files changed

+752
-4
lines changed

6 files changed

+752
-4
lines changed

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* [Lifecycle Methods](client-dsl/lifecycle-methods.md)
1818
* [Component State](client-dsl/state.md)
1919
* [Events and Callbacks](client-dsl/events-and-callbacks.md)
20+
* [Interlude: Tic Tac Toe](client-dsl/interlude-tic-tac-toe.md)
2021
* [Recovering from Errors](client-dsl/error-recovery.md)
2122
* [JavaScript Components](client-dsl/javascript-components.md)
2223
* [Elements and Rendering](client-dsl/elements-and-rendering.md)

docs/client-dsl/error-recovery.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,5 @@ The data passed to the rescue handler is an array of two items, the Ruby error t
7979

8080
## Caveats
8181

82-
1. You cannot rescue errors raised in lifecycle handlers in the same component. Errors raised by lifecycle handlers in inner components are fine, just not
83-
in the same component as the rescue.
82+
1. You cannot rescue errors raised in lifecycle handlers in the same component. Errors raised by lifecycle handlers in inner components are fine, just not in the same component as the rescue.
8483
2. Errors raised in event handlers will neither stop the rendering cycle, nor will they be caught by a rescue callback.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
At this point if you have been reading sequentially through these chapters you know enough to put together a simple tic-tac-toe game.
2+
3+
### The Game Board
4+
5+
The board is represented by an array of 9 cells. Cell 0 is the top left square, and cell 8 is the bottom right.
6+
7+
Each cell will contain nil, an :X or an :O.
8+
9+
### Displaying the Board
10+
11+
The `DisplayBoard` component displays a board. `DisplayBoard` accepts a `board` param, and will fire back a `clicked_at` event when the user clicks one of the squares.
12+
13+
A small helper function `draw_squares` draws an individual square which is displayed as a `BUTTON`. A click handler is attached which
14+
will fire the `clicked_at` event with the appropriate cell id.
15+
16+
Notice that `DisplayBoard` has no internal state of its own. That is handled by the `Game` component.
17+
18+
```ruby
19+
class DisplayBoard < HyperComponent
20+
param :board
21+
fires :clicked_at
22+
23+
def draw_square(id)
24+
BUTTON(class: :square, id: id) { board[id] }
25+
.on(:click) { clicked_at!(id) }
26+
end
27+
28+
render(DIV) do
29+
(0..6).step(3) do |row|
30+
DIV(class: :board_row) do
31+
(row..row + 2).each { |id| draw_square(id) }
32+
end
33+
end
34+
end
35+
end
36+
```
37+
38+
### The Game State
39+
40+
The `Game` component has two state variables:
41+
+ `@history` which is an array of boards, each board being the array of cells.
42+
+ `@step` which is the current step in the history (we begin at zero)
43+
44+
`@step` and `@history` allows the player to move backwards or forwards and replay parts of the game.
45+
46+
These are initialized in the `before_mount` callback. Because Ruby will adjust the array size as needed
47+
and return nil if an array value is not initialized, we can simply initialize the board to an empty array.
48+
49+
There are two reader methods that read the state:
50+
51+
+ `player` returns the current player's token. The first player is always :X so even steps
52+
are :X, and odd steps are :O.
53+
+ `current` returns the board at the current step.
54+
+ `history` uses state_reader to encapsulate the history state.
55+
56+
Encapsulated access to state in reader methods like this is not necessary but is good practice
57+
58+
```ruby
59+
class Game < HyperComponent
60+
before_mount do
61+
@history = [[]]
62+
@step = 0
63+
end
64+
65+
def player
66+
@step.even? ? :X : :O
67+
end
68+
69+
def current
70+
@history[@step]
71+
end
72+
73+
state_reader :history
74+
end
75+
```
76+
77+
### Calculating the Winner Based on the Game State
78+
79+
We also have a `current_winner?` method that will return the winning player or nil based on the value of the current board:
80+
81+
```ruby
82+
class Game < HyperComponent
83+
WINNING_COMBOS = [
84+
[0, 1, 2],
85+
[3, 4, 5],
86+
[6, 7, 8],
87+
[0, 3, 6],
88+
[1, 4, 7],
89+
[2, 5, 8],
90+
[0, 4, 8],
91+
[2, 4, 6]
92+
]
93+
94+
def current_winner?
95+
WINNING_COMBOS.each do |a, b, c|
96+
return current[a] if current[a] &&
97+
current[a] == current[b] &&
98+
current[a] == current[c]
99+
end
100+
false
101+
end
102+
end
103+
```
104+
105+
### Mutating the Game State
106+
107+
There are two mutator methods that change state:
108+
+ `handle_click!` is called with the id of the square when a user clicks on a square.
109+
+ `jump_to!` moves the user back and forth through the history.
110+
111+
The `handle_click!` mutator first checks to make sure that no one has already won at the current step, and that
112+
no one has played in the cell that the user clicked on. If either of these conditions is true `handle_click!`
113+
returns and nothing changes.
114+
115+
To update the board `handle_click!` duplicates the squares; adds the player's token to the cell; makes a new
116+
history with the new squares on the end, and finally updates the value of `@step`.
117+
118+
> We like to use the convention where practical of ending mutator methods with a bang (!) so that readers of the
119+
code are aware that these will change state.
120+
121+
```ruby
122+
123+
class Game < HyperComponent
124+
mutator :handle_click! do |id|
125+
board = history[@step]
126+
return if current_winner? || board[id]
127+
128+
board = board.dup
129+
board[id] = player
130+
@history = history[0..@step] + [board]
131+
@step += 1
132+
end
133+
134+
mutator(:jump_to!) { |step| @step = step }
135+
end
136+
```
137+
138+
### The Game Display
139+
140+
Now we have a couple of helper methods to build parts of the game display.
141+
142+
+ `moves` creates the list items that allow the user to move back and forth through the history.
143+
+ `status` provides the play state
144+
145+
```ruby
146+
class Game < HyperComponent
147+
def moves
148+
return unless history.length > 1
149+
150+
history.length.times do |move|
151+
LI(key: move) { move.zero? ? "Go to game start" : "Go to move ##{move}" }
152+
.on(:click) { jump_to!(move) }
153+
end
154+
end
155+
156+
def status
157+
if (winner = current_winner?)
158+
"Winner: #{winner}"
159+
else
160+
"Next player: #{player}"
161+
end
162+
end
163+
end
164+
```
165+
166+
And finally our render method which displays the Board and the game info:
167+
168+
```ruby
169+
class Game < HyperComponent
170+
render(DIV, class: :game) do
171+
DIV(class: :game_board) do
172+
DisplayBoard(board: current)
173+
.on(:clicked_at, &method(:handle_click!))
174+
end
175+
DIV(class: :game_info) do
176+
DIV { status }
177+
OL { moves }
178+
end
179+
end
180+
end
181+
```
182+
183+
> `&method` turns an instance method into a Proc rather than having to say `{ |id| handle_click(id) }`
184+
185+
### Summary
186+
187+
This small game uses everything covered in the previous sections: HTML Tags, Component Classes, Params, Events and Callbacks, and State.
188+
The project was derived from this React tutorial: https://reactjs.org/tutorial/tutorial.html.
189+
You may want to compare our Ruby code with the React original.
190+
191+
192+
The following sections cover reference materials, and some advanced information. You may want to skip to the HyperState section which
193+
will use this example to show how state can be encapsulated, extracted and shared resulting in easier to understand and maintain code.

docs/client-dsl/methods.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ When an event occurs it will probably change state. The mutate method is used t
7979
state change.
8080

8181
+ `mutate` signals that this instance's state has been mutated
82-
+ `toggle` i.e. `toggle(:foo)` => `mutate @foo = !@foo`
82+
+ `toggle(:foo)` is short for `mutate @foo = !@foo`
8383

8484
**Other Methods**
8585

docs/hyper-state/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11

2-
<img align="left" width="100" height="100" style="margin-right: 20px" src="https://github.com/hyperstack-org/hyperstack/blob/edge/docs/wip.png?raw=true" /> The Hyperstack::State::Observable module allows you to build classes that share their state with Hyperstack Components, and have those components update when objects in those classes change state.
2+
<img align="left" width="100" height="100" style="margin-right: 20px" src="https://github.com/hyperstack-org/hyperstack/blob/edge/docs/wip.png?raw=true" /> The `Hyperstack::State::Observable` module allows you to build classes that share their state with Hyperstack Components, and have those components update when objects in those classes change state.
33

44
## This Page Under Construction

0 commit comments

Comments
 (0)