Tinkerings

Rock Paper Scissors Lizard Spock: Compressing 25 Matchups Into Two Comparisons

Date Published

## Situation


I'd already built Rock Paper Scissors in Angular 8. It was fine — three weapons, three matchups per pair, nine outcomes, and a switch statement that handled all of them. Then I decided to add Lizard and Spock, the variant popularised by *The Big Bang Theory*. Suddenly there were five weapons and twenty-five matchups. A naive implementation would have meant a chain of twenty-five conditionals, or a 5×5 lookup table, or a map of nested arrays. None of those felt right for what was, at heart, a small game.


## Task


Extend the existing Angular implementation to support all five weapons without letting the win-condition logic balloon. Keep the state model simple. Use the existing UI framework — Angular Material and Font Awesome — rather than introducing new dependencies.


## Action


**1. Modelled the weapons as an ordered array.**

```ts

weapons = ['rock', 'paper', 'scissors', 'spock', 'lizard'];

```

The order is not arbitrary. It's chosen so that the win relationships can be expressed arithmetically. Each weapon has an integer index that participates in the outcome calculation. The array is the data structure *and* the mathematical model.


**2. Encoded the win condition as modular arithmetic.**

```ts

if (this.playerWeapon === this.computerWeapon) {

// DRAW

this.result = 0;

} else if (((this.playerWeapon - this.computerWeapon + 5) % 5) === 1) {

// PLAYER WIN

} else if (((this.playerWeapon - this.computerWeapon + 5) % 5) === 3) {

// PLAYER WIN

} else {

// COMPUTER WIN

}

```

The expression `(player - computer + 5) % 5` reduces the matchup to an offset in the range 0–4 on a modular ring. The `+ 5` is the standard trick to keep the result positive when `player < computer`, since JavaScript's `%` operator returns the sign of the dividend.


**3. Derived the offsets from the rule set rather than hardcoding them.**

With the ordering `[rock, paper, scissors, spock, lizard]`, every winning matchup resolves to an offset of either 1 or 3. For example:


- rock (0) crushes scissors (2): `(0 - 2 + 5) % 5 = 3` ✓

- paper (1) covers rock (0): `(1 - 0 + 5) % 5 = 1` ✓

- scissors (2) cuts paper (1): `(2 - 1 + 5) % 5 = 1` ✓

- lizard (4) poisons spock (3): `(4 - 3 + 5) % 5 = 1` ✓

- spock (3) vaporizes rock (0): `(3 - 0 + 5) % 5 = 3` ✓


The ten winning relationships collapse into two offset values. Twenty-five possible matchups are resolved by a single arithmetic expression and three comparisons.


**4. Chose the array ordering specifically to make this possible.**

This is the part that matters. The order is not the natural one — it's not alphabetical, not the order the weapons appear on the show, not the order they were invented in. It's the order that makes the modular arithmetic work. The data structure encodes the rule set.


**5. Stored both scores in a single array.**

```ts

scores = [0, 0];

```

`this.scores[0]` is the player. `this.scores[1]` is the computer. The array is used as a two-element slot machine — index 0 or 1 — and the increment is written once per branch rather than duplicated across separate variables.


**6. Mapped the result to display strings through a lookup array.**

```ts

resultStrings = [

'DRAW',

'PLAYER WIN',

'COMPUTER WIN',

''

];

```

The calculation produces an integer; the display layer translates it. The result of the game is never a string internally — it's a number that indexes into the display array. That separation means the logic can change without touching the UI strings, and the strings can be localised without touching the logic.


**7. Used a `chooseWeapon(weapon)` method that keeps the UI free of game logic.**

```ts

chooseWeapon(weapon: number): void {

this.playerWeapon = weapon;

this.computerWeapon = Math.floor(Math.random() * 3);

this.calculateResult();

}

```

The template calls `chooseWeapon(0)` or `chooseWeapon(4)` — it passes an index, nothing more. All the arithmetic lives in the component. The template stays declarative; the logic stays testable.


**8. Placed the win calculation in its own method.**

`calculateResult()` is called from `chooseWeapon()` but lives as a separate method. It could be called from a test, from a debugger, or from a future feature that replays a game. The calculation is not entangled with the click handler.


**9. Composed the UI with Angular Material's grid and toolbar components.**

`mat-toolbar`, `mat-grid-list`, and `mat-grid-tile` structure the layout without custom CSS for the shell. The weapon list is a plain `<ul>` with Font Awesome icons — `fa-hand-rock-o`, `fa-hand-paper-o`, `fa-hand-scissors-o`, `fa-hand-lizard-o`, `fa-hand-spock-o`. The framework handles the chrome; the game handles the logic.


**10. Displayed the scores as Material badges.**

```html

<h2 matBadge="{{ scores[0] }}" matBadgeColor="accent">Your score</h2>

<h2 matBadge="{{ scores[1] }}" matBadgeColor="accent">Computer score</h2>

```

The score is a badge, not a paragraph. It's what Material is for — the visual hierarchy is idiomatic rather than bespoke.


**11. Typed every field on the component.**

`weapons: string[]`, `playerWeapon: number`, `computerWeapon: number`, `scores: number[]`, `result: number`, `resultStrings: string[]`. Each method has an explicit return type — `calculateResult(): void`, `chooseWeapon(weapon: number): void`. The state is fully described by the type system, and the compiler catches mismatches.


**12. Kept the game state on the component rather than in a service.**

For a single-screen game with no persistence and no routing between states, a component is the right home. Introducing a service here would be ceremony without benefit. The state is small, local, and dies with the view — which is exactly what it should do.


**13. Wrote the display message as a composition of the computer's choice and the result.**

```ts

this.resultMsg = "Computer chooses " + this.weapons[this.computerWeapon] + " ||| " + this.resultStrings[this.result];

```

The message is assembled from the weapon array and the result strings. Both are already available. No new state is introduced to render the outcome.


**14. Matched the UI indices to the array indices.**

The template's click handlers pass 0 for rock, 1 for paper, 2 for scissors, 4 for lizard, and 3 for spock — the same integer positions the weapons occupy in the array. There is no translation layer between what the user clicks and what the logic sees. One representation, used everywhere.


**15. Extended the original game rather than rewriting it.**

The three-weapon game's structure — weapons array, scores array, result integer, result strings — was preserved. Adding lizard and spock meant adding two entries and changing the mod operation from 3 to 5. The architecture absorbed the extension without a redesign.


## Result


A working Rock Paper Scissors Lizard Spock game in Angular 8, with the full five-weapon rule set implemented through a single modular arithmetic expression. Twenty-five possible matchups are resolved by three comparisons and one subtraction. The state model is small, typed, and local to the component. The UI uses Angular Material and Font Awesome without bespoke styling for the shell.


The interesting decision is the array ordering. It isn't the order the weapons appear on the show, and it isn't the order they were invented in. It's the order that makes the arithmetic work — where every win is offset 1 or 3 in a mod-5 ring. That's the compression: not code golf, but a data structure chosen so that the rule set expresses itself mathematically.