In the previous article we wrote the main component for our app: the Word component. Now, it's time to implement some game logic!
The models
Create a models/game.ts file which will store our models.
First, we have a GameInfo interface: this is what could be eventually be store on a server (we won't have one, but we'll pretend that we do). It stores the 5 middle words, each with its own hint, the 2 edge words (top and bottom) and a common hint.
/** What comes from the server */
export interface GameInfo {
words: Array<{ correct: string; hint: string }>;
edgeWords: [string, string];
edgeHint: string;
};
Then, we need a similar interface, which I called Game, which stores some additional strings: the user's current values.
/** What we store in our component */
export interface Game {
words: Array<{ correct: string; hint: string; current: string }>;
edgeWords: [string, string];
currentEdgeWords: [string, string];
edgeHint: string;
};
This is what I imagined for the purpose of this demo, but it's not the only way to store the data.
As I said, we won't use an actual server, so let's export some fake data!
export const mock: GameInfo = {
words: [
{ correct: 'CARE', hint: 'Compassionate attention given to someone' },
{ correct: 'CORE', hint: 'Discarded part of an apple' },
{ correct: 'CORK', hint: 'Stopper in a champagne bottle' },
{ correct: 'FORK', hint: 'Eating utensil with tines' },
{ correct: 'FORT', hint: '____nite (popular online game)' }
],
edgeWords: ['BARE', 'FOOT'],
edgeHint: 'Compound word for "having no shoes or socks on"'
};
Remember that the 2 edge words share a common hint!
So, to recap, the user will have to guess these words in this exact order:
- BARE
- CARE
- CORE
- CORK
- FORK
- FORT
- FOOT
The service
We'll store pretty much all of our data in a service, so create a services/game.service.ts file.
This is where you may use a State Management solution, like NgRx. We'll keep it simple and use Angular Signals!
@Injectable({ providedIn: 'root' })
export class GameService {
}
This service will hold 2 states: game and focused.
@Injectable({ providedIn: 'root' })
export class GameService {
game = signal<Game | null>(null);
focused = signal<number | null>(null);
}
game will store the current Game object, and focused will store the index of the focused word (in our case, since we have 7 words, 0 to 6). We'll take advantage of this state to show the appropriate hint to the user.
Let's implement a method to initialize the game object. In a real scenario, you would perform an HTTP request, but we'll use our mock data!
init() {
const emptyString = Array(mock.edgeWords[0].length).fill(' ').join('');
this.game.set({
...mock,
words: mock.words.map(word => ({ ...word, current: emptyString })).sort(() => Math.random() - 0.5),
currentEdgeWords: [emptyString, emptyString]
});
}
Here we're simply populating the current words with empty strings. So, if our words have 4 letters, the current words will be 4 empty characters each.
We're also randomizing the order of our words (see that Math.random()?), so that the user will be forced to sort them correctly!
The derived states
All of the info we need can be derived from our game and focused states, so let's create some derived states using computed.
First, let's calculate the word length, just for convenience.
wordLength = computed(() => this.game()?.edgeWords[0].length || 0);
It doesn't matter which word you choose, they have the same length.
We also need to know the current game status. The game could be in 5 different states:
-
idle(game not loaded yet) -
error(not solved at all) -
unsorted(middle words are solved, but not sorted) -
sorted(middle words have been sorted) -
solved(the edge words have been solved)
In order to calculate it, we need a helper method: let's call it differByOne(), and its job will be to tell us if 2 strings differ by exactly one character.
private differByOne(str1: string, str2: string) {
if (str1.length !== str2.length) return false;
let diffCount = 0;
for (let i = 0; i < str1.length; i++) {
if (str1[i] !== str2[i]) diffCount++;
if (diffCount > 1) return false;
}
return diffCount === 1;
}
Now we can write our gameStatus derived state!
gameStatus = computed(() => {
});
First, we check if there is a game at all. If we have no game, we return idle.
const game = this.game();
if (!game) return 'idle';
Then, we check if the middle words have been solved, and if not, we return error.
const isHalfSolved = game.words.every(word => word.correct === word.current);
if (!isHalfSolved) return 'error';
Then, we check if the middle words have been sorted correctly, and if not, we return unsorted.
const isSorted =
isHalfSolved
&& game.words.every((word, i) => i === game.words.length - 1 || this.differByOne(word.current, game.words[i + 1].current))
if (!isSorted) return 'unsorted';
The words could be arranged in reverse order, it doesn't matter.
Finally, we check if the edge words are correct (and we check that the top one and the first middle word differ by one character, to make sure they're written in the correct order).
const isSolved =
isHalfSolved
&& isSorted
&& game.currentEdgeWords.every((word, i) => game.edgeWords.includes(word))
&& this.differByOne(game.currentEdgeWords[0], game.words[0].current);
if (!isSolved) return 'sorted';
return 'solved';
This derived state is done!
We just need another one: we'll call it currentHint, and it will store the hint to show to the user based on the gameStatus and the focused index.
currentHint = computed(() => {
if (this.gameStatus() === 'solved') return 'Solved!';
if (this.gameStatus() === 'sorted') return 'Top + bottom: ' + this.game()!.edgeHint;
if (this.gameStatus() === 'unsorted') return 'Sort the rows!';
if (this.focused() === null) return undefined;
return this.game()?.words.at(this.focused()!)!.hint;
});
This should be self-explanatory.
We're almost done with our service! We just need 3 additional methods to mutate our game state.
First, create a replaceWord method to change the value of one of the middle words:
replaceWord(index: number, text: string) {
this.game.update(game => (game && {
...game,
words: game.words.map((word, i) => {
if (index !== i) return word;
return { ...word, current: text }
})
}))
}
The
indexwill go from0to4since we're only dealing with the 5 middle words.
Then, two similar methods to replace the edge words:
replaceTop(word: string) {
this.game.update(game => (game && {
...game,
currentEdgeWords: [word, game.currentEdgeWords[1]]
}));
}
replaceBottom(word: string) {
this.game.update(game => (game && {
...game,
currentEdgeWords: [game.currentEdgeWords[0], word]
}));
}
And we're done! In the next article we'll take advantage of our Word component and our newly created GameService to display the actual game, with the help of the Angular CDK.
At the end, we'll have something like this:

Top comments (0)