Color Griddlers is a picture logic puzzle game in which cells in a grid must be colored or left blank according to numbers at the side of the grid to reveal a hidden picture. It is based on popular Nonograms pen and paper game. The game is also known as Paint by Numbers, Picross or Pic-a-Pix.
This is an implementation of the game in React with typescript. It saves progress in local storage.
Game Play
The game starts with an empty grid. I specifically designed the screen to be clutter-free and minimalistic. The player needs to fill the cells according to the numbers at the side of the grid to reveal a hidden picture.
The player can fill the cells by clicking on them. The game also has a feature to fill the cells by dragging the mouse over them. There are also keyboard shortcuts to pick a color.
When the player fills the cells correctly, the hidden picture is revealed.
Levels
The game has multiple levels of increasing difficulty.
- The first level is not yet color, just black and white.
- The second level has colors, but the colors are only revealed when the puzzle is solved.
- The third level has colors and each board has a different color scheme depending on the picture.
Making of
The game is written in React with Typescript and uses only one external library, the excellent localForage for saving the progress in local storage.
On dev side I used Vite for development and sass for styling.
Each piece of the game is a small React component; for example, this is how the clue cell is defined:
import "./ClueCellView.scss";
import React from "react";
import { Clue, FillEmpty } from "./Board";
export type Props = {
clue: Clue;
cellSize: number;
};
export const ClueCellView = ({ clue, cellSize }: Props) => {
return (
<div
className="ClueCell"
style={{
color: clue.fill ?? "none",
}}
>
{clue.fill !== FillEmpty ? clue.count : null}
</div>
);
};
For the state, I used a single context that holds the entire state of the game, which seems to work well enough:
export type GameState = {
selectedPack: PackWithProgress | null;
selectedBoard: Board | null;
nextBoard: Board | null;
completedBoards: Board[];
packs: PackWithProgress[];
};
One interesting part is the definition of the boards.
There is a (hidden) editor where the levels are defined and can use a side image for inspiration.
Initially, the format was text based using characters for each color, something like #R.B.R#
,
but I found it hard to work with, so I switched to using block colored emojis.
Here is what a board definition looks like in the code:
export const pack2: Pack = {
position: 2,
id: "8179C477-8348-4E56-84DD-37CD95565D10",
coverBoardIndex: 2,
boardSpecs: [
...,
{
positionInPack: 3,
boardId: "F54A2379-C060-4750-B7B7-797250582299",
cellSpecs: `
⬜⬜⬜⬛⬛⬜⬜⬜
⬜⬜⬛🟥🟥⬛⬜⬜
⬜⬛🟥🟥🟥🟥⬛⬜
⬛🟥🟥🟥🟥🟥🟥⬛
⬜⬛🟧🟧🟧🟧⬛⬜
⬜⬛🟧⬛🟧🟧⬛⬜
⬜⬛🟧🟧🟧🟧⬛⬜
⬜⬛⬛⬛⬛⬛⬛⬜
`,
difficulty: 1,
withHiddenColors: true,
},
...
The most complex parts where the building of a board from the spec and determining if the board is solved. These are the only parts that I covered with tests (and it was well deserved) :D. Building on the board involves computing the clues for each row and column. Solution checking involves checking if the board is filled correctly and if the clues are satisfied. For this, I took the following approach:
static isCompleted(board: Board) {
const guessedFillMatrix = BoardBuilder.mapToFillMatrix(board, true);
const cluesV = BoardClues.extractCluesV(
guessedFillMatrix,
board.spec.withHiddenColors
);
if (!BoardClues.equals(cluesV, board.cluesV)) {
return false;
}
const cluesH = BoardClues.extractCluesH(
guessedFillMatrix,
board.spec.withHiddenColors
);
if (!BoardClues.equals(cluesH, board.cluesH)) {
return false;
}
return true;
}
- first map the board to what is currently guessed
- then extract the clues for each row and column exactly the same as the board was built
- if the clues match, the board is solved
Another interesting and unexpected aspect was when I realized that a board can be solved in multiple ways. Because I am using an image of the solved board on the level screen, it was a bit strange to have it solved in one way and shown in another. I ended up saving the actual player solved solution and show that on the level screen.
The source code is available on GitHub.