Skip to content

Commit 2d84b0f

Browse files
committed
Game
1 parent 9d70baa commit 2d84b0f

18 files changed

Lines changed: 4723 additions & 1963 deletions

src/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const config = {
5656
{title: 'Todo app', value: 'todos'},
5757
{title: 'Chat app', value: 'chat'},
5858
{title: 'Drawing app', value: 'drawing'},
59+
{title: 'Tic-tac-toe game', value: 'game'},
5960
],
6061
initial: 0,
6162
},

templates/index.html.hbs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
{{includeFile template="public/favicon.svg" output="public/favicon.svg"}}
1111
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
1212
<title>
13-
TinyBase {{#if (eq appType "chat")}}Chat{{else if (eq appType "drawing")}}Drawing{{else}}Todos{{/if}}
13+
TinyBase {{#if (eq appType "chat")}}Chat{{else if (eq appType "drawing")}}Drawing{{else if (eq appType "game")}}Game{{else}}Todos{{/if}}
1414
</title>
1515
<style>
1616
* {
@@ -162,6 +162,8 @@
162162
A real-time chat application demonstrating TinyBase's reactive data management.
163163
{{else if (eq appType "drawing")}}
164164
A collaborative drawing canvas showcasing TinyBase's state synchronization.
165+
{{else if (eq appType "game")}}
166+
A tic-tac-toe game demonstrating turn-based logic and computed game state.
165167
{{/if}}
166168
<br><br>
167169
Built with {{#if typescript}}TypeScript{{else}}JavaScript{{/if}}{{#if react}} + React{{/if}}.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{{includeFile template="src/components/board.css.hbs" output="src/components/board.css"}}
2+
import './board.css';
3+
import {useMemo} from 'react';
4+
import {useValue} from 'tinybase/ui-react';
5+
{{includeFile template="src/components/Square.tsx.hbs" output="src/components/Square.{{ext}}"}}
6+
import {Square} from './Square';
7+
8+
export const Board = () => {
9+
const gameStatus = useValue('gameStatus') as string;
10+
const winningLine = useValue('winningLine') as string | undefined;
11+
12+
const winningPositions = useMemo(() => {
13+
if (!winningLine) return new Set();
14+
return new Set(winningLine.split(',').map(Number));
15+
}, [winningLine]);
16+
17+
const disabled = gameStatus !== 'playing';
18+
19+
return (
20+
<div id="board">
21+
{Array.from({length: 9}, (_, i) => (
22+
<Square
23+
key={i}
24+
position={i}
25+
disabled={disabled}
26+
winning={winningPositions.has(i)}
27+
/>
28+
))}
29+
</div>
30+
);
31+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{{includeFile template="src/components/gameStatus.css.hbs" output="src/components/gameStatus.css"}}
2+
import './gameStatus.css';
3+
import {useValue} from 'tinybase/ui-react';
4+
5+
export const GameStatus = () => {
6+
const gameStatus = useValue('gameStatus') as string;
7+
const currentPlayer = useValue('currentPlayer') as string;
8+
const winner = useValue('winner') as string | undefined;
9+
10+
return (
11+
<div id="gameStatus">
12+
{gameStatus === 'playing' && (
13+
<>Player <span className="player">{currentPlayer}</span>'s turn</>
14+
)}
15+
{gameStatus === 'won' && (
16+
<>Player <span className="winner">{winner}</span> wins!</>
17+
)}
18+
{gameStatus === 'draw' && <>It's a draw!</>}
19+
</div>
20+
);
21+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{{includeFile template="src/components/square.css.hbs" output="src/components/square.css"}}
2+
import './square.css';
3+
import {useCell, useStore, useValue} from 'tinybase/ui-react';
4+
5+
export const Square = ({position, disabled, winning}: {position: number; disabled: boolean; winning: boolean}) => {
6+
const store = useStore();
7+
const value = useCell('board', position.toString(), 'value') as string | undefined;
8+
const currentPlayer = useValue('currentPlayer') as string;
9+
10+
const isDisabled = disabled || !!value;
11+
12+
const handleClick = () => {
13+
if (!isDisabled) {
14+
store.setCell('board', position.toString(), 'value', currentPlayer);
15+
}
16+
};
17+
18+
return (
19+
<button
20+
className={`square${isDisabled ? ' disabled' : ''}${winning ? ' winning' : ''}`}
21+
onClick={handleClick}
22+
disabled={isDisabled}
23+
>
24+
{value || ''}
25+
</button>
26+
);
27+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#board {
2+
display: grid;
3+
grid-template-columns: repeat(3, 100px);
4+
grid-template-rows: repeat(3, 100px);
5+
gap: 0;
6+
border: 2px solid var(--border);
7+
border-radius: 0.5rem;
8+
overflow: hidden;
9+
margin: 2rem auto;
10+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{{includeFile template="src/components/board.css.hbs" output="src/components/board.css"}}
2+
import './board.css';
3+
{{includeFile template="src/components/square.ts.hbs" output="src/components/square.{{ext}}"}}
4+
import {createSquare} from './square';
5+
6+
export const createBoard = (store: any): HTMLDivElement => {
7+
const board = document.createElement('div');
8+
board.id = 'board';
9+
10+
const render = () => {
11+
const gameStatus = store.getValue('gameStatus');
12+
const winningLine = store.getValue('winningLine');
13+
const currentPlayer = store.getValue('currentPlayer');
14+
const winningPositions = new Set(
15+
winningLine ? winningLine.split(',').map(Number) : []
16+
);
17+
const disabled = gameStatus !== 'playing';
18+
19+
board.innerHTML = '';
20+
for (let i = 0; i < 9; i++) {
21+
const cell = store.getCell('board', i.toString(), 'value');
22+
const square = createSquare(
23+
i,
24+
cell,
25+
() => {
26+
if (gameStatus === 'playing' && !cell) {
27+
store.setCell('board', i.toString(), 'value', currentPlayer);
28+
}
29+
},
30+
disabled || !!cell,
31+
winningPositions.has(i)
32+
);
33+
board.appendChild(square);
34+
}
35+
};
36+
37+
store.addValuesListener(render);
38+
store.addTableListener('board', render);
39+
render();
40+
41+
return board;
42+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#gameStatus {
2+
text-align: center;
3+
font-size: 1.5rem;
4+
font-weight: 800;
5+
margin-bottom: 1rem;
6+
min-height: 2rem;
7+
}
8+
9+
#gameStatus .player {
10+
color: var(--accent);
11+
}
12+
13+
#gameStatus .winner {
14+
color: var(--accent);
15+
animation: pulse 1s ease-in-out infinite;
16+
}
17+
18+
@keyframes pulse {
19+
0%, 100% {
20+
opacity: 1;
21+
}
22+
50% {
23+
opacity: 0.7;
24+
}
25+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{{includeFile template="src/components/gameStatus.css.hbs" output="src/components/gameStatus.css"}}
2+
import './gameStatus.css';
3+
4+
export const createGameStatus = (store: any): HTMLDivElement => {
5+
const status = document.createElement('div');
6+
status.id = 'gameStatus';
7+
8+
const render = () => {
9+
const gameStatus = store.getValue('gameStatus');
10+
const currentPlayer = store.getValue('currentPlayer');
11+
const winner = store.getValue('winner');
12+
13+
if (gameStatus === 'playing') {
14+
status.innerHTML = `Player <span class="player">${currentPlayer}</span>'s turn`;
15+
} else if (gameStatus === 'won') {
16+
status.innerHTML = `Player <span class="winner">${winner}</span> wins!`;
17+
} else if (gameStatus === 'draw') {
18+
status.textContent = "It's a draw!";
19+
}
20+
};
21+
22+
store.addValuesListener(render);
23+
render();
24+
25+
return status;
26+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.square {
2+
width: 100px;
3+
height: 100px;
4+
background: var(--bg2);
5+
border: 2px solid var(--border);
6+
font-size: 2.5rem;
7+
font-weight: 800;
8+
color: var(--fg);
9+
cursor: pointer;
10+
transition: all 0.15s ease;
11+
display: flex;
12+
align-items: center;
13+
justify-content: center;
14+
}
15+
16+
.square:hover:not(.square.disabled) {
17+
background: var(--bg);
18+
border-color: var(--accent);
19+
}
20+
21+
.square.disabled {
22+
cursor: not-allowed;
23+
opacity: 0.7;
24+
}
25+
26+
.square.winning {
27+
background: var(--accent);
28+
color: var(--bg);
29+
}

0 commit comments

Comments
 (0)