Skip to content

Commit aba2673

Browse files
committed
example(tic-tac-toe): done
1 parent db2e333 commit aba2673

File tree

7 files changed

+381
-1
lines changed

7 files changed

+381
-1
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ folder. You can preview them at [this site](https://frender-rs.github.io/frender
3434

3535
```toml
3636
[dependencies]
37-
frender = "= 1.0.0-alpha.6"
37+
frender = "= 1.0.0-alpha.7"
3838
```
3939

4040
3. Create `index.html` in the project root directory.

examples/tic-tac-toe/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "tic-tac-toe"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
frender = { path = "../../frender" }

examples/tic-tac-toe/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Tic Tac Toe - frender</title>
6+
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
7+
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
8+
<link data-trunk rel="css" href="styles.css" />
9+
</head>
10+
<body>
11+
<div id="frender-root"></div>
12+
</body>
13+
</html>

examples/tic-tac-toe/src/data.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2+
pub enum Square {
3+
Empty,
4+
FillX,
5+
FillO,
6+
}
7+
8+
impl Square {
9+
pub fn is_empty(&self) -> bool {
10+
match self {
11+
Square::Empty => true,
12+
_ => false,
13+
}
14+
}
15+
16+
pub fn to_str(&self) -> &'static str {
17+
match self {
18+
Square::Empty => "",
19+
Square::FillX => "X",
20+
Square::FillO => "O",
21+
}
22+
}
23+
}
24+
25+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26+
pub struct Board {
27+
pub squares: [Square; 9],
28+
}
29+
30+
impl Board {
31+
pub fn new_empty() -> Self {
32+
Self {
33+
squares: [Square::Empty; 9],
34+
}
35+
}
36+
37+
pub fn calculate_winner(&self) -> Square {
38+
static LINES: &[[usize; 3]] = &[
39+
[0, 1, 2],
40+
[3, 4, 5],
41+
[6, 7, 8],
42+
[0, 3, 6],
43+
[1, 4, 7],
44+
[2, 5, 8],
45+
[0, 4, 8],
46+
[2, 4, 6],
47+
];
48+
49+
for &[a, b, c] in LINES {
50+
let a = &self.squares[a];
51+
if !a.is_empty() && a == &self.squares[b] && a == &self.squares[c] {
52+
return *a;
53+
}
54+
}
55+
56+
Square::Empty
57+
}
58+
}
59+
60+
#[derive(Debug, Clone)]
61+
pub struct Game {
62+
// make the fields private so that we can guard the game logic
63+
history: Vec<Board>,
64+
step_number: usize,
65+
}
66+
67+
impl Game {
68+
pub fn new() -> Self {
69+
let mut history = Vec::with_capacity(9);
70+
history.push(Board::new_empty());
71+
Game {
72+
history,
73+
step_number: 0,
74+
}
75+
}
76+
77+
pub fn current(&self) -> &Board {
78+
&self.history[self.step_number]
79+
}
80+
81+
pub fn full_history(&self) -> &Vec<Board> {
82+
&self.history
83+
}
84+
85+
pub fn step_number(&self) -> usize {
86+
self.step_number
87+
}
88+
89+
#[inline]
90+
pub fn x_is_next(&self) -> bool {
91+
self.step_number % 2 == 0
92+
}
93+
94+
#[inline]
95+
pub fn next_player(&self) -> Square {
96+
if self.x_is_next() {
97+
Square::FillX
98+
} else {
99+
Square::FillO
100+
}
101+
}
102+
103+
/// returns whether state changed
104+
pub fn click(&mut self, i: usize) -> bool {
105+
let current = self.current();
106+
107+
// i valid and the game is not over and the square is not filled
108+
if i <= 8 && current.squares[i].is_empty() && current.calculate_winner().is_empty() {
109+
// copy current board as the next
110+
let mut next = *current;
111+
next.squares[i] = self.next_player();
112+
113+
let keep = self.step_number + 1;
114+
if keep < self.history.len() {
115+
self.history.truncate(keep);
116+
}
117+
self.history.push(next);
118+
119+
self.step_number += 1;
120+
121+
true
122+
} else {
123+
false
124+
}
125+
}
126+
127+
/// returns whether state changed
128+
pub fn jump_to(&mut self, i: usize) -> bool {
129+
if &i == &self.step_number || &i >= &self.history.len() {
130+
return false;
131+
}
132+
self.step_number = i;
133+
true
134+
}
135+
}

