Building the Slot Machine SPARK Edition
From a 2023 legacy app to a fully integrated TNT simulation —
the refactor process, the feature decisions, and the teaching moments.
This log documents the session that refactored the 2023 Slot Machine app into the 2026 SPARK Edition. The focus is on how decisions were made — which parts of the original to keep, which to change, and why certain tools (emoji vs Font Awesome) were chosen.
Watch for Prompt Critique boxes (amber) and Design Decision boxes (teal). The code is a side effect of these decisions.
This is a processing app from the former TNT site. I’m happy with its functionality, I’d just like to refactor it to look like our current TNT apps. It will need an entry in our ‘Simulations’ card of explore, processing apps, and since you are helping me upgrade/update it, let’s use it as a SPARK teaching/development training app, with a chatlog. Looking at the slotMachineIndex.html file, how should we proceed? I’m open to improvement suggestions but I do like how the former app worked.
This is a strong opening prompt for a refactoring task. It:
- States the existing state — a legacy processing app with good functionality
- States the desired state — fits the 2026 TNT ecosystem, listed in explore and processing pages
- Sets a key constraint — “I do like how the former app worked” (preserve the game engine)
- Asks for analysis first — “how should we proceed?” instead of “refactor it now”
- Signals openness — “open to improvement suggestions”
The one thing that would make it stronger: specifying which files are most important to read. Saying “the key files are sketch.js, Reel.js, and index.html” would have saved one round of the AI reading the folder structure first.
After reading all files, here is what I found and what I recommend:
Reel.js— clean, well-structured class. No reason to touch it at all.sketch.js— solid p5.js global-mode sketch. Needs: dark background, theme expansion, win detection,updateMsg()HTML integration. Stage number removed.index.html— Bootstrap 5.0.2, old navbar pattern, hand-rolled footer. Full rewrite needed for 2026 ecosystem.- Styles — replace
siteStyles.csswith a singleslotMachineStyles.cssusing TNT custom properties.
Suggested improvements: three emoji theme sets (sports, food, casino) + digits; win detection on centre row; dark canvas background to make emoji pop.
The AI read every file before writing a line. On a refactoring task this is
mandatory. Reading revealed that Reel.js was clean enough to copy unchanged —
a decision that shaped the entire architecture. If the AI had started rewriting immediately, it would
have likely “improved” Reel.js unnecessarily, introducing new bugs into working code.
The constraint “I like how it worked” and the read-first discipline together
produced the right answer: change as little as possible.
Yes, proceed, but why not let the user pick the theme: sports, food or casino symbols? A new feature: I’m guessing we could use FA icons or would I need to provide noun-project icons? What’s best?
This is an excellent example of asking before committing to a tool. The user suspected FA icons might work but wasn’t sure. Asking saved significant time — if they had said “use FA icons,” the AI would have had to build it, discover the rendering problem, and then rebuild with emoji anyway.
Habit to build: whenever you’re choosing between multiple implementation approaches, ask the AI to compare them before picking one. The cost is one extra prompt turn. The benefit is avoiding a rebuild.
Emoji is the clear winner for p5.js canvas work. Here is why each option falls short:
Font Awesome: FA icons render via CSS — the browser draws them using
a special web font and CSS ::before pseudo-elements. A <canvas>
element has no CSS context. To use FA glyphs inside a canvas you would need to: (1) load the FA
font as a FontFace object, (2) wait for the font to load, (3) look up the
Unicode hex glyph codes for each icon, and (4) draw them with text().
This is fragile, version-dependent, and not documented by either p5.js or Font Awesome.
In practice, glyph rendering fails silently on many browsers.
Noun Project SVGs: These are image files. In p5.js you would call
loadImage() in preload() for each one, store them as variables,
and draw each with image(x, y, w, h). This works, but it requires you to own
and manage the files, it adds a preload() function, and sizing each icon to
fit the reel width requires extra arithmetic.
Emoji: Unicode characters rendered by the OS emoji font.
p5.js text() handles them natively — zero setup, zero dependencies,
zero additional files. They are full-colour, universally supported, and the existing app
already uses them (the sports set is already the original symbol array).
This is not a workaround. It is the correct tool.
This is a fundamental concept worth internalising:
a <canvas> element and the rest of the HTML page live in separate rendering worlds.
- The HTML page renders via the CSS layout engine. Fonts, colours, margins, and pseudo-elements all live here.
- The canvas renders via a JavaScript drawing API (2D context or WebGL). No CSS here — you draw shapes, lines, images, and text manually.
FA icons are CSS constructs. They do not exist as drawable objects in the canvas rendering context. This is not a limitation of p5.js — it is a fundamental property of how the web platform works.
Emoji exist in Unicode, which means they are just characters. The canvas
text() function draws any Unicode string. The OS provides the emoji font.
No imports, no loading, no configuration. When a tool is available that does exactly
what you need with zero friction, use it.
Four architecture decisions shaped the SPARK Edition:
- Reel.js: zero modifications. The class is clean, well-encapsulated,
and works. Changing it risks introducing bugs into logic that has already been tested through
Stage 7 of development. All new features sit in
sketch.js. - Theme selector: four buttons, not a dropdown. Buttons give immediate visual feedback (the active theme is highlighted amber). A dropdown hides the options behind a click. For a game interface, visible state is better UX.
- Win detection: centre row only. The reel displays multiple symbols
vertically. The centre row is the conventional “payline” in real slot machines.
Math.floor(aReel.upperBound / 2)reliably identifies the centre symbol regardless of even or odd reel count. - Scripts at bottom of
<body>.sketch.jsreferences DOM elements (getElementById) at the global scope level, outside any function. These calls run when the script is loaded. If the script were in<head>, the DOM would not yet exist and all element references would benull. Loading scripts at the end of<body>ensures the DOM is fully parsed first.
This is a common source of bugs in p5.js projects. Global-mode p5.js
(the simpler, non-instance mode used here) works by defining setup() and draw()
as global functions that p5.js calls automatically. Any code at the top level of the script
file runs immediately when the file loads.
In this app, sketch.js has these lines at the global level:
var frameSlider = document.getElementById("frameSlider");
If this script loaded in <head>, frameSlider
would be null because the <input> element hasn’t been
parsed yet. Then every function that reads frameSlider.value would throw a
TypeError.
The fix is simple: put all app scripts at the bottom of
<body>, after all HTML elements. The page renders top-to-bottom;
scripts at the bottom run after the full DOM exists.
The loading order in the HTML is: Bootstrap → p5.js → Reel.js → sketch.js.
Each depends on the previous one being available.
Assume we have an app like Slot Machine, that can fit under multiple categories: S.P.A.R.K., Simulation, Processing. I’m inclined to think we should always have such an app cross-listed. Your view? Let’s add this reflection in our Slot Machine chatlog. I think it’s a worthy entry.
This is exactly the kind of meta-question worth asking. Rather than just adding links and moving on, the user is asking: should there be a rule here? That elevates a one-off decision into a documented convention. Good systems documentation comes from exactly this kind of prompt — noticing a pattern, naming it, and asking whether it should be policy.
Agreed — cross-listing is the right call, and worth making into an explicit convention rather than a case-by-case decision. Here is the reasoning:
The Slot Machine genuinely belongs in all three categories, but for different reasons:
- Simulations — it models randomness and probability. That’s the content: what the app does.
- Processing — it’s built with p5.js. That’s the technology: how the app was built.
- S.P.A.R.K. — the refactor session is a documented AI-assisted development conversation with prompt critiques and design decisions. That’s the pedagogy: what the app teaches about the development process.
These are orthogonal dimensions — content, technology, and pedagogy — and an app can have a genuine home in each without that being redundant. A student browsing Simulations wants to find slot machine behaviour; a student browsing Processing wants to find p5.js work; a student browsing S.P.A.R.K. wants to find documented AI sessions.
A proposed rule for the TNT ecosystem:
- Primary category = what the app does. The Slot Machine’s primary home is Simulations. That’s where a student thinking “I want to see randomness and probability” would look.
- Technology category = how it was built. If an app uses p5.js, it belongs in Processing also. If it uses PHP, it belongs in PHP also. This is especially useful for students who are studying a specific technology and want to see real examples.
- S.P.A.R.K. = any app with a documented AI-assisted development session. If there is a chat log — real prompts, critiques, design decisions — it belongs in S.P.A.R.K. This is the category for the process, not just the product. The Yahtzee SPARK Edition, the Purple People Eater exercise, the Font Awesome tutorial, and the Slot Machine all qualify.
The rule in one sentence: List an app once in its primary content category, once in its technology category (if different), and once in S.P.A.R.K. if a chat log exists. Duplication is intentional — different students navigate by different criteria.
I just had a 5-symbol run after playing with the number of symbols. It gave: football, basketball, football, football, 8-ball. It yielded “Winner! 3 of a kind!” and revealed the capture button. Do we win with a match of 3 or is there a bug because I played with the number of symbols earlier?
This is excellent field-testing communication. The user provides:
- Exact observed state — the specific five symbols displayed
- The exact output — what the app said
- A causal theory — “is it a bug from changing the slider?”
- A genuine question — not assuming either way
This is the same gold-standard format used in the Yahtzee sort-bug report. Specific state + observed output + theory = diagnosis in one turn.
No bug — that is correct behaviour. The win thresholds are:
- JACKPOT:
maxCount === myReels.length— all N symbols match - Winner:
maxCount >= 3— any 3 or more match
With 5 reels and 3 matching footballs, the >= 3 branch fires correctly.
Changing the number of symbols is handled cleanly: drawReelsInCanvas()
rebuilds the myReels array from scratch on each change, so there is no
stale state from the previous reel count.
An alternative worth considering: require a majority
(maxCount > myReels.length / 2) before declaring a win,
so 3-of-5 would be “close” while 4-of-5 would be a winner.
But this is a design choice, not a bug.
No, let’s keep it as is, but we should indicate in the ‘About’ area what it takes to be a ‘Winner’.
When field testing reveals that a feature is confusing — even if working correctly — the right fix is documentation, not code change. The user identified that the win threshold wasn’t obvious and chose the minimal correct response: explain it in the About section rather than changing the rules. This is a professional UX decision: the threshold is reasonable, the interface just needs to set expectations. Adding one sentence to About costs nothing and removes the confusion permanently.
The win threshold had been in the code since the first build, but nobody had run the app with 5 reels and hit a 3-of-a-kind until the player did. The mismatch between expectation (“is this a bug?”) and reality (“no, it’s correct”) was invisible until someone played it.
This is a recurring lesson from this session: field testing surfaces issues that code review cannot find. The Yahtzee sort bug required a specific sequence (roll → sort → click). The win threshold confusion required a specific setup (5 reels, 3-of-a-kind roll). Neither was predictable from reading the code. Build it, play it, document what you find.
I have an idea about an additional feature. If a player were to hit a jackpot, a natural desire would be to ‘prove it’. Why not have a button that captures a screenshot of the canvas as proof?
“A natural desire would be to prove it” is the entire design brief in one clause. It tells the AI the why (social proof, bragging rights) not just the what (download button). The AI could then make decisions that serve that goal: show the button only on wins, use a green colour, hide it on the next spin, name the file with the theme. None of that was specified — the motivation implied all of it. This is how strong prompts work: state the goal, let the implementation follow.
p5.js has a built-in saveCanvas(filename, format) function that
downloads the canvas as a PNG — zero dependencies needed.
Implementation decisions, each driven by the stated goal of “proof”:
- Button appears only on wins (3+ match or jackpot) — no-match spins don’t need proving
- Hidden immediately when
spin()is called — the proof is for the current result, not the previous one - Filename:
slot-machine-{theme}.png— includes the theme so the download is self-describing - Green colour — distinct from red Roll button; affirming
saveCanvas() is a p5.js built-in that calls
canvas.toBlob() and triggers a browser download —
no server, no library, no permissions beyond a normal file save.
Same principle as using emoji instead of Font Awesome for canvas symbols:
when the platform already provides exactly what you need, use it.
The feature touched three files for three separate reasons: HTML (structure — add the button element), JS (behaviour — show/hide + download), CSS (presentation — green style). Each file had exactly one reason to change. That clean separation — the single-responsibility principle again — made the implementation and testing straightforward.
Let’s create a new card, Slot Machine History, on our clips page. It should link to a new movie page, slotMachineHistory.html, populated with a YouTube clip. Let’s use our slot-machine noun-project image for its icon, and let’s link the card to our new slotMachineIndex.html page. Let’s include a subtle link on the slotMachineIndex page back to the movie page. Please add this to our chatlog too.
This prompt demonstrates a mature content-architecture instinct: a simulation app is richer when it has companion educational content. The four deliverables were all specified:
- New movie clip page (
slotMachineHistory.html) - Card in the movie clips index (with specified icon)
- Bidirectional link — movie card links to the simulation AND the simulation links back
- Chatlog entry (this one)
The “subtle link” instruction is worth noting. The user knew the link belonged in the simulation but didn’t want it to dominate the game interface. “Subtle” guided placement (in the About area, small font, alongside other secondary links) without requiring a detailed specification. Single-word qualifiers that communicate visual weight are highly efficient in UI prompts.
The movie page links to the simulation; the simulation links back to the movie page. This bidirectional pattern serves two different student entry points:
- A student browsing the Movie Clips page discovers the history clip and follows it to the simulation — content-first discovery
- A student using the simulation opens the About section and finds the history clip as context — app-first discovery
Neither path is primary. Both are valid. Bidirectional linking makes the ecosystem feel connected rather than siloed, without requiring a central index page to maintain. The cost is minimal (one link in each direction); the navigation benefit is real.
- Canvas and CSS are separate rendering worlds.
FA icons, CSS variables, pseudo-elements, and Bootstrap classes do not exist inside
a
<canvas>element. Anything drawn on canvas must be drawn programmatically. Emoji are Unicode characters and work everywheretext()does. - Preserve working code; add new code around it.
Reel.jswas not modified. The win detection, theme selector, and message system were all added insketch.jswithout touching the class. This is the single-responsibility principle in practice: the Reel class knows how to display and iterate. It does not need to know about themes or win states. - Script loading order is architecture.
In global-mode p5.js, scripts that reference the DOM must load after the DOM is ready.
Put application scripts at the bottom of
<body>. Load order: Bootstrap → p5.js → Reel.js (dependencies) → sketch.js (depends on all of the above).
| File | Status | What it does |
|---|---|---|
scripts/Reel.js | Unchanged | Original Stage 7 Reel class — exact copy |
scripts/sketch.js | Enhanced | 4 emoji themes, win detection, dark canvas, updateMsg() |
styles/slotMachineStyles.css | New | TNT amber/dark palette; replaces siteStyles.css |
slotMachineIndex.html | New | Full TNT navbar/footer, dark hero, two-column layout |
slotMachineChatlog.html | New | This page |
explore.html | Updated | Simulations card upgraded; new Simulations offcanvas |
processing_apps.html | Updated | Slot Machine figure added to app grid |
Original Stage 7 preserved untouched at:
_LegacyTNT/SlotMachineP5JSBS5-2023-10-30-S7/