This is a web sdk that is convenient for you to develop a game in a declarative way. It is an optional way to build and launch your games on with Stake Engine with some easy steps. It is powered by Svelte 5, PixiJS 8 and TurboRepo.
Here is a complete tutorial to start with one of our sample games from storybook running and local test to build and launch it on Stake Engine. Please ignore those steps that you already know or done.
We use VSCode as IDE but this is optional and it is up to you.
# Download and install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
# in lieu of restarting the shell
\. "$HOME/.nvm/nvm.sh"
# Download and install Node.js:
nvm install 22.16.0
# Verify the node versions. Should print "v22.16.0".
node -v
# Install pnpm
npm install [email protected] -g
# Verify the pnpm versions. Should print "v10.5.0"
pnpm -v
git clone https://github.com/StakeEngine/web-sdk.git
cd web-sdk
pnpm install
WIth out any error messages showing up, you are good to continue.
pnpm run storybook --filter=lines
pnpm run storybook --filter=<MODULE_NAME> in the terminal to see the storybook of a sample game in a TurboRepo way. <MODULE_NAME> is the name in the package.json file of a module in apps or packages folders."name": "lines" in the apps/lines/package.json, so we can find it and run its storybook."storybook": "cross-env PUBLIC_CHROMATIC=true storybook dev -p 6001 public",
MODE_BASE/book/random in the left sidebar, you will see an Action button appear on the left right conner of the game.
Action button and wait for a base game to finish.pnpm run dev --filter=lines
It is all good to have that error screen for now, because we have not connected to the RGS with DEV mode. We will show you how to do that in the launch-a-game step.
For more information about how the authentication is handled, please refer to Authenticate.svelte
pnpm run build --filter=lines
You should see an index.html output here apps/lines/.svelte-kit/output/prerendered/pages/index.html.
Create a new folder anywhere you prefer and put the index.html file there. For example:
build
|-index.html
apps/lines/.svelte-kit/output/client to the same folder like:build
|-index.html
|-_app
|-assets
|-favicon.svg
|-loader.gif
|-stake-engine-loader.gif
Files page of a game. Import files to upload your the frontend by selecting the whole build folder.
Publish Game button then select Front End.
Developer page, click on Start game session button and click on Launch in New Tab button.
A game is live with a staging environment and you can play with all the buttons.
Check the query string in the URL and those are the values required to connect to the RGS.
Now you can go back to the DEV mode and copy/paste the query string in the url and play it locally.
Congratulations! You've completed the tutorial. You can explore the more content in the following documentation for details.
Q: Would it be possible to use a different programming language or framework, such as using Pixi.js only without Svelte?
Q: If we use our own UI/Web SDK, how can we pass configuration data into it?
Q: Do you have any specific policies (or methods) for handling currencies?
stake.us will be handled by "NO_LOCALISATION_CURRENCY_MAP".Q: As we are using our own game engine and integrating to RGS with the Web SDK as references. Is there a preference to when "end-round" API gets called? After the winning animation is done or it can be called even before the animations is finished.
packages/utils-xstate/src/createPrimaryMachines.ts
const BET_TYPE_METHODS_MAP = {
noWin: {
newGame: async () => undefined,
endGame: async () => undefined,
},
singleRoundWin: {
newGame: async () => {
const endRoundData = await handleRequestEndRound();
if (endRoundData?.balance) {
balanceAmountFromApiHolder = endRoundData.balance.amount;
}
},
endGame: async () => {
if (balanceAmountFromApiHolder !== null) {
handleUpdateBalance({ balanceAmountFromApi: balanceAmountFromApiHolder });
balanceAmountFromApiHolder = null;
}
},
},
bonusWin: {
newGame: async () => undefined,
endGame: async () => {
const data = await handleRequestEndRound();
if (data?.balance) {
handleUpdateBalance({ balanceAmountFromApi: data.balance.amount });
balanceAmountFromApiHolder = null;
}
},
},
} as const;
Q: From what I see, most of the animated graphics seem to be spine, is their any other alternatives that you know of, besides spine?
Q: Is there a mechanism to switch from a game type (i.e. lines) to another game type (i.e. cluster) as a mechanic?
A: It's easy. What you can do:
From this example, it shows you that you can basically do anything you want to achieve through this pattern.
Q: What are the requirements to launch the same game on social casinos like stake.us?
social=true in the query string. For example 'BET' to 'SPIN'. Check the example in the codebase here packages/components-ui-pixi/src/i18n/i18nDerived.tsQ: When updating things in packages/pixi-svelte, it doesn't seem to take effect. Why is that?
pnpm run build --filter=pixi-svelte
Q: Loading time for Storybook on Windows is insanely long, what can I do?
Besides basic web skills (html, css and javascript), here it shows a list of npm dependencies of this repo. It would be great to start with understanding them before kicking off Get Started.
Storybook is a powerful and handy tool to test our games. For example:
COMPONENTS/<Game>/component: It tests the <Game /> component. In this case, it doesn't skip the loading screen.COMPONENTS/<Game>/preSpin: It tests the <Game /> component with the preSpin function.COMPONENTS/<Game>/emitterEvent: It tests the <Game /> component with an emitterEvent "boardHide".COMPONENTS/<Symbol>/component: It tests the <Symbol /> component with controls e.g. state of the symbol.COMPONENTS/<Symbol>/symbols: It tests the <Symbol /> component with all the symbols and all the states.MODE_BASE/book/random: It tests the <Game /> component with a random book of base mode.MODE_BASE/bookEvent/reveal: It tests the <Game /> component with a "reveal" bookEvent of the base mode. It will spin the reels.MODE_BONUS/book/random: It tests the <Game /> component with a random book of bonus mode.MODE_BONUS/bookEvent/reveal: It tests the <Game /> component with a "reveal" bookEvent of the bonus mode. It will spin the reels.
With all the stories above and the stories that created and customised by yourself, we are able to test the whole game, intermediate components and atomic components.
We are also able to test our game with a book, a sequence of bookEvents and a single bookEvent. If each bookEvent is implemented well with emitterEvents and its story is resolved properly, the game is technically finished.
Here it is a simplified flow chart of steps how a game is processed after RGS request. The real situation might be more complicated, but it follows the same idea.
This function is created by packages/utils-book/src/createPlayBookUtils.ts. It goes through bookEvents one by one, handles each one with async function playBookEvent(). It resolves them one after another with sequence() in the order of the bookEvents array. It means the sequence of bookEvents matters eminently and it determines the behaviors of the game. For example, we don't want to see the "win" before "spin", so we should put "win" after the "spin". This function is also used in the MODE_<GAME_MODE>/book/random stories.
playBookEvent(): This is a function that takes in a bookEvent with some context (usually all the bookEvents), then find the bookEventHandler in bookEventHandlerMap based on bookEvent.type to process it. This function is also used in the MODE_<GAME_MODE>/bookEvent/<BOOK_EVENT_TYPE> stories.
sequence(): This is an async function to achieve resolving async functions/promises one after another. On the contrast, Promise.all() will trigger all the async functions/promises together at the same time, which is not what we desire for the sequence of the game.
book: A book is a json data that is returned from the RGS (Remote Game Server) for each game requested. It is randomly picked from over a million of books, which is math. It is mainly composed by bookEvents.// base_books.ts - Example of a base game book
{
id: 1,
payoutMultiplier: 0.0,
events: [
{
index: 0,
type: 'reveal',
board: [
[{ name: 'L2' }, { name: 'L1' }, { name: 'L4' }, { name: 'H2' }, { name: 'L1' }],
[{ name: 'H1' }, { name: 'L5' }, { name: 'L2' }, { name: 'H3' }, { name: 'L4' }],
[{ name: 'L3' }, { name: 'L5' }, { name: 'L3' }, { name: 'H4' }, { name: 'L4' }],
[{ name: 'H4' }, { name: 'H3' }, { name: 'L4' }, { name: 'L5' }, { name: 'L1' }],
[{ name: 'H3' }, { name: 'L3' }, { name: 'L3' }, { name: 'H1' }, { name: 'H1' }],
],
paddingPositions: [216, 205, 195, 16, 65],
gameType: 'basegame',
anticipation: [0, 0, 0, 0, 0],
},
{ index: 1, type: 'setTotalWin', amount: 0 },
{ index: 2, type: 'finalWin', amount: 0 },
],
criteria: '0',
baseGameWins: 0.0,
freeGameWins: 0.0,
}
bookEvent: A bookEvent is a json data that is one of the element of the book.events array.// base_books.ts - Example of a "reveal" bookEvent
{
index: 0,
type: 'reveal',
board: [
[{ name: 'L2' }, { name: 'L1' }, { name: 'L4' }, { name: 'H2' }, { name: 'L1' }],
[{ name: 'H1' }, { name: 'L5' }, { name: 'L2' }, { name: 'H3' }, { name: 'L4' }],
[{ name: 'L3' }, { name: 'L5' }, { name: 'L3' }, { name: 'H4' }, { name: 'L4' }],
[{ name: 'H4' }, { name: 'H3' }, { name: 'L4' }, { name: 'L5' }, { name: 'L1' }],
[{ name: 'H3' }, { name: 'L3' }, { name: 'L3' }, { name: 'H1' }, { name: 'H1' }],
],
paddingPositions: [216, 205, 195, 16, 65],
gameType: 'basegame',
anticipation: [0, 0, 0, 0, 0],
}
// base_books.ts - Example of a setTotalWin bookEvent
{ index: 1, type: 'setTotalWin', amount: 0 },
bookEventHandler: An async function that takes in a bookEvent and do some operations with it. Usually it broadcasts some emitterEvents, so the components will receive and handle.An object that the key is bookEvent.type and value is a bookEventHandler. We can find an example in apps/lines/src/game/bookEventHandlerMap.ts.
// bookEventHandlerMap.ts - Example of "updateFreeSpin" bookEventHandler
export const bookEventHandlerMap: BookEventHandlerMap<BookEvent, BookEventContext> = {
...,
updateFreeSpin: async (bookEvent: BookEventOfType<'updateFreeSpin'>) => {
eventEmitter.broadcast({ type: 'freeSpinCounterShow' });
eventEmitter.broadcast({
type: 'freeSpinCounterUpdate',
current: bookEvent.amount,
total: bookEvent.total,
});
},
...,
}
It achieves event-driven programming for the development. It can either broadcast or subscribe to emitterEvents. It connects the javascript scope and svelte component scope with emitterEvents instead of passing the different states as svelte component props directly. The three most used functions are:
eventEmitter.broadcast()eventEmitter.broadcastAsync()eventEmitter.subscribeOnMount()An emitterEvent is a json data that eventEmitter.broadcast(emitterEvent) or eventEmitter.broadcastAsync(emitterEvent) broadcasts, so that a component which has eventEmitter.subscribeOnMount(emitterEventHandlerMap) can receive the data and deal with it in a synchronous or asynchronous way.
For a game we have many animations, so sometimes we need to "await" for those animations to finish before going to the next step.
Conceptually a bookEvent is composed by emitterEvents. Nevertheless, the flexibility lies in that the emitterEvents composing a bookEvent can come from multiple different svelte components. This way we can achieve and control the interactions and timing between different svelte components for the same bookEvent, ultimately, to achieve our games.
// bookEventHandlerMap.ts - Example of an emitterEvent
{
type: 'freeSpinCounterUpdate',
current: undefined,
total: bookEvent.totalFs,
}
EmitterEventHandler (Synchronous): A sync function that takes in an emitterEvent. It usually deals with some sync operations e.g. show/hide component, tidy up, update some numbers and so on.// bookEventHandlerMap.ts - Example of broadcast
eventEmitter.broadcast({
type: 'freeSpinCounterUpdate',
current: undefined,
total: bookEvent.totalFs,
});
// FreeSpinCounter.svelte - Example of receiving
context.eventEmitter.subscribeOnMount({
...,
freeSpinCounterUpdate: (emitterEvent) => {
if (emitterEvent.current !== undefined) current = emitterEvent.current;
if (emitterEvent.total !== undefined) total = emitterEvent.total;
},
...,
});
EmitterEventHandler (Asynchronous): An async function that takes in an emitterEvent. It usually deals with some async operations e.g. wait for fading in/out component, wait for animations to finish, wait for numbers to increase/decrease with svelte-tween and so on.// bookEventHandlerMap.ts - Example of broadcastAsync
await eventEmitter.broadcastAsync({
type: 'freeSpinIntroUpdate',
totalFreeSpins: bookEvent.totalFs,
});
// FreeSpinIntro.svelte - Example of receiving
context.eventEmitter.subscribeOnMount({
...,
freeSpinIntroUpdate: async (emitterEvent) => {
freeSpinsFromEvent = emitterEvent.totalFreeSpins;
await waitForResolve((resolve) => (oncomplete = resolve));
},
...,
});
An object that the key is emitterEvent.type and value is an emitterEventHandler. We can find this object in each component. For example, apps/lines/src/components/FreeSpinCounter.svelte.
freeSpinCounterShow just shows this component and does nothing more.// FreeSpinCounter.svelte and its emitterEventHandlers
<script lang="ts" module>
export type EmitterEventFreeSpinCounter =
| { type: 'freeSpinCounterShow' }
| { type: 'freeSpinCounterHide' }
| { type: 'freeSpinCounterUpdate'; current?: number; total?: number };
</script>
<script lang="ts">
...
context.eventEmitter.subscribeOnMount({
freeSpinCounterShow: () => (show = true),
freeSpinCounterHide: () => (show = false),
freeSpinCounterUpdate: (emitterEvent) => {
if (emitterEvent.current !== undefined) current = emitterEvent.current;
if (emitterEvent.total !== undefined) total = emitterEvent.total;
},
});
</script>
<MainContainer>
...
</MainContainer>
There is one single idea that is been applied across the whole carrot-game-sdk that is Task Breakdown.
To extend a bit more of the topic above, if an emitterEventHandler does too much work, then it is better we consider to split it into smaller emitterEventHandlers as a process of task-breakdown.
For example, "tumbleBoard" bookEvent is a fairly complicated bookEvent. Instead of having one "tumbleBoard" emitterEvent, we split it into "tumbleBoardInit", "tumbleBoardExplode", "tumbleBoardRemoveExploded", "tumbleBoardSlideDown".
This way we can implement a big and complicated emitterEvent step by step. More importantly, we can test the implementations one by one in storybook of COMPONENTS/<Game>/emitterEvent.
// bookEventHandlerMap.ts - Example of task-breakdown
{
...,
tumbleBoard: async (bookEvent: BookEventOfType<'tumbleBoard'>) => {
eventEmitter.broadcast({ type: 'tumbleBoardShow' });
eventEmitter.broadcast({ type: 'tumbleBoardInit', addingBoard: bookEvent.newSymbols });
await eventEmitter.broadcastAsync({
type: 'tumbleBoardExplode',
explodingPositions: bookEvent.explodingSymbols,
});
eventEmitter.broadcast({ type: 'tumbleBoardRemoveExploded' });
await eventEmitter.broadcastAsync({ type: 'tumbleBoardSlideDown' });
eventEmitter.broadcast({
type: 'boardSettle',
board: stateGameDerived
.tumbleBoardCombined()
.map((tumbleReel) => tumbleReel.map((tumbleSymbol) => tumbleSymbol.rawSymbol)),
});
eventEmitter.broadcast({ type: 'tumbleBoardReset' });
eventEmitter.broadcast({ type: 'tumbleBoardHide' });
},
...,
}
// TumbleBoard.svelte - Example of task-breakdown
context.eventEmitter.subscribeOnMount({
tumbleBoardShow: () => {},
tumbleBoardHide: () => {},
tumbleBoardInit: () => {},
tumbleBoardReset: () => {},
tumbleBoardExplode: () => {},
tumbleBoardRemoveExploded: () => {},
tumbleBoardSlideDown: () => {},
});
Stateless games can be complicated as well (vs. stateful games). For example, a slots game can have different types of spins, number of spins, win rules, number of bookEvents, game modes, global multiplier, multiplier symbols and so on.
However with the data structure of math and the functions we have, we are able to break down a complicated game into small and atomic tasks (emitterEvents). It enables us to test the atomics independently as well. Visually it is something like this:
The colors of the emitterEvents under a bookEvent can be different, which means they are from different svelte components.
For example, we have a game apps/lines already. Assume that we have added a new bookEvent updateGlobalMult to the bonus game mode (MODE_BONUS) in math, so that we have a new global multiplier feature for the game. Based on that, here we will go through the steps together to implement this new bookEvent and add it to the game. Along the way we will introduce part of our file structure as well.
MODE_BONUS/book/random will randomly pick at. This is to simulate requesting data from RGS. All we need to do is to copy/paste data from our new math package and format it.// bonus_books.ts
{
type: 'updateGlobalMult',
globalMult: 3,
},
MODE_BONUS/bookEvent/<BOOK_EVENT_TYPE> uses. All we need to do is to copy/paste data from our new math package and format it.// bonus_events.ts
export default {
...,
updateGlobalMult: {
type: 'updateGlobalMult',
globalMult: 3,
},
...,
}
MODE_BONUS/bookEvent. With the following code added in this file, you will see the a new story MODE_BONUS/bookEvent/updateGlobalMult that is added in our storybook with an Action button. Now if we click on it and nothing would happen, but it is a good start because we set up the testing environment first. Next step is to add code of bookEventHandler to handle it.// ModeBonusBookEvent.stories.svelte
<Story
name="updateGlobalMult"
args={templateArgs({
skipLoadingScreen: true,
data: events.updateGlobalMult,
action: async (data) => await playBookEvent(data, { bookEvents: [] }),
})}
/>
type BookEvent is a union type (typescript union type) of BookEvent types.// typesBookEvent.ts
type BookEventUpdateGlobalMult = {
index: number;
type: 'updateGlobalMult';
globalMult: number;
};
export type BookEvent =
| ...
| BookEventUpdateGlobalMult
| ...
;
type EmitterEventGlobalMultiplier is a union type of EmitterEvent types.// GlobalMultiplier.svelte
<script lang="ts" module>
export type EmitterEventGlobalMultiplier =
| { type: 'globalMultiplierShow' }
| { type: 'globalMultiplierHide' }
| { type: 'globalMultiplierUpdate'; multiplier: number };
</script>
type EmitterEventGame is a union type of EmitterEvent types.// typesEmitterEvent.ts
...
import type { EmitterEventGlobalMultiplier } from '../components/GlobalMultiplier.svelte';
...
export type EmitterEventGame =
| ...
| EmitterEventGlobalMultiplier
| ...
;
EmitterEventGame and other EmitterEvent types to compose type EmitterEvent.type EmitterEvent is a union type of EmitterEvent types.// eventEmitter.ts
...
import type { EmitterEventGame } from './typesEmitterEvent';
export type EmitterEvent = EmitterEventUi | EmitterEventHotKey | EmitterEventGame;
export const { eventEmitter } = createEventEmitter<EmitterEvent>();
// GlobalMultiplier.svelte
<script lang="ts" module>
export type EmitterEventGlobalMultiplier =
| { type: 'globalMultiplierShow' }
| { type: 'globalMultiplierHide' }
| { type: 'globalMultiplierUpdate'; multiplier: number };
</script>
<script lang="ts">
...
context.eventEmitter.subscribeOnMount({
globalMultiplierShow: () => (show = true),
globalMultiplierHide: () => (show = false),
globalMultiplierUpdate: async (emitterEvent) => {
console.log(emitterEvent.multiplier)
},
});
</script>
<SpineProvider key="globalMultiplier" width={PANEL_WIDTH}>
...
<SpineTrack trackIndex={0} {animationName} />
</SpineProvider>
Test it individually (MODE_BONUS/bookEvent/updateGlobalMult): Run storybook and we should see this a new story "updateGlobalMult" has been added.
Now click on the Action button and we should see the <GlobalMultiplier /> component animates correctly followed by the " ⓘ Action is resolved ✅ " message, otherwise we need to go back to the component and figure out what is wrong until it is resolved.
If you find out the component hard to debug, we'd better start creating a new story COMPONENTS/<GlobalMultiplierSpine>/component. <GlobalMultiplierSpine /> component will purely take props and achieve its duty instead of being controlled by emitterEvents. This way it becomes more friendly for testing the component with the storybook controls.
Test it in books (MODE_BONUS/book/random): Final step is to test it in a book environment by switching to this book story. In a previous step we have updated apps/lines/src/stories/data/bonus_books.ts, so the new bookEvent will appear if we keep hitting the Action button in this story.
The file structure is in a way of structure of TurboRepo to achieve a monorepo. Besides the files for the configurations of TurboRepo, sveltekit, eslint, typescript, git and so on, here is a list of of key modules of apps and packages.
root
|_apps
| |_cluster
| |_lines
| |_price
| |_scatter
| |_ways
|
|_packages
|_config-*
|_constants-*
|_state-*
|_utils-*
|_components-*
|_pixi-*
For each game, it has an individual folder in the apps, for example apps/lines.
{
"name": "lines",
...
}
pnpm run dev --filter=<MODULE_NAME> in the terminal.pnpm run dev --filter=lines
// +page.svelte
<script lang="ts">
import Game from '../components/Game.svelte';
import { setContext } from '../game/context';
setContext();
</script>
<Game />
Mode<GAME_MODE>Book.stories.svelte and Mode<GAME_MODE>BookEvent.stories.svelte.// ComponentsGame.stories.svelte
<script lang="ts">
...
import Game from '../components/Game.svelte';
import { setContext } from '../game/context';
...
setContext();
</script>
<Story name="component (loadingScreen)">
<StoryLocale lang="en">
<Game />
</StoryLocale>
</Story>
For every TurboRepo local package, you can import and use them in an app or in another local package directly without publishing them to npm. Our codebase benefits considerably from a monorepo because it brings reusability, readability, maintainability, code splitting and so on. Here is an example of importing local packages with workspace:* in apps/lines/package.json:
// package.json
{
"name": "lines",
...,
"devDependencies": {
...,
"config-ts": "workspace:*",
},
"dependencies": {
...,
"pixi-svelte": "workspace:*",
"constants-shared": "workspace:*",
"state-shared": "workspace:*",
"utils-shared": "workspace:*",
"components-shared": "workspace:*",
}
}
The naming convention of packages is a combination of <PACKAGE_TYPE>, hyphen and <SPECIAL_DEPENDENCY> or <SPECIAL_USAGE>. For example, components-pixi is a local package that the package type is "components" and the special dependency is pixi-svelte.
config-*:pixi-*stateApp and ContextApp as a svelte-context.pixi-svelte.constants-*:state-*:utils-*:eventEmitter and ContextEventEmitter as a svelte-contextstateXstate, stateXstateDerived and ContextXstate as a svelte-contextstateLayout, stateLayoutDerived and ContextLayout as a svelte-contextcomponents-*:utils-layout.pixi-svelte.html.For *-shared packages, they are created to be reused as much as possible by other apps and packages. Instead of having a special dependency or usage, they should have a minimum list of dependencies and a broad set of use cases.
pixi-svelte, utils-event-emitter, utils-layout and utils-xstate they have functions to create corresponding svelte-context. For the contexts, they can be used by either an app or a local components-* package by just calling the getContext<CONTEXT_NAME>(). For example, components in components-layout use getContextLayout() from utils-layout. In this way, we can regard pixi-svelte as an integration of "utils-pixi-svelte" and "components-pixi-svelte".
svelte-context is a useful feature from svelte especially when a shared state requires some inputs/types to create. Here it shows the structure of context of sample game apps/lines. As showed before, setContext() is called at entry level component. For example, apps/lines/src/routes/+page.svelte or apps/lines/src/stories/ComponentsGame.stories.svelte. It sets four major contexts from the packages by this:
// context.ts - Example of setContext in apps
export const setContext = () => {
setContextEventEmitter<EmitterEvent>({ eventEmitter });
setContextXstate({ stateXstate, stateXstateDerived });
setContextLayout({ stateLayout, stateLayoutDerived });
setContextApp({ stateApp });
};
Different apps and packages require different contexts.
eventEmitter is created by packages/utils-event-emitter/src/createEventEmitter.ts. We have covered eventEmitter in the previous content.
stateLayout and stateLayoutDerived are created by packages/utils-layout/src/createLayout.svelte.ts. It provides canvasSizes, canvasRatio, layoutType and so on. Because we have a setting resizeTo: window for PIXI.Application, we use the sizes of window from svelte-reactivity as canvasSizes.
For html, the tags will auto-flow by default. However, in the canvas/pixijs we need to set positions manually to avoid overlapping. The importance of LayoutContext is that it provides us the values of boundaries (canvasSizes), device type based on the dimensions (layoutType) and so on. For example:
<Component x={0} /><Component x={context.stateLayoutDerived.canvasSizes().width} anchor={{ x: 1: y: 0 }} /><App /> is the parent of the component, otherwise it will be determined by its parent <Container />.anchor is because that the drawing is always go from top-left to bottom-right in pixijs.// createLayout.svelte.ts
import { innerWidth, innerHeight } from 'svelte/reactivity/window';
...
const stateLayout = $state({
showLoadingScreen: true,
});
const stateLayoutDerived = {
canvasSizes,
canvasRatio,
canvasRatioType,
canvasSizeType,
layoutType,
isStacked,
mainLayout,
normalBackgroundLayout,
portraitBackgroundLayout,
};
stateXstate and stateXstateDerived are created by packages/utils-xstate/src/createXstateUtils.svelte.ts. It provides a few functions to check the state of finite state machine, also known as gameActor, which is created by packages/utils-xstate/src/createGameActor.svelte.ts.
// createXstateUtils.svelte.ts
import { matchesState, type StateValue } from 'xstate';
...
const stateXstate = $state({
value: '' as StateValue,
});
const matchesXstate = (state: string) => matchesState(state, stateXstate.value);
const stateXstateDerived = {
matchesXstate,
isRendering: () => matchesXstate(STATE_RENDERING),
isIdle: () => matchesXstate(STATE_IDLE),
isBetting: () => matchesXstate(STATE_BET),
isAutoBetting: () => matchesXstate(STATE_AUTOBET),
isResumingBet: () => matchesXstate(STATE_RESUME_BET),
isForcingResult: () => matchesXstate(STATE_FORCE_RESULT),
isPlaying: () => !matchesXstate(STATE_RENDERING) && !matchesXstate(STATE_IDLE),
};
gameActor: To avoid using massive "if-else" conditions in the code, we use npm/xstate to create a finite state machine to handle the complicated logic and states of betting. It provides a few pre-defined mechanics like one-off bet, autoBet with a count down, resumeBet to continue an unfinished bet and so on.
// createGameActor.svelte.ts
import { setup, createActor } from 'xstate';
...
const gameMachine = setup({
actors: {
bet: intermediateMachines.bet,
autoBet: intermediateMachines.autoBet,
resumeBet: intermediateMachines.resumeBet,
forceResult: intermediateMachines.forceResult,
},
}).createMachine({
initial: 'rendering',
states: {
[STATE_RENDERING]: stateRendering,
[STATE_IDLE]: stateIdle,
[STATE_BET]: stateBet,
[STATE_AUTOBET]: stateAutoBet,
[STATE_RESUME_BET]: stateResumeBet,
[STATE_FORCE_RESULT]: stateForceResult,
},
});
const gameActor = createActor(gameMachine);
This is highly useful when it comes to the interactions with UI, for example disable the bet button when the a game is playing.
// BetButton.svelte - Example of interaction between xstate and UI
<script lang="ts">
import { getContext } from '../context';
const context = getContext();
</script>
<SimpleUiButton disabled={context.stateXstateDerived.isPlaying()} />
stateApp is created by packages/pixi-svelte/src/lib/createApp.svelte.ts. loadedAssets contains the static images, animations and sound data that is processed by PIXI.Assets.load with stateApp.assets. loadedAssets can be digested by pixi-svelte components directly as showed in pixi-svelte component <Sprite />.
// createApp.svelte.ts
const stateApp = $state({
reset,
assets,
loaded: false,
loadingProgress: 0,
loadedAssets: {} as LoadedAssets,
pixiApplication: undefined as PIXI.Application | undefined,
});
We have provided solutions for the UI, which are components-ui-pixi and components-ui-html. They are functional with a few features like auto gaming, turbo mode, bonus button, responsiveness and so on, although they are not as beautiful.
<script lang="ts">
import { UI, UiGameName } from 'components-ui-pixi';
import { GameVersion, Modals } from 'components-ui-html';
</script>
<App>
<UI>
{#snippet gameName()}
<UiGameName name="LINES GAME" />
{/snippet}
{#snippet logo()}
<Text
anchor={{ x: 1, y: 0 }}
text="ADD YOUR LOGO"
style={{
fontFamily: 'proxima-nova',
fontSize: REM * 1.5,
fontWeight: '600',
lineHeight: REM * 2,
fill: 0xffffff,
}}
/>
{/snippet}
</UI>
</App>
<Modals>
{#snippet version()}
<GameVersion version="0.0.0" />
{/snippet}
</Modals>
For the branding purpose, we recommend you to regard them as just an example of UI packages instead of applying them directly to your final product. It would be a good choice to use them as a starting point and add more style to them to build your UI. It is completely fine to ignore them and build your own UI from scratch.
To be continued.