examples/tic-tac-toe/src/main.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
//! The frender version of
2+
//! [ReactJs Tic Tac Toe](https://codepen.io/gaearon/pen/gWWZgR?editors=0010)
3+
4+
pub mod data;
5+
6+
use frender::prelude::*;
7+
8+
def_props! {
9+
pub struct SquareProps {
10+
pub value<TNode: react::Node>(v: TNode) -> react::AnyNode {
11+
v.into_node()
12+
},
13+
pub on_click: react::AnyFn<dyn Fn()>,
14+
}
15+
}
16+
17+
#[component]
18+
fn Square(props: &SquareProps) {
19+
rsx!(
20+
<button class="square" on_click={props.on_click.clone()}>
21+
{&props.value}
22+
</button>
23+
)
24+
}
25+
26+
def_props! {
27+
pub struct BoardProps {
28+
pub board: data::Board,
29+
pub on_click: react::AnyFn<dyn Fn(usize)>,
30+
}
31+
}
32+
33+
#[component]
34+
fn Board(props: &BoardProps) {
35+
let render_square = |i: usize| {
36+
let on_click = props.on_click.clone();
37+
rsx!(
38+
<Square
39+
value:={props.board.squares[i].to_str()}
40+
on_click={move || on_click(i)}
41+
/>
42+
)
43+
};
44+
45+
rsx!(
46+
<div>
47+
<div class="board-row">
48+
{render_square(0)}
49+
{render_square(1)}
50+
{render_square(2)}
51+
</div>
52+
<div class="board-row">
53+
{render_square(3)}
54+
{render_square(4)}
55+
{render_square(5)}
56+
</div>
57+
<div class="board-row">
58+
{render_square(6)}
59+
{render_square(7)}
60+
{render_square(8)}
61+
</div>
62+
</div>
63+
)
64+
}
65+
66+
#[component]
67+
fn Game() {
68+
let (state, state_setter) = react::use_state!(() => data::Game::new());
69+
70+
let current = state.current();
71+
let winner = current.calculate_winner();
72+
73+
let status = match winner {
74+
data::Square::Empty => format!("Next player: {}", state.next_player().to_str()),
75+
_ => format!("Winner: {}", winner.to_str()),
76+
};
77+
78+
let on_click = {
79+
let state_setter = state_setter.clone();
80+
move |i| {
81+
state_setter.set_optional_from_old(move |old| {
82+
let mut game = (**old).clone();
83+
if game.click(i) {
84+
// game state changed
85+
Some(game)
86+
} else {
87+
// game state not changed
88+
None
89+
}
90+
})
91+
}
92+
};
93+
94+
let jump_to = move |i| {
95+
state_setter.set_optional_from_old(move |old| {
96+
let mut game = (**old).clone();
97+
if game.jump_to(i) {
98+
// game state changed
99+
Some(game)
100+
} else {
101+
// game state not changed
102+
None
103+
}
104+
})
105+
};
106+
107+
let moves = (0..state.full_history().len())
108+
.map(|i: usize| {
109+
let jump_to = jump_to.clone();
110+
let desc = if i > 0 {
111+
format!("Go to move #{}", i)
112+
} else {
113+
"Go to game start".to_string()
114+
};
115+
rsx!(
116+
<li key={i}>
117+
<button on_click={move || jump_to(i)}>{desc}</button>
118+
</li>
119+
)
120+
})
121+
.collect::<Vec<_>>();
122+
123+
rsx!(
124+
<div class="game">
125+
<div class="game-board">
126+
<Board
127+
board={*current}
128+
on_click={on_click}
129+
/>
130+
</div>
131+
<div class="game-info">
132+
<div>{status}</div>
133+
<ol>{moves}</ol>
134+
</div>
135+
</div>
136+
)
137+
}
138+
139+
#[component(main(mount_element_id = "frender-root"))]
140+
fn Main() {
141+
rsx!(
142+
<div style={style! {
143+
"margin": "auto",
144+
"padding": 16,
145+
"maxWidth": 768,
146+
}}>
147+
<h1>
148+
"Tic Tac Toe - "
149+
<i>
150+
<a href="https://github.com/frender-rs/frender" target={html::AnchorTarget::Blank}>
151+
<b children="f" />
152+
"render"
153+
</a>
154+
</i>
155+
</h1>
156+
<p>
157+
"This is the frender version of "
158+
<a href="https://codepen.io/gaearon/pen/gWWZgR?editors=0010" target={html::AnchorTarget::Blank}>"ReactJs Tic Tac Toe"</a>
159+
</p>
160+
<main style={style!{ "marginTop": "32px" }}>
161+
<Game />
162+
</main>
163+
</div>
164+
)
165+
}

0 commit comments

Comments
 (0)