Tutorial: Tic-Tac-Toe
You will build a tic-tac-toe game in this tutorial. This tutorial does not assume any existing oBerry knowledge. You will learn the fundamentals necessary to start building websites in oBerry. Below you can see the game that we will be building.
Getting started
To create a new oBerry project use:
npm create oberry@latestPick the project name and the language you want to use and this will create a new scaffolding project.
The project structure is:
/tic-tac-toe
├── src/
│ ├── assets/
│ ├── main.ts
│ └── style.css
├── public/
├── index.html
└── package.jsonThen go into the project directory and install the dependencies:
cd ./tic-tac-toe
npm iCreating the scaffolding
To create tic-tac-toe we need the board. Let's make it just a div with 9 buttons for each cell. We'll also add a paragraph for displaying the messages, such as the winner or the next player.
Remove the generated HTML and put this in the body instead:
<div id="board">
<button></button>
<button></button>
<button></button>
<button></button>
<button></button>
<button></button>
<button></button>
<button></button>
<button></button>
</div>
<p id="message"></p>And let's add some styling to make it look good:
#board {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
width: 200px;
gap: 4px;
}
button {
border: none;
border-radius: 4px;
cursor: pointer;
}
#board button {
aspect-ratio: 1 / 1;
font-size: 18px;
}Writing the game logic
Now let's open src/main.ts and write the game logic. We'll build it piece by piece.
Selecting elements
The first thing we need is to get references to the DOM elements we want to work with.
import { $ } from 'oberry';
const board = $('#board');
const squares = board.children();
const message = $('#message');$() is oBerry's element selector — it works like document.querySelector but returns an ElementWrapper, which gives you a chainable API to interact with the DOM. .children() returns all direct children of #board — our 9 buttons — also as an ElementWrapper.
Creating reactive state
import { $, $ref } from 'oberry';
// ...
type Player = 'X' | 'O';
const cells = $ref<(Player | null)[]>(Array(9).fill(null));
const curPlayer = $ref<Player>('O');
const curMessage = $ref<string>('Next player: O');
const gameOver = $ref<boolean>(false);$ref() creates a reactive signal — a value that the UI can automatically stay in sync with. You read it by calling it with no arguments curPlayer(), and write to it by calling it with a new value curPlayer('X') or a function that returns a new value curPlayer(previous => ...).
Here we have three pieces of state:
cells— an array of 9 entries, one per square, initially all nullcurPlayer— whose turn it is, starting with 'O'curMessage— the status text shown to the playergameOver- a boolean value used for detecting whether the game has ended
Binding state to the DOM
Now we can connect curMessage to the #message paragraph so it always displays the current value:
message.bind(curMessage);.bind() is a reactive binding — whenever curMessage changes, the paragraph's text content updates automatically. No need to manually query the element and set .innerText every time.
Helper functions
Let's add two small helpers before wiring up the buttons:
const nextPlayer = () => curPlayer(prev => prev === 'X' ? 'O' : 'X');
const checkWin = (): Player | null => {
const b = cells();
const wins = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], // rows
[0, 3, 6], [1, 4, 7], [2, 5, 8], // cols
[0, 4, 8], [2, 4, 6], // diagonals
];
for (const [a, b2, c] of wins) {
if (b[a] && b[a] === b[b2] && b[b2] === b[c]) return b[a] as Player;
}
return null;
};nextPlayer is simple - set the curPlayer value to 'O' if the previous player is 'X', set 'X' otherwise. checkWin reads cells() and checks all eight winning combinations. It returns the winning player or null if there's no winner yet.
Wiring up the squares
Finally, let's make each button interactive:
import { $, $ref, $computed } from 'oberry';
// ...
squares.forEach((square, i) => {
square.bind($computed(() => cells()[i] ?? ''));
square.on('click', () => {
if (square.text() || gameOver()) return; // already filled or game over
cells(prev => {
const next = [...prev];
next[i] = curPlayer();
return next;
});
nextPlayer();
curMessage(`Next player: ${curPlayer()}`);
const winner = checkWin();
if (winner) {
curMessage(`${winner} wins!`);
gameOver(true);
} else if (cells().every(c => c !== null)) {
curMessage("It's a draw!");
gameOver(true);
}
});
});There are a few things happening here:
squares.forEach((square, i) => ...)loops through every element ofsquare. Each square is available as anElementWrapper.$computed()creates a derived, read-only reactive value.$computed(() => cells()[i] ?? '')reads the current value of squareifrom the cells array. Whenever cells changes, the computed updates, and since it's bound to the button with.bind(), the button's text updates too.square.on('click', ...)adds an event listener for each square
In the click event listener we:
- Check if the square already has been filled by using
.text(), which returns the text content or if the game is already over. - Update the cells value using the
cells(prev => ...) - Move to the next player with the previously defined
nextPlayer()helper function - Update the message to display the next player
- Check for the winner. If there is one, we stop the game and update the message.
- Check if all the cells are all filled. If yes, stop the game and call it a draw.
WARNING
We use [...prev] to create a new copy of the array. If were to update prev directly it would not trigger an update, since the reference stays the same.
Extending the game
Congratulations! You just built a working tic-tac-toe game in oBerry.
But currently there is no way to restart the game. Let's implement that.
Add a button to your HTML:
<div id="board">
<!-- ... -->
</div>
<p id="message"></p>
<button id="reset">Reset</button>And let's add some css:
#reset {
padding: 8px 16px;
}In the main.ts file select the reset button:
const resetButton = $('#reset');And add a helper function that resets all the state to it's inital values:
const reset = () => {
cells(Array(9).fill(null));
curPlayer('O');
curMessage('Next player: O');
gameOver(false);
}And now we just have to call it on click:
resetButton.on('click', reset);Finished game
That's it! Now you have a fully functional tic-tac-toe game in oBerry. You can find the full code below.