Hello world!
I figured that I should start a development blog. In part, it serves as a portfolio, but it may also help others that stumble at the same places I did.
I have generally worked on ETL systems, and I want my career to take a different path. The problem is that almost every job out there requires at least some amount of web development experience, which is a domain in which I am still a beginner. The web has always had a fowl reputation for being complex, bloated and slow. However, there are also benefits:
- The web is built on open standards, thus anyone with a web browser can access a web app. There is nothing about the web that forces restrictions by a proprietor.
- Browsers often support functions that can turn web pages into full applications, and mobile browsers let users install them as progressive web apps (PWAs).
- Browsers are incredibly battle tested, and as such already make use of the best sand boxing technology and other mitigations.
- Web applications are often written in JavaScript, which is not compiled, and therefore customizable through browser extensions and user scripts. Try ad blocking a native compiled app for comparision.
I suspect that with Apple’s app store getting more restrictive, PWAs are going to become more and more popular. If normal non-tech-enthusiast users start to understand that you can install apps through the browser rather than exclusively from the app store, PWAs may grow rapidly. I’m bullish.
But this also means that learning how to do modern web development is now a priority. The only way to actually learn a technology is to make something with it, so I decided to make a Yahtzee clone. In terms of why I chose Svelte, I saw this video a while ago, and it gave me an itch I wanted to scratch.
Rules & Architecture
Before even writing code, I first want to figure out what actual components are required. The rules of Yahtzee are quite simple:
- The player has five dice, which the player has three chances to roll per turn.
- After each roll, the player can freeze dice so that fewer dice are rolled each time. This is reset at the start of the next turn.
- The player ends a turn by scoring it in one of 13 categories. The player’s total score is calculated as the sum of the results from these categories.
- There is a bonus given if the sum in just the top rows exceeds a certain amount. The bonus is added to the total score.
- With the exception of the Yahtzee category (5 of a kind), when a slot is used, it cannot be reused. The Yahtzee category can be reused if you get bonus Yahtzees.
- The game ends when you fill all the slots.
With this in mind, I can think of 5 components (not including the main app):
- A die (responsible for containing a number and drawing a die with dots)
- The dice container (contains 5 dice and buttons to roll and freeze them)
- A score slot (a text box that can be clicked to freeze and store its contents)
- The score board (contains all of the score slots)
- The leader board (contains the historical score record)
Because the Yahtzee score slot has different logic for scoring bonus Yahtzees, it made sense to turn that into its own component, as I learned over the course of making this.
Getting Started
$ npx degit sveltejs/template yahtzee
$ cd yahtzee
$ node scripts/setupTypeScript.js
$ npm install
$ npm run dev
Provided one has node installed, running the above commands will download the svelte starter template, convert it to TypeScript, install dependencies, and start the test server. It’s a quick and painless process, as Svelte requires few dependencies.
Drawing Dice
Because I am not an artist, I wanted to draw the face of the dice with an HTML canvas. For the sake of demonstration, here is a Dice.svelte component that just writes a number to the screen in a paragraph block:
<script lang="ts">
export let num: number = 0;
</script>
<p>{num}</p>
I then changed the main App.svelte to instantiate it, along with buttons to change the value.
<script lang="ts">
import Dice from './Dice.svelte';
let d: number = 1;
<script>
<button on:click={() => number += 1}>+</button>
<Dice num={d}/>
<button on:click={() => number -= 1}>-</button>
In HTML component framework lingo, the d
variable is passed to the Dice
component as a prop. We now have the ability to actually change the value inside of the dice component. With this in mind, let’s make a canvas! As I was just starting out, I tried this first (incorrect) version:
<script lang="ts">
export let num: number = 0;
let canvas: HTMLCanvasElement;
let ctx: CanvasRenderingContext2D;
ctx = canvas.getContext("2d");
</script>
<canvas bind:this={canvas} width=64 height=64></canvas>
This code will not work because the script code runs before the actual element is created. It tries to call getContext
before canvas actually points to an element. To fix this, we need to give the onMount
life cycle function a callback, as svelte will call it after it has set up our components in the DOM.
<script lang="ts">
import { onMount } from 'svelte';
export let num: number = 0;
let canvas: HTMLCanvasElement;
let ctx: CanvasRenderingContext2D;
onMount(async () => {
ctx = canvas.getContext("2d");
});
</script>
<canvas bind:this={canvas} width=64 height=64></canvas>
And now, we really have a canvas! We only want to redraw the canvas when the the num
changes. Svelte has the ability to execute a statement on a variable change using the $:
operator. My first incorrect version was as follows:
$: {
ctx.clearRect(0, 0, 64, 64);
ctx.fillStyle = "black";
ctx.beginPath();
if (num === 1) {
ctx.arc(32, 32, 6, 0, Math.PI * 2);
} else if (num === 2) {
ctx.arc(21, 42, 6, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(42, 21, 6, 0, Math.PI * 2);
} else if (num === 3) {
// Extend this pattern for 3, 4, 5 and 6
}
ctx.fill();
}
This does not work, as the statement is invoked before the context is created causing an error. You can fix it by wrapping it in an if
check:
$: {
if (ctx) {
// Everything else goes here unchanged
}
}
And now it works! You can click on the buttons and the dice should redraw automatically. If you want another example of how this statement can be used, in the App component, you can add the following lines to automatically clamp the value of the dice between 1 and 6:
$: if (d > 6) { d = 6; }
$: if (d < 1) { d = 1; }
With this, the dice component is done, and we can make the next part of our app.
The Dice Bar
Part of the problem with the DiceBar is that it needs to send data to the score board, and I have not made that yet, so I will work on that later. I’ll start with just the UI and its internal logic for now. Let’s use a table for layout.
<script lang="ts">
import Dice from './Dice.svelte';
let d1: number = 1;
let d2: number = 2;
let d3: number = 3;
let d4: number = 4;
let d5: number = 5;
let l1: boolean = false;
let l2: boolean = false;
let l3: boolean = false;
let l4: boolean = false;
let l5: boolean = false;
let rerolls: number = 3;
let disabled: boolean = false;
function rollDice() {
// TODO
}
</script>
<table>
<tr>
<td><Dice num={d1}/></td>
<td><Dice num={d2}/></td>
<td><Dice num={d3}/></td>
<td><Dice num={d4}/></td>
<td><Dice num={d5}/></td>
<td rowspan=2>
<button on:click={rollDice} disabled={disabled}
>{rerolls} rolls remaining!</button>
</td>
</tr>
<tr>
<td><button disabled={l1} on:click={() => { l1 = true; }}>Lock</button></td>
<td><button disabled={l2} on:click={() => { l2 = true; }}>Lock</button></td>
<td><button disabled={l3} on:click={() => { l3 = true; }}>Lock</button></td>
<td><button disabled={l4} on:click={() => { l4 = true; }}>Lock</button></td>
<td><button disabled={l5} on:click={() => { l5 = true; }}>Lock</button></td>
</tr>
</table>
Here, we store 5 numbers for the dice, 5 booleans to track which dice are frozen and data to track the number of rerolls we have remaining. We just have to fill in the logic for running a roll in rollDice
.
if (!l1) { d1 = Math.floor(Math.random() * 6 + 1); }
if (!l2) { d2 = Math.floor(Math.random() * 6 + 1); }
// Same for the other 3 dice
rerolls -= 1;
if (rerolls < 1) {
disabled = true;
l1 = true;
l2 = true;
// Same for the other 3 dice
}
This is as much as we can really do for now, because we have no way to actually store a score in the score board. For that, we actually need to make the score board.
Making a Scoreboard
To make the ScoreBoard, I started with a table that contains the needed labels and buttons.
<script lang="ts">
</script>
<table>
<tr>
<td rowspan=2>Upper Level</td>
<td>Ones</td>
<td>Twos</td>
<td>Threes</td>
<td>Fours</td>
<td>Fives</td>
<td>Sixes</td>
<td>Upper Level Bonus</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>Grand Total</td>
</tr>
<tr>
<td rowspan=2>Lower Level</td>
<td>3 of a Kind</td>
<td>4 of a Kind</td>
<td>Full House</td>
<td>Small Straight</td>
<td>Large Straight</td>
<td>Yahtzee</td>
<td>Chance</td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
In terms of the way that the state works, there are two sets of values: the theoretical sum based off of the value of the dice currently, and the value of the dice once it is locked into the score board. Let’s prefix the theoretical ones with an underscore for distinction:
let _ones: number = 0;
let _twos: number = 0;
let _threes: number = 0;
let _fours: number = 0;
let _fives: number = 0;
let _sixes: number = 0;
let _threeOfAKind: number = 0;
let _fourOfAKind: number = 0;
let _fullHouse: number = 0;
let _smallStraight: number = 0;
let _largeStraight: number = 0;
let _yahtzee: number = 0;
let _chance: number = 0;
let ones: number = 0;
let twos: number = 0;
let threes: number = 0;
let fours: number = 0;
let fives: number = 0;
let sixes: number = 0;
let threeOfAKind: number = 0;
let fourOfAKind: number = 0;
let fullHouse: number = 0;
let smallStraight: number = 0;
let largeStraight: number = 0;
let yahtzee: number = 0;
let chance: number = 0;
The grand total and upper level bonus are the direct result of other values here, so we can use Svelte’s $:
operator.
$: upperLevelSum = ones + twos + threes + fours + fives + sixes;
$: upperLevelBonus = upperLevelSum > 62 ? 35 : 0;
$: grandTotal = upperLevelSum + upperLevelBonus + threeOfAKind +
fourOfAKind + fullHouse + smallStraight + largeStraight +
yahtzee + chance;
The buttons that actually display the values need some internal logic, as depending on whether or not they are frozen, they must display one of two numbers. Let’s create a ScoreSlot component to manage this state.
<script lang="ts">
export let score: number = 0;
export let displayScore: number = 0;
let used: boolean = false;
let disabled: boolean = false;
function onClick() {
used = true;
}
</script>
<button {disabled} on:click={onClick}>{used ? score : displayScore}</button>
Now, I cannot assign a prop value from inside the element. The data passed in a prop only flows in one direction. We need to communicate an event back up to the ScoreBoard for the actual assignment to happen. To do this, I needed to use another Svelte function in ScoreSlot: createEventDispatcher
. Here is how we use it:
import { createEventDispatcher } from 'svelte';
let dispatch = createEventDispatcher();
// Change this function...
function onClick() {
used = true;
dispatch('assignment'); // ...by adding this line here
}
Back in our ScoreBoard, here is how I actually used the component, in this case for the “ones” category:
<ScoreSlot displayScore={_ones} score={ones} on:assignment={() => ones = _ones}/>
Because the Yahtzee slot works different than all of the others (it can be assigned to more than once if one gets a bonus Yahtzee), I duplicated the entire component as YahtzeeSlot with the main difference being that used
is replaced with burned
, and only set if the score is 0 when clicked:
<script lang="ts">
import { createEventDispatcher } from 'svelte';
let dispatch = createEventDispatcher();
export let score: number = 0;
export let displayScore: number = 0;
let burned: boolean = false;
let disabled: boolean = false;
function onClick() {
if (displayScore === 0) {
burned = true;
}
dispatch('assignment');
}
</script>
<button {disabled} on:click={onClick}>{disabled ? score : displayScore}</button>
With this, the the UI components for the score board starts to look at bit like Yahtzee.
Passing Data Around
Now that the UI skeleton is in place, I needed to actually get the dice data sent from the DiceBar to the ScoreBoard. The strategy I used to move data inside the existing components is a common technique in component based web frameworks called “lifting state up”. That is, data that needs to be used inside multiple different components is lifted into a shared component and passed to its children in the form of props.
The problem is that for global state components, this can become a tiresome process of storing state in places where there is no logical reason for it to be there at first glance. In my case, this would involve putting a bunch of state data in the top level App component and creating a ton of event dispatchers to move state through all of the higher level components.
To solve this problem, Svelte gives us a tool called stores. They exist at the global level and can move data to and from any component that imports it. As an example, I’ll first create a store that contains a boolean to indicate if the score board should be ready to score a turn. Let’s create a new file called stores.ts:
import { writable } from 'svelte/store';
export const ready = writable(false);
To make use of it, I’ll start by adding it to DiceBar. First, I want to make sure that I am setting ready
to true
after a roll. Second, I want to make it so that when a score slot sets ready back to false by scoring a value, that it resets the board.
import { ready } from './stores';
function rollDice() {
// Everything else
$ready = true;
}
$: if (!$ready) {
rerolls = 3;
disabled = false;
l1 = false;
l2 = false;
// Do this for l3, l4 and l5
}
Next, ScoreSlot and YahtzeeSlot need to be adjusted. The button should be enabled when ready and unused, and it should set ready to false on click:
// Change the 'disabled' declaration to:
$: disabled = used || !$ready;
// Change 'onClick' to:
function onClick() {
used = true;
$ready = false;
dispatch('assignment');
}
Now, the buttons should enable and disable themselves in the way one would reasonable expect, but with one tiny exception: the lock buttons on the DiceBar enable themselves before the dice are rolled. This means that if you get a Yahtzee, you can score it, re-lock all the dice, roll, and score it as a bonus. To fix this, let’s only enable those buttons on the first roll.
function rollDice() {
if (rerolls === 3) {
l1 = false;
l2 = false;
// l3, l4 and l5
}
// Everything else in this function
}
$: if (!$ready) {
rerolls = 3;
disabled = false;
}
Scoring the Dice
The next task is to actually connect the dice values to the score board so that scores can be calculated. This is not as easy as passing an Array<number>
from the DiceBar to the ScoreBoard. Arrays are good data structures for storing values that need to be in order, but there is no reason to do this. Yahtzee does not care which die contains which number. They can be 1-1-1-2-3 or 1-2-1-3-1 and both are scored the same way. The score board cares about which numbers are present and how many of each number are present.
A better data structure is a Map<number, number>
, with keys being the number on the dice and the value being the count of dice. Both of the rolls I displayed above would become { 1: 3, 2: 1, 3: 1 }
. In stores.ts, I added the following line:
export const dice = new writable(new Map());
To actually calculate the value from the dice, I added the following line to the rollDice
function:
$dice = [d1, d2, d3, d4, d5].reduce((prev, item) => {
prev.set(item, 1 + (prev.get(item) ?? 0));
return prev;
}, new Map());
This operation uses the reduce
method, which converts an iterable into a single value. While often used to take the sum or product of the contents of an array, I used it here to create a map and either add new keys or increment their values for every dice value.
To turn these values into scores for the score board, I turned the displayScores into reactive statements based on the $dice
store.
$: _ones = $dice.get(1) ?? 0;
$: _twos = 2 * ($dice.get(2) ?? 0);
$: _threes = 3 * ($dice.get(3) ?? 0);
// Same for _fours, _fives and _sixes
// Some of the other scores depend on the sum of all the values, which
// can be calculated with the following. e.g. it replaces '_chance'
$: sum = _ones + _twos + _threes + _fours + _fives + _sixes;
For the remaining score slots, the result is conditional. As an example, a 3 of a kind scores the sum of the values if there are 3 of a value, otherwise, it scores 0. These depend on the presence of a value, not a key, thus I needed to do an iteration over the map. I wrote a helper function to help me here:
function maphas<K, V>(map: Map<K, V>, func: Function): boolean {
let r: boolean = false;
map.forEach((value: V, key: K) => {
if (func(key, value)) {
r = true;
}
});
return r;
}
$: hasThreeOfAKind = maphas($dice, (_: number, val: number) => val >= 3);
$: _threeOfAKind = hasThreeOfAKind ? sum : 0;
Similar things were done for hasFourOfAKind
and hasYahtzee
. Full house requires both exactly two of a kind and exactly three of a kind:
$: hasExactlyThreeOfAKind = maphas($dice, (_: number, val: number) => val === 3);
$: hasExactlyTwoOfAKind = maphas($dice, (_: number, val: number) => val === 2);
$: _fullHouse = (hasExactlyTwoOfAKind && hasExactlyThreeOfAKind) ? 25 : 0;
Large and small straight do not require iteration, as simply the presence of certain numbers is enough. While it is possible to come up with an algorithm to detect the presence of straights, I simply hard coded it, as there are a finite number of options available.
$: hasSmallStraight = $dice.has(3) && $dice.has(4) && (
($dice.has(1) && $dice.has(2)) ||
($dice.has(2) && $dice.has(5)) ||
($dice.has(5) && $dice.has(6)) );
$: hasLargeStraight = ($dice.has(1) || $dice.has(6)) &&
$dice.has(2) && $dice.has(3) && $dice.has(4) && dice.has(5);
$: _smallStraight = hasSmallStraight ? 30 : 0;
$: _largeStraight = hasLargeStraight ? 40 : 0;
The last thing that needs to be taken into account is the fact that bonus Yahtzees need to be scored. To do this, I moved the yahtzee final score above the display score and had the display score make use of its value to determine if it is scoring a bonus.
let yahtzee: number = 0;
$: _yahtzee = hasYahtzee
? (yahtzee === 0 ? 50 : yahtzee + 100)
: yahtzee;
And the game of Yahtzee is now playable!
Restarting the Game on Completion
Once all of the slots are filled, the game needs to restart. Currently, the DiceBar has no idea that the game is done and tries to prompt the user for another roll. To do this, I created another store called usedSlots
which tracks the number of slots that are filled.
export const usedSlots = writable(0);
Once 13 slots are filled in, the DiceBar should self-disable, and once the slots have been cleared, the DiceBar should reset.
$: if ($usedSlots === 13) {
disabled = true;
rerolls = 0;
}
$: if ($usedSlots === 0) {
disabled = false;
rerolls = 3;
}
The ScoreSlot components need to increment usedSlots
when filled. It also needs to reset itself when usedSlots
is cleared.
import { ready, usedSlots } from './stores';
// ...
function onClick() {
used = true;
$ready = false;
$usedSlots += 1;
dispatch('assignment');
}
$: if ($usedSlots === 0) {
used = false;
}
The YahtzeeSlot component needs to do the same, but only if it has not already been filled, to account for bonus Yahtzees.
import { ready, usedSlots } from './stores';
// ...
function onClick() {
if (displayScore === 0) {
burned = true;
}
if (score === 0) {
$usedSlots += 1;
}
$ready = false;
dispatch('assignment');
}
$: if ($usedSlots === 0) {
burned = false;
}
The ScoreBoard needs to know to reset its values upon completion:
$: if ($usedSlots === 0) {
ones = 0;
twos = 0;
// Same for threes, fours, fives, sixes, threeOfAKind, fourOfAKind,
// fullHouse, smallStraight, largeStraight, yahtzee and chance
}
There is still no way to actually reset the game, which at this point just requires setting $usedSlots
to 0.
The Leader Board
Now that we have the ability to create high scores, the game needs a place to display them for users to brag to their friends. For the UI, I created a text box for a name entry and a button. The actual leader board will render as a table when there is data. This is what the Leaderboard component looks like:
<script lang="ts">
import { usedSlots } from './stores';
let disabled: boolean = true;
let nickName: string = "";
$: if ($usedSlots === 13) {
disabled = false;
}
let leaderboard = [];
// TODO - Get historical data
function onSave() {
// TODO - Save historical data
disabled = true;
$usedSlots = 0;
nickName = "";
}
</script>
<input type="text" autocomplete="off" {disabled} bind:value={nickName}>
<button on:click={onSave} {disabled}>Submit</button>
<br>
{#if leaderboard.length === 0}
No scores in leaderboard yet!
{:else}
<table>
<tr>
<th>Name</th>
<th>Date</th>
<th>Score</th>
</tr>
{#each leaderboard as leader (leader.date)}
<tr>
<td>{leader.name}</td>
<td>{leader.date.toDateString()}</td>
<td>{leader.score}</td>
</tr>
{/each}
</table>
{/if}
I then adjusted the App component to include the Leaderboard:
<script lang="ts">
import DiceBar from './DiceBar.svelte';
import Leaderboard from './Leaderboard.svelte';
import ScoreBoard from './ScoreBoard.svelte';
</script>
<main>
<ScoreBoard/>
<DiceBar/>
<Leaderboard/>
</main>
The game will now be completely playable, as when a game is over, it can be restarted when the player adds their name to the leader board. The new problem is that currently the leader board does not actually display any data. Nothing is actually added to the board, so it remains blank. I fixed this by adding one last store: $totalScore
.
export const totalScore = writable(0);
The ScoreBoard component writes to it when all the components are full:
$: if ($usedSlots === 13) {
$totalScore = grandTotal;
}
And finally, the Leaderboard needs to use this to actually add a record to the leaderboard
array:
function onSave() {
leaderboard = [ ...leaderboard, {
name: nickName,
score: $totalScore,
date: new Date()
}];
// The rest of the function
}
And now, the leader board should actually display data when the round finishes!
Leader Board Persistence using IndexedDB
The leader board is not yet complete because the data is stored in browser memory. If the user refreshes the page, or navigates away and back, the contents will be lost. To keep the data stored in a way that it will always be there, I used IndexedDB, a standardized NoSQL object store supported by all modern browsers. I used it with the idb package, which makes it work with async/await. For more information about how to use it, this is a good resource.
I created a class called YahtzeeDB that manages the connection to the DB. My schema is basically a single table with columns for name, date, and score. The date column is the primary key, as there will never be two scores created at exactly the same time. In addition, we will want to get the entries by the score variable, as the leader board should start by showing the leaders. This requires an index to be made on the column.
The public API needs to be a function to get the entries in the DB by score, and the ability to add a new score to the leader board, taking a name and score.
import { IDBPDatabase, openDB } from 'idb';
const DB_NAME = 'djk-yahtzee-svelte';
const DB_VERSION = 1;
const DB_STORE = 'Leaderboard';
const DB_LEADERBOARD_SCORE_IDX = 'Leaderboard-Score-Idx';
class YahtzeeDB {
db: IDBPDatabase;
intitialized: boolean;
constructor() {
this.initialized = false;
}
async init() {
// TODO - initialize the database
this.initialized = true;
}
async getLeaderboard() {
if (this.initialized === false) {
await this.init();
}
// TODO - get leaderboard from the database
}
async addNewScore(name: string, score: number) {
if (this.initialized === false) {
await this.init();
}
let newScore = {
name: name,
score: score,
date: new Date()
};
// TODO - add newScore to the database
}
}
export let yahtzeeDB = new YahtzeeDB;
To initialize the database, the openDB
function is used. It takes a name, version, and an object that contains functions describing what actions should be taken in a variety of circumstances. It supports versioning, thus I defined the DB_VERISON
constant, but I haven’t needed to make a schema migration yet beyond the initial creation.
this.db = await openDB(DB_NAME, DB_VERSION, {
upgrade(db, oldVer, newVer, transaction) {
const store = db.createObjectStore(DB_STORE, { keyPath: 'date' });
store.createIndex(DB_LEADERBOARD_SCORE_IDX, 'score');
}
});
An object store is what most DBs call a table. Because it stores objects with arbitrary keys, I do not need to specify every key it can contain. I just need to specify the primary key that the DB uses internally to organize the data. Because I want to get data out organized by score instead of date, I then create an index on that parameter. In the future, if I want to make changes, I can use the oldVer
and newVer
parameters to ensure a smooth transition.
In order to get the leader board, I used the following code:
const tx = this.db.transaction(DB_STORE, 'readonly');
const tstore = tx.objectStore(DB_STORE);
const tsidx = tstore.index(DB_LEADERBOARD_SCORE_IDX);
return await tsidx.getAll();
I specify that I only need to read from the DB. This helps the DB avoid extra contention if multiple connections to the DB are made at once. The transaction takes a list of all stores that it needs to touch, though for this example, I only have one store. I then specify which specific store I would like to query. Next, I specify which index on the store I would like to query. Finally, I run the query and return the data.
To add a new score to the leader board, I used the following code:
const tx = this.db.transaction(DB_STORE, 'readwrite');
const tstore = tx.objectStore(DB_STORE);
await tstore.add(newScore);
The same rules apply regarding transactions, except because I am writing to the table, I need to specify this. I do not need to notify an index to add a new object to the DB.
To make use of our new database in the Leaderboard, I start by populating the leaderboard
array when the component is loaded. Note that we need to reverse the leaders list we get from the DB, because of the direction that the index works.
yahtzeeDB.getLeaderboard().then(leaders => {
leaderboard = leaders.reverse();
});
Finally, I need to add new scores to the DB on save. I replaced the existing onSave
function with the following:
function onSave() {
yahtzeeDB.addNewScore(nickName, $totalScore).then(() => {
yahtzee.getLeaderboard().then(leaders => {
leaderboard = leaders.reverse();
});
});
disabled = true;
$usedSlots = 0;
nickName = "";
}
And with this change, the leaderboard will persist between sessions in the local storage of the browser.
Conclusion and Next Steps
There is still some room for improvement here, but I learned a lot about how the web works in practice. I found Svelte to be enjoyable to work in, and I look forward to what future innovations like SvelteKit bring to the project. For this Yahtzee game, you can find it on my GitHub here.
Next on my educational ToDo list is to learn React, as it is unavoidable in the modern job market. Hopefully I will have a new blog entry about that soon.
Leave a Reply