Building a word game looks simple at first.
A Boggle-style game seems to need only a letter grid, a word list, some input handling, and a score counter. But once the game needs to work well on both desktop and mobile, the implementation becomes a combination of UI state management, graph traversal, word validation, touch interaction, and performance optimization.
I recently built a browser-based Boggle game using Astro and Vue 3, and this article explains some of the engineering decisions behind it.
The goal was not to build a large game engine. Instead, I wanted a lightweight architecture where Astro handles the website and content-oriented pages, while Vue manages the interactive game itself.
The resulting project is Puzzle Boggle.
Why Astro + Vue 3?
Astro is particularly interesting for projects that contain both static content and interactive applications.
A word game website usually has two very different types of pages:
- Content and informational pages
- Highly interactive game pages
A traditional SPA can handle the second problem well, but it can be unnecessarily heavy for the first one.
Astro provides a useful separation between the two.
The static parts of the site can be rendered by Astro, while Vue components can be introduced where interactive behavior is required.
The basic architecture looks roughly like this:
Astro
│
├── Layout
├── SEO / Metadata
├── Content Pages
├── Game Pages
│
└── Vue
├── GameBoard
├── LetterTile
├── WordInput
├── ScorePanel
└── GameResult
The important idea is that not everything needs to become a Vue application.
Only the parts that actually need client-side state need to be hydrated.
Separating the Game UI From the Website
One of the first architectural decisions was separating the website layer from the game layer.
The Astro side is responsible for things such as:
- page structure
- navigation
- metadata
- informational content
- SEO
- static layouts
Vue is responsible for:
- current game state
- selected letters
- user input
- word validation
- scoring
- timers
- animations
- game completion
This separation makes the project easier to reason about.
The game can behave like a small application inside a larger content-oriented website.
For example:
src/
├── components/
│ ├── GameBoard.vue
│ ├── LetterTile.vue
│ ├── ScorePanel.vue
│ └── GameResult.vue
│
├── composables/
│ ├── useGame.ts
│ ├── useBoard.ts
│ └── useTimer.ts
│
├── data/
│ ├── dictionaries/
│ └── puzzles/
│
├── pages/
│ ├── index.astro
│ ├── daily-challenge/
│ └── boggle/
│
└── layouts/
└── Layout.astro
This is not the only valid structure, but separating UI components, game logic, and data early makes the project much easier to maintain.
Modeling the Boggle Board
The board can be represented as a two-dimensional array.
For example, a 4 × 4 board can be represented as:
const board = [
['T', 'R', 'A', 'P'],
['E', 'S', 'I', 'N'],
['L', 'O', 'G', 'D'],
['M', 'E', 'T', 'A']
]
Each cell needs to contain more than just a letter during gameplay.
A useful internal representation is:
interface Cell {
row: number
col: number
letter: string
}
The row and column coordinates are important because Boggle is fundamentally a path-search problem.
A player is not simply selecting letters.
They are creating a path through adjacent cells.
The Core Gameplay Problem Is a Graph Traversal
This is probably the most interesting part of implementing Boggle.
Each letter tile can be considered a node in a graph.
A tile can connect to its neighboring tiles:
A B C
D E F
G H I
From E, the player can potentially move to:
A B C
D F
G H I
That means the game board can be treated as a graph where each tile has up to eight neighbors.
The eight possible directions are:
const directions = [
[-1, -1],
[-1, 0],
[-1, 1],
[ 0, -1],
[ 0, 1],
[ 1, -1],
[ 1, 0],
[ 1, 1]
]
When a player selects a tile, the game needs to determine whether the next tile is adjacent to the previous tile.
A simple helper function can handle this:
function isAdjacent(
a: Cell,
b: Cell
): boolean {
const rowDistance = Math.abs(a.row - b.row)
const colDistance = Math.abs(a.col - b.col)
return (
rowDistance <= 1 &&
colDistance <= 1 &&
!(rowDistance === 0 && colDistance === 0)
)
}
The next important rule is that the same tile cannot normally be used twice in the same word.
So the game state needs to maintain the path:
const selectedCells = ref<Cell[]>([])
When the player selects a new tile, the game checks:
- Is this tile adjacent to the previous tile?
- Has this tile already been selected?
- If valid, add it to the current path.
- Otherwise reject the selection.
These rules become especially important on mobile devices where users interact with the board using touch rather than a mouse.
Using Vue 3 Composition API
Vue 3's Composition API is a good fit for this type of game because game logic can be separated into reusable composables.
For example:
const score = ref(0)
const currentWord = ref('')
const selectedCells = ref<Cell[]>([])
const foundWords = ref<string[]>([])
const gameOver = ref(false)
Then the logic can be organized into functions:
function selectCell(cell: Cell) {
if (!canSelect(cell)) {
return
}
selectedCells.value.push(cell)
currentWord.value += cell.letter
}
And:
function submitWord() {
const word = currentWord.value.toLowerCase()
if (!isValidWord(word)) {
resetSelection()
return
}
if (foundWords.value.includes(word)) {
resetSelection()
return
}
foundWords.value.push(word)
score.value += calculateScore(word)
resetSelection()
}
This is one of the reasons I prefer Composition API for interactive game components.
The state and behavior can stay close together without forcing the entire game into one huge component.
Word Validation Is a Separate Problem
The board traversal algorithm and the dictionary system should not be tightly coupled.
These are two different problems:
Can the player create this sequence of letters?
↓
currentWord
↓
Is this sequence a valid word?
↓
dictionary
Keeping them separate makes the game easier to modify.
For example, the dictionary can be loaded into a Set:
const dictionary = new Set([
'apple',
'orange',
'stone',
'game'
])
Then validation becomes very fast:
function isValidWord(word: string) {
return dictionary.has(word)
}
In a real game, the dictionary is obviously much larger.
One of the interesting engineering challenges is therefore not simply "finding words", but deciding how much dictionary data should be loaded into the browser.
Client-Side vs Server-Side Validation
For a casual browser word game, client-side validation can be extremely fast.
The flow can be:
Player
↓
Select tiles
↓
Build word
↓
Check dictionary
↓
Calculate score
↓
Update UI
This avoids sending a network request every time the player submits a word.
For a competitive game, however, there is another consideration.
If the client is responsible for everything, a technically sophisticated player can potentially manipulate the game state.
For a casual game this may be acceptable.
For competitive scoring or leaderboards, important results should be verified on a server.
That leads to a useful distinction:
Local gameplay
↓
Fast client-side validation
Competitive result
↓
Server-side verification
This architecture also makes it easier to introduce daily challenges or leaderboards later.
Generating Interesting Boards
Random letters do not necessarily produce an interesting Boggle board.
A completely random board can generate too many impossible combinations or produce boards with very few playable words.
A better approach is to treat board generation as a constrained problem.
For example:
Random letters
↓
Generate candidate board
↓
Evaluate board
↓
Check word count
↓
Check difficulty
↓
Accept / regenerate
The board generator can evaluate a candidate board using a dictionary and estimate how many valid words are available.
This creates an interesting separation between:
- Board generation
- Word discovery
- Difficulty evaluation
- Gameplay
A deterministic seed can also be useful.
For example:
generateBoard(seed)
If the same seed produces the same board, a daily puzzle can be reproduced consistently.
This is useful for a daily challenge system because every player can receive the same puzzle.
Designing a Daily Challenge
A daily puzzle introduces another interesting engineering problem.
The simplest model is:
date
↓
deterministic seed
↓
board generator
↓
daily board
For example:
const seed = createDailySeed('2026-09-26')
const board = generateBoard(seed)
The server does not necessarily need to store every generated board if the generation algorithm is deterministic.
Instead, the date can act as the source of the seed.
This can significantly simplify the data model.
The same concept can also be used for:
- daily Sudoku
- daily Wordle-style games
- daily crossword puzzles
- daily logic puzzles
Mobile Interaction Is Harder Than Desktop Interaction
A desktop implementation can start with:
mousedown
mousemove
mouseup
But mobile users interact with their fingers.
That introduces several issues:
- Finger size is much larger than a mouse cursor.
- The finger can obscure the selected tile.
- Touch movement can accidentally trigger page scrolling.
- Rapid movement between tiles needs to feel responsive.
- The selection path needs clear visual feedback.
The interaction therefore needs to be designed around touch from the beginning rather than treating mobile as an afterthought.
A good Boggle interface should make the current path visually obvious.
For example:
[ T ][ R ][ A ][ P ]
\ \
[ E ][ S ]
The exact visual design can vary, but the user should immediately understand:
- which tiles are selected
- what word is being formed
- where the path started
- what happens when the finger moves
Keeping the Vue Component Manageable
One mistake I wanted to avoid was putting everything inside GameBoard.vue.
A game board can quickly become responsible for:
Board rendering
Touch events
Mouse events
Word validation
Scoring
Timer
Animations
Game state
Statistics
API calls
That becomes difficult to maintain.
Instead, I prefer smaller responsibilities.
For example:
GameBoard.vue
↓
useGame()
↓
useBoard()
↓
useWordValidation()
↓
useScore()
The components handle presentation while composables handle stateful behavior.
This is especially useful when the same game logic needs to be reused by different interfaces.
Astro Handles the Site, Vue Handles the Game
The final architecture can be summarized as:
Astro
│
┌───────────┼───────────┐
│ │ │
Content SEO Routing
│ │
└───────────┬───────────┘
│
Vue Island
│
┌───────┴────────┐
│ │
Game Board Game State
│ │
Touch / Mouse Composables
│ │
└───────┬────────┘
│
Word Engine
│
┌───────┴────────┐
│ │
Dictionary Board Logic
This architecture keeps the interactive portion focused while allowing the rest of the website to remain lightweight.
It also leaves room for future features without turning the entire website into a monolithic JavaScript application.
What I Learned From Building It
The biggest lesson is that a simple word game is actually several different engineering problems combined together.
The core gameplay requires:
- Grid modeling
- Graph traversal
- Path validation
- Dictionary lookup
- Scoring
- State management
- Touch interaction
- Board generation
- Difficulty balancing
- Performance considerations
The technology stack is only part of the problem.
The more interesting challenge is finding clean boundaries between these systems.
Astro works well for the website layer because it allows the application to contain static and interactive parts without forcing everything to become a client-side application.
Vue 3 works well for the game layer because reactive state and composable logic map naturally to gameplay.
And the Boggle rules themselves provide a surprisingly interesting algorithmic problem.
A Small Game Is a Good Engineering Exercise
One reason I enjoy building browser games is that they force you to think about both software architecture and user experience.
A Boggle board is small.
The number of rules is small.
The interface is small.
But implementing it well requires thinking about algorithms, state, input devices, performance, accessibility, and product design at the same time.
That's what makes these projects useful engineering exercises.
If you'd like to see the result, I've published the browser version as Puzzle Boggle.
The project is still evolving, but building it has been a useful way to explore how Astro and Vue 3 can work together for a small, interactive web application.
I hope some of these architectural ideas are useful for anyone building a browser game or another interactive application with Astro and Vue.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.