Tic Tac Toe Game
In this example we are looking at an online game of Tic Tac Toe for
two players.
The game is available for three platforms: Web in a
browser, and for iOS and Android as a native app downloadable from
their stores. The code is cross platform and works the same on all
three platforms.
This document is describing the key parts of the platform used in
the app, and the full source code with the comments throughout is
available at the very bottom of this page.
The app incorporates two workflows: game play and chat. This
documentat describes the key game play elements only, while the chat
workflow is very similar to that of game play and is commented
through in the source code.
The document assumes the reader’s existing knowledge of
JavaScript and JSX.
Screens
The interface is structured using JSX made of Chatium cross platform
blocks. And the logic is written in JavaScript.
The game has three screens:
- Title – where the player chooses to start a new game or join an existing.
- Game – actual screen where the game is happening.
- Join – screen where the player is joining an existing game.
Screen handlers, Title screen as an example
In Chatium all screen data is prepared on the backend and displayed
on the frontend (in the app). There is no logic on the frontend. The
data on the backend is prepared in screen handlers.
Each screen handler is registered using the
`app.screen` call, where the first parameter is the screen location
pattern and the second is handler function.
The screen handler function is expected to return a
JSX structure of UI elements.
With all styling and functionality removed, the bare
bone Title screen of the game would look like this:
app.screen('', async (ctx, req) => {
return (
<screen title="Tic Tac Toe">
<button>New game</button>
<button>Join via Game ID</button>
<text>Privacy policy</text>
<text>Source code link</text>
</screen>
)
})
In the example above, you can seen all the elements of the Title
screen. Except in that stripped example the elements look different
to the actual Title screen, where they have specific positioning and
respond to the user inputs.
The screen handler returns <screen> element as a root element
to all other UI elements. There are two buttons: “New game” and
“Join via Game ID”. And two text elements: “Privacy policy”
and “Source code link”. In the actual screen, the later two are
styled as clickable links.
To position the elements properly, the buttons need
to move closer to the screen center and the text to the very bottom.
To achieve that, we use <box> containers to wrap the controls
around:
- All of the <screen> content is wrapped in <box> with a style property height set to 100%, to make it take the entirety of the space, from top to bottom. <box> is a Chatium element similar to <div> in HTML.
- Then the buttons and the text elements are wrapped in two other <box> containers, practically splitting the screen in two sections.
- The top <box> is set to take all of the available space with the style property flex = 1, while the bottom <box> is kept to a minimum size with no flex specified.
- The top <box> centers the elements vertically (along the main flex axis) with the style property justifyContent = 'center'.
- Both <box> center the elements horizontally (across the main flex axis) with the style property alignItems = 'center'.
Read more about flex in Chatium: https://docs.chatium.com/en/reference/app-ui/style/flex
app.screen('', async (ctx, req) => {
return (
<screen title="Tic Tac Toe">
<box style={{ height: '100%' }}>
<box
style={{
alignItems: 'center',
justifyContent: 'center',
flex: 1
}}
>
<button>New game</button>
<button>Join via Game ID</button>
</box>
<box style={{ alignItems: 'center' }}>
<text>Privacy policy</text>
<text>Source code link</text>
</box>
</box>
</screen>
)
})
With the positioning done, it is only down to the style of the
buttons and the text. Styling of these elements is similar to that in
HTML and the list of available properties can be found on their
respective manual pages:
Unlike with the <box> elements, where we added the style
property right in the element, here we want to reuse the same style
between two buttons and the other one between two text elements. So
we declare each style in a variable and then use them in the style
property on the elements.
app.screen('', async (ctx, req) => {
const buttonStyle = {
paddingVertical: 30,
marginVertical: 10,
width: 200,
border: [ 1, 'solid', '#445' ],
color: '#445'
}
const linkStyle = {
textDecorationLine: 'underline',
marginBottom: 10
}
return (
<screen title="Tic Tac Toe">
<box style={{ height: '100%' }}>
<box
style={{
alignItems: 'center',
justifyContent: 'center',
flex: 1
}}
>
<button style={buttonStyle}>New game</button>
<button style={buttonStyle}>Join via Game ID</button>
</box>
<box style={{ alignItems: 'center' }}>
<text style={linkStyle}>Privacy policy</text>
<text style={linkStyle}>Source code link</text>
</box>
</box>
</screen>
)
})
Now the screen looks identical to the Title screen in the game,
except it does nothing.
The functionality is added via onClick property of
the elements. In the Title screen we utilise four types of onClick
actions:
- ctx.router.navigate to open a different screen, with a path relevant to the current router (eg. relevant to the request point of entry).
- ctx.account.navigate to open a different screen, with a path relevant to the current account (eg. relevant to the absolute path, instead of the request point of entry).
- ctx.router.apiCall to call a backend defined API handler, with a path relevant to the current router.
- copyToClipboard to copy text to clipboard.
ctx is a short for context. It carries properties and functions
specific for the currently processed request (preparing Title screen
in our case). ctx.router is a set of properties and function helpers
around the request point of entry (file index.tsx in our case) and
ctx.router is for the application account (eg. root folder).
With the onClick actions added, the final code look like this. Note
that ` copyToClipboard` action is imported from the `@app/ui` module.
import { copyToClipboard } from '@app/ui'
app.screen('', async (ctx, req) => {
const buttonStyle = {
paddingVertical: 30,
marginVertical: 10,
width: 200,
border: [ 1, 'solid', '#445' ],
color: '#445'
}
const linkStyle = {
textDecorationLine: 'underline',
marginBottom: 10
}
return (
<screen title="Tic Tac Toe">
<box style={{ height: '100%' }}>
<box
style={{
alignItems: 'center',
justifyContent: 'center',
flex: 1
}}
>
<button
style={buttonStyle}
onClick={ctx.router.apiCall('newGameApi')}
>
New game
</button>
<button
style={buttonStyle}
onClick={ctx.router.navigate('join')}
>
Join via Game ID
</button>
</box>
<box style={{ alignItems: 'center' }}>
<text
style={linkStyle}
onClick={ctx.account.navigate('privacy policy')}
>
Privacy policy
</text>
<text
style={linkStyle}
onClick={copyToClipboard('https://tic-tac-toe.chatium.com/s/ide/embed-folder/index')}
>
Source code link
</text>
</box>
</box>
</screen>
)
})
Heap, built-in NoSQL database
Heap is a Chatium built-in NoSQL database engine. It allows runtime
in-code table definitions with type checking. Each table is defined
using the `Heap.Table` call, where the first parameter is the table
name and the second is it’s definition.
Let’s have a look at the fields of the table
storing game boards in this app:
const boardTable = Heap.Table('ttt.boards', {
/**
* gameId - 5 letter Game ID. Used to invite a guest player
* into the game by the creator.
*/
gameId: Heap.NonEmptyString(),
/**
* creatorId - iternal identifier of the game creator.
* Either a Session ID or the Chatium registered User ID.
*/
creatorId: Heap.NonEmptyString(),
/**
* guestId - similar to creatorId, is an identifier of a
* guest player. Unlike the creatorId, guestId is not known
* upon initiation of the game by the creator.
* Therefore we must allow `null` as a possible value.
* For that we use Heap.Union to create a new type allowing
* either Heap.NonEmptyString or Heap.Null stored as a value.
*/
guestId: Heap.Union(
[
Heap.NonEmptyString(),
Heap.Null()
]
),
/**
* nextMoveBy is a filed describing who is making the next move
* in the game. It is an enum of type `NextMoveBy` declared
* earlier in the code.
*/
nextMoveBy: Heap.Enum(
NextMoveBy,
{ default: NextMoveBy.Creator }
),
/**
* xo - actual data for the current board with Xs and Os.
* The data is an array of nine cells (3 x 3).
* Xs are represented by integer values of 1, and Os by 0.
* An unoccupied cell has a value of `null`.
* Here again we are using Heap.Union to represent a cell value.
* Apart from allowing only either Heap.Integer or Heap.Null,
* we further restrict Heap.Integer to the range of 0 to 1 only.
*/
xo: Heap.Array(
Heap.Union(
[
Heap.Integer({ minimum: 0, maximum: 1 }),
Heap.Null()
]
)
)
})
Now we have `boardTable` variable, handler for the table storing the
games. In the app, we use the following methods to manipulate the
data in that table:
- boardTable.create – adds a new record to the table; used to add a new board/game to the table.
- boardTable.findOneBy – returns a record from the table; used to find a board by the Game ID.
- boardTable.update – updates a record in the table; used to makes changes to the existing board or game state.
Backend defined API and boardTable.create
Similar to `app.screen` call binding a screen
handler, `app.apiCall` binds an API handler. It takes the first
parameter as the handler location pattern and the second is the
handler function.
Lets have a look at a very simple `newGameApi`
handler and `boardTable.create` it calls to create a new board.
/**
* newGameApi creates a new game. The user who called the API
* becomes the Creator. In the end, it navigates the user to
* open a screen for the newly created game.
*/
app.apiCall('newGameApi', async (ctx, req) => {
/**
* New random Game ID made if 5 capital letters
*/
const gameId = makeId()
const board = await boardTable.create(ctx, {
gameId,
creatorId: myId(ctx),
guestId: null,
nextMoveBy: NextMoveBy.Creator,
xo: [
null, null, null,
null, null, null,
null, null, null
]
})
/**
* Navigate the user to the Game screen. While the
* `board.guestId` is null, the screen will display a
* Guest player invitation blok. Read about navigation:
* https://docs.chatium.com/en/handbook/intro/navigation
*/
return ctx.router.navigate(`${board.gameId}`)
})
So it pretty much does just two things – creates a new game, and
then commands back to the frontend (the app) to navigate the user to
a screen for the newly created game.
Note how the board is initialized with nine null
values and the next move is set to the game creator.
Path templates, boardTable.findOneBy and boardTable.update
A different API handler `boardClickApi` is
called when a player clicks on an empty
cell on the board to place an X or O.
Unlike previously looked at `newGameApi`
API call, `boardClickApi` path is
defined using a path template and accepts two parameters gameId and
xoIdx. The handler registration looks like that:
app.apiCall('boardClickApi/:gameId/:xoIdx', async (ctx, req) => {
// handler’s code here
})
Note how the parameters are prefixed with a colon. These parameters
are accessible in the handler as req.params.gameId and
req.params.xoIdx.
At the very start of the handler, it loads the game data from the
boardTable using gameId from the template as the search parameter:
const board = await boardTable.findOneBy(ctx, {
gameId: req.params.gameId
})
If the board with the supplied gameId is not found,
boardTable.findOneBy then returns null.
Fast forward past the parameters and the access check, the handler
makes the changes to the game status, placing and X or O on the board
and changing which player is expected to make the next move. All of
this is done on the abject previously loaded via
boardTable.findOneBy.
const xoIdx = parseInt(req.params.xoIdx)
board.xo[xoIdx] = mySymbol(ctx, board)
board.nextMoveBy = (
board.nextMoveBy === NextMoveBy.Creator ?
NextMoveBy.Guest :
NextMoveBy.Creator
)
And finally the changes are saved back to the boardTable:
await boardTable.update(ctx, board)
The actual table record (board in our case) to make changes to is
identified by the id value of the record (board.id for us). That
value is immutable.
The last step in the ` boardClickApi`
is a call to
update the Game screens.
Screen update sockets
In this app, two online players are taking part in
one game. Whenever one player makes a move, the Game screens for both
players need to be updated to display the current situation in the
game.
The Game screen is listening for a signal issued upon
a move made by either of the players taking part in the game. That is
done using Chatium sockets, whose sole purpose is delivering screen
update signals.
When such signal is received, the entire screen that
is waiting for it gets updated: the initial request that has opened
the screen gets resent and the data for the entire screen is
returned. However only the section of the screen which data that has
changed will be redrawn for the performance sake.
Looking at the Game <screen> block, inside
<BoardScreenBlock> component function, we see how it is bound
to an update socket:
<screen title="Game" socketIds={[await gameSocketId(board.gameId)]}>
Here the `socketIds` parameter is what binds the screen. Note
how the game socket is specified by `board.gameId`.
Now, looking at the `boardClickApi` handler
again, at the very end of the function we
see a call to issue an update signal for the Game
screen:
await updateGameSocket(board.gameId)
And again note how the game socket is
specified by `board.gameId`. Only
a socket for the particular game is triggered.
Both `gameSocketId` and `updateGameSocket` are simple
wrapper functions around Chatium’s `genSocketId` and `updateSocket`
functions from the `@app/socket` module:
function gameSocketId(gameId) {
return genSocketId(`tic-tac-toe-update-${gameId}`)
}
async function updateGameSocket(gameId) {
return updateSocket(`tic-tac-toe-update-${gameId}`)
}
Game screen
The Game screen handler is bound to a path template in a similar
way to ` boardClickApi`
API. It has no path and takes one template parameter `gameId`
only:
app.screen(':gameId', async (ctx, req) => {
// Game screen code here
})
The handler doesn’t return a <screen> block directly, but
rather returns either of three compoents defining the specific screen
situations:
- <InviteScreenBlock> – a screen displaying invitation code for the Guest player.
- <BoardScreenBlock> – a screen for the actual game.
- <AccessDeniedScreenBlock> – if the user tries to acces a game they are neither a creator of, nor were the original guest player.
Both <InviteScreenBlock> and <BoardScreenBlock> are
screen bound to the same socket.
Once a Guest player joins the game, the update signal on the
socket that <InviteScreenBlock> is bound to redraws the Game
screen, making the <BoardScreenBlock> replace the
<InviteScreenBlock>.
All consequent updates on that socket simply redraw
<BoardScreenBlock> to display the actual game situation.
<InviteScreenBlock>
Is a very simple screen block. Practically does two things:
displays the invite code for the Guest player and binds to the game
update socket. Upon the socket update, the parent function handler
for the Game screen is called, and returns the <BoardScreenBlock>
instead of the <InviteScreenBlock>.
The screen itself doesn’t have any user actionable actions,
other that copying the code to the clip board. It practically is a
waiting screen for the game Creator.
Join screen and API
Join screen allows the Guest player to join a new game started by
a Creator. The screen is made of a very simply form displaying a Game
ID input field and the submit button.
Both input field and the submit button are bound to the same
submit action:
const submitFormAction = {
type: 'submitForm',
formId: 'formJoin',
url: ctx.router.url('joinGameApi')
}
The input field executes submitFormAction upon the Done press on the
virtual keyboard or the Enter button on the laptop.
The bare bone version of the form, stripped of elements styling
and positioning, looks like this:
<text-input
name='gameId'
formId='formJoin'
onReturnKeyPress={submitFormAction}
/>
<button onClick={submitFormAction}>Join!</button>
Note how formId value in submitFormAction matches that in the input.
It is required for the action to know which fields to submit, if
there were multiple inputs on the screen.
The API handler receiving the form data is `joinGameApi`. It is
specified in the `submitFormAction` as `url`.
In the `joinGameApi`, the form data is accessible via `req.body`.
The only field our form has is named gameId and is accessed this way:
const gameId = req.body.gameId?.trim().toUpperCase()
The only thing this API does is the basic checks, before it redirects
the Guest player to the Game screen:
return ctx.router.navigate(`${board.gameId}`)
So we see, how the Join game form, submits the data to API, which in
return redirects the player to a different screen.
<BoardScreenBlock>
<BoardScreenBlock> is a <screen> block with the Game
board, where the actual
game play occurs. The block is returned by
the Game screen handler – the same handler that returns
<InviteScreenBlock>. Both players end
up on the screen after the Creator starts a new game, and the
Guest joins it via an invitation. The
<screen> is bound to an update socket: see “Screen
update sockets” section earlier.
The screen features a
number of components. The component of the
most interest for us is <Board>.
The boards is made of
3 x 3 cells: 3 horizontally oriented <box> containers each
including another 3 <box> containers. Each
in turn including an <icon> with either X, O or an empty space.
<icon> element
uses icons from FontAwesome version 5. For Xs and Os it uses icons
“fas times” and “far circle”
respectively. For an empty cell an <icon>
with no specified name is
used. This makes it
appear empty but still hold the space and ensure the containing
cell doesn’t collapse.
See
https://docs.chatium.com/en/reference/app-ui/blocks/icon
for more information on <icon>.
If the current player is expected to make a
move, then all empty cells become clickable. In this case instead of
displaying empty <icon>
in cells on their own, <button> elements are displayed with
`leftIcon` property set to empty <icon>.
Even though <icon> elements have
`onClick` property, using <button> is more preferable as it
displays progress animation once clicked,
unlike <icon> if used on it’s own.
At the very bottom of the <BoardScreenBlock>
is <footer> section. This element is pinned to the bottom of
the physical screen and overlays the content, if there is any
underneath. In this screen it is used for
chat <MessageInput> component.
Full source code
Below is the full source code for the app and the emulator. Feel
free to experiment and change the code. However Android and iOS apps
currently deployed to Play Google and Apps Store do work with the
original unmodified project only. You will need to deploy you own
builds to the stores to work with the modified code. However this is
a task that goes beyond the scope of this document.