-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreadme.html
More file actions
699 lines (608 loc) · 35.9 KB
/
Copy pathreadme.html
File metadata and controls
699 lines (608 loc) · 35.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>How to Use Claude to Build a Browser Game | Cat Ski README</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Crawler hints — explicit index/follow + allow large image previews. -->
<meta name="robots" content="index, follow, max-image-preview:large">
<!-- Performance hints: warm up DNS + TLS for the external origins this
page touches. -->
<link rel="dns-prefetch" href="https://www.googletagmanager.com">
<!-- Fonts are same-origin, so they preload directly. -->
<link rel="preload" as="font" type="font/woff2" href="fonts/press-start-2p.woff2" crossorigin>
<link rel="preload" as="font" type="font/woff2" href="fonts/vt323.woff2" crossorigin>
<!-- Search Console verification -->
<meta name="google-site-verification" content="Blnw1PeynoNtYxKYh57M27xYxnoz3UKCrqQ4a3mvxt8">
<!-- Google Analytics 4 with Consent Mode v2.
Mirrors the default-denied posture from index.html — GA fires no
cookies on this article page until the visitor accepts on the
game page (or vice-versa). localStorage carries the decision
across both pages. -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-MSN465FL9R"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied',
'wait_for_update': 500
});
try {
if (localStorage.getItem('skior_cookies') === 'accepted') {
gtag('consent', 'update', { 'analytics_storage': 'granted' });
}
} catch (_) {}
gtag('js', new Date());
gtag('config', 'G-MSN465FL9R');
</script>
<meta name="description" content="A walkthrough of building a browser-based skiing game with Claude as a design collaborator. Covers stack choices, per-element design criteria, color palettes derived from photography, and a free toolchain (HTML, GitHub, Vercel) you can use to ship a similar project.">
<meta name="keywords" content="claude, claude ai, claude code, build a game with claude, browser game, javascript game, html5 canvas, web audio api, pwa, skifree, retro game, vibe coding tutorial">
<link rel="canonical" href="https://ski.mike-lee.me/readme.html">
<!-- Open Graph -->
<meta property="og:type" content="article">
<meta property="og:title" content="How to Use Claude to Build a Browser Game">
<meta property="og:description" content="Walkthrough of building Cat Ski, a browser-based SkiFree tribute, using Claude as a design collaborator. Stack, design criteria, color palettes from photography, and a free deploy toolchain.">
<meta property="og:image" content="https://ski.mike-lee.me/preview.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:url" content="https://ski.mike-lee.me/readme.html">
<meta property="og:site_name" content="cat-ski">
<meta property="article:author" content="Mike Lee">
<meta property="article:published_time" content="2026-04-25">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="How to Use Claude to Build a Browser Game">
<meta name="twitter:description" content="Walkthrough of building a SkiFree-style browser game with Claude. Stack, design criteria, photography-derived color palettes, free toolchain.">
<meta name="twitter:image" content="https://ski.mike-lee.me/preview.png">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
<link rel="icon" type="image/png" sizes="48x48" href="/favicon-48.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<meta name="theme-color" content="#0b1320">
<!-- Structured data: TechArticle -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "How to Use Claude to Build a Browser Game",
"alternativeHeadline": "Building Cat Ski with Claude as a design collaborator",
"description": "A walkthrough of building a browser-based skiing game with Claude as a design collaborator. Covers stack choices, per-element design criteria, color palettes derived from photography, and a free toolchain you can use to ship a similar project.",
"image": "https://ski.mike-lee.me/preview.png",
"datePublished": "2026-04-25",
"dateModified": "2026-07-10",
"inLanguage": "en",
"isAccessibleForFree": true,
"proficiencyLevel": "Beginner",
"about": [
{ "@type": "Thing", "name": "Claude AI" },
{ "@type": "Thing", "name": "Browser game development" },
{ "@type": "Thing", "name": "HTML5 Canvas" },
{ "@type": "Thing", "name": "Web Audio API" }
],
"author": {
"@type": "Person",
"name": "Mike Lee",
"alternateName": "Whisker Leaks",
"url": "https://www.linkedin.com/in/mikelee89/"
},
"publisher": {
"@type": "Person",
"name": "Mike Lee",
"url": "https://go.mike-lee.me/"
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://ski.mike-lee.me/readme.html"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Is Cat Ski free?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Cat Ski is free to play, with no account, no ads, and no in-game purchases. The line on the start screen, no quarter required, is literal."
}
},
{
"@type": "Question",
"name": "Is there a version of SkiFree you can play online?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Cat Ski is a browser tribute to SkiFree. It runs in any modern browser on desktop or phone, with nothing to download. The core loop matches the 1991 original. You ski downhill, dodge trees, and keep ahead of the chaser that appears when you slow down."
}
},
{
"@type": "Question",
"name": "What is Cat Ski based on?",
"acceptedAnswer": {
"@type": "Answer",
"text": "SkiFree, written by Chris Pirih and released in 1991 as part of Microsoft Entertainment Pack 3 for Windows 3.0. Cat Ski keeps the downhill feel and the chaser, swaps the skier for a cat and the yeti for a snowman, and adds mountain themes, mid-air tricks, and a leaderboard."
}
},
{
"@type": "Question",
"name": "Does the snowman catch you, like the SkiFree yeti?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. If you slow down or ski too far, a snowman starts chasing and will catch you. In 9-lives mode you can afford a few mistakes. In 1-life classic mode, one catch ends the run."
}
},
{
"@type": "Question",
"name": "Can I install Cat Ski and play offline?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Cat Ski is a Progressive Web App. Use the INSTALL button in the game, or your browser's Add to Home Screen option, and it runs offline afterward."
}
},
{
"@type": "Question",
"name": "What are the controls?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Steer with the arrow keys or WASD, and press Space to start, pause, or restart. On a phone, use the on-screen joystick and the trick buttons."
}
}
]
}
</script>
<style>
/* Self-hosted, SIL Open Font License. See index.html. */
@font-face {
font-family: 'Press Start 2P';
src: url('fonts/press-start-2p.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: 'VT323';
src: url('fonts/vt323.woff2') format('woff2');
font-display: swap;
}
:root {
--snow: #eaf4ff;
--snow-dim: #cfe2f5;
--ink: #0b1320;
--accent: #ff3b30;
--accent-2: #ffcc00;
--void: #050910;
--bezel-hi: #24384f;
--sunken: #0a111c;
--sunken-hi: #111a27;
--deep: #1a2838;
--panel-border: #1d2a3c;
--ctl-border: #2b3c54;
--ice: #7aa7c7;
--text-dim: #798fa6;
/* Same 8px grid as the game. See index.html. */
--font-display: 'Press Start 2P', monospace;
--font-body: 'VT323', monospace;
--type-sm: 8px;
--type-md: 16px;
--type-lg: 24px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
background: var(--ink);
color: var(--snow);
font-family: var(--font-body);
}
body {
min-height: 100vh;
padding: 20px;
background:
radial-gradient(ellipse at 50% 0%, var(--bezel-hi) 0%, var(--ink) 60%, var(--void) 100%);
background-attachment: fixed;
overflow-x: hidden;
}
.container {
max-width: 740px;
margin: 0 auto;
padding: 26px 30px 30px;
background: linear-gradient(var(--sunken-hi), var(--sunken));
border-radius: 6px;
box-shadow:
0 0 0 2px var(--panel-border),
0 0 0 4px #000,
0 30px 60px rgba(0,0,0,.6);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 22px;
padding-bottom: 14px;
border-bottom: 2px solid var(--panel-border);
}
.header h1 {
font-family: var(--font-display);
font-size: var(--type-lg);
letter-spacing: 2px;
color: var(--snow);
text-shadow: 0 0 6px rgba(122,167,199,.6);
}
.header h1 .alt { color: var(--accent); }
.header h1 .label { color: var(--snow-dim); margin-left: 8px; }
.nav-back {
font-family: var(--font-display);
font-size: var(--type-sm);
letter-spacing: 2px;
color: var(--snow-dim);
text-decoration: underline;
text-underline-offset: 3px;
padding: 4px 6px;
}
.nav-back:hover { color: var(--accent-2); }
h2 {
font-family: var(--font-display);
font-size: var(--type-md);
letter-spacing: 2px;
color: var(--accent-2);
margin: 32px 0 14px;
padding-bottom: 8px;
border-bottom: 1px solid var(--panel-border);
scroll-margin-top: 16px;
}
.toc {
margin: 4px 0 24px;
padding: 14px 16px;
background: rgba(26, 40, 56, 0.5);
border: 1px solid var(--panel-border);
}
.toc ul { margin: 0; }
.toc li {
font-size: 16px;
line-height: 1.4;
margin: 4px 0;
}
.toc a {
color: var(--snow-dim);
border-bottom: 1px dotted var(--text-dim);
}
.toc a:hover { color: var(--accent-2); border-bottom-color: var(--accent-2); }
h3 {
font-family: var(--font-display);
font-size: var(--type-sm);
letter-spacing: 2px;
color: var(--snow);
margin: 22px 0 8px;
}
p {
font-size: 18px;
line-height: 1.55;
color: var(--snow-dim);
margin: 12px 0;
}
a {
color: var(--accent-2);
text-decoration: none;
border-bottom: 1px dotted var(--accent-2);
}
a:hover { color: var(--snow); border-bottom-color: var(--snow); }
ul {
list-style: none;
padding: 0;
margin: 12px 0;
}
li {
font-size: 18px;
line-height: 1.5;
color: var(--snow-dim);
padding-left: 20px;
position: relative;
margin: 8px 0;
}
li::before {
content: '\25B8';
position: absolute;
left: 0;
color: var(--accent-2);
}
strong { color: var(--snow); }
code {
font-family: var(--font-body);
font-size: 0.95em;
background: var(--deep);
padding: 1px 5px;
border: 1px solid var(--ctl-border);
color: var(--accent-2);
}
ol.numbered {
list-style: decimal outside;
padding-left: 28px;
margin: 12px 0;
}
ol.numbered li {
font-size: 18px;
line-height: 1.55;
color: var(--snow-dim);
padding-left: 4px;
position: static;
margin: 10px 0;
}
ol.numbered li::before { content: ''; }
ol.numbered li::marker { color: var(--accent-2); font-family: var(--font-display); font-size: var(--type-sm); }
table {
width: 100%;
border-collapse: collapse;
margin: 14px 0;
font-size: 16px;
color: var(--snow-dim);
}
th, td {
text-align: left;
padding: 8px 10px;
border-bottom: 1px solid var(--panel-border);
vertical-align: top;
line-height: 1.4;
}
th {
font-family: var(--font-display);
font-size: var(--type-sm);
letter-spacing: 1px;
color: var(--accent-2);
border-bottom: 2px solid var(--ctl-border);
}
td:first-child { color: var(--snow); }
.footer {
margin-top: 32px;
padding-top: 16px;
border-top: 2px solid var(--panel-border);
text-align: center;
font-size: 14px;
color: var(--text-dim);
letter-spacing: 1px;
}
.footer .blink { color: var(--accent-2); animation: blink 1.2s steps(2,end) infinite; }
@keyframes blink { 50% { opacity: 0; } }
/* Cookie consent banner — same component as index.html; styled to
match the readme's monospace body type. */
.cookie-banner {
position: fixed;
bottom: 0; left: 0; right: 0;
z-index: 9999;
background: var(--void);
border-top: 2px solid var(--panel-border);
padding: 12px 16px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 12px;
font-family: var(--font-body);
font-size: 16px;
color: var(--snow-dim);
box-shadow: 0 -4px 16px rgba(0,0,0,0.5);
}
.cookie-banner[hidden] { display: none; }
.cookie-banner .cb-text { flex: 1 1 280px; min-width: 0; }
.cookie-banner .cb-text a { color: var(--accent-2); text-decoration: underline; }
.cookie-banner .cb-btn {
font-family: var(--font-display);
font-size: var(--type-sm);
letter-spacing: 1px;
padding: 8px 12px;
background: transparent;
color: var(--snow-dim);
border: 2px solid var(--ice);
cursor: pointer;
}
.cookie-banner .cb-btn:hover { color: var(--accent-2); border-color: var(--accent-2); }
.cookie-banner .cb-btn.cb-accept {
background: var(--accent-2);
color: var(--ink);
border-color: var(--accent-2);
box-shadow: 2px 2px 0 #000;
}
.cookie-banner .cb-btn.cb-accept:hover { color: var(--ink); }
.cookie-banner .cb-btn:active { transform: translate(1px, 1px); }
@media (max-width: 600px) {
body { padding: 10px; }
.container { padding: 18px 16px 22px; }
.header h1 { font-size: 11px; letter-spacing: 1px; }
.header h1 .label { display: block; margin-left: 0; margin-top: 4px; font-size: 9px; }
.nav-back { font-size: 8px; }
h2 { font-size: 11px; }
h3 { font-size: 9px; }
p, li { font-size: 16px; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><span class="alt">CAT</span> SKI<span class="label">// README</span></h1>
<a href="/" class="nav-back">< PLAY</a>
</div>
<nav class="toc" aria-label="Contents">
<ul>
<li><a href="#tldr">How to play (quick start)</a></li>
<li><a href="#about">About cat-ski</a></li>
<li><a href="#play">Gameplay details</a></li>
<li><a href="#build">How the game is built</a></li>
<li><a href="#design">Design criteria for each game element</a></li>
<li><a href="#colors">Selecting colors from real photographs</a></li>
<li><a href="#claude">Working with Claude as a design collaborator</a></li>
<li><a href="#diy">How you can build this yourself</a></li>
<li><a href="#privacy">Privacy notes</a></li>
<li><a href="#faq">Common questions</a></li>
<li><a href="#resources">Further resources</a></li>
</ul>
</nav>
<h2 id="tldr">How to play (quick start)</h2>
<p>Ski downhill, dodge trees, land tricks for bonus points, and try to outlast the dog chasers and the snowman that ends every run. By default you have nine lives; toggle the <strong>9</strong> button to play 1-life classic. Pick a cat color and a mountain theme, then press <strong>SPACE</strong> to start.</p>
<table>
<thead>
<tr><th>Input</th><th>Action</th></tr>
</thead>
<tbody>
<tr><td>Arrow keys or WASD</td><td>Steer left and right</td></tr>
<tr><td>Down arrow or S</td><td>Tuck for higher speed</td></tr>
<tr><td>Up arrow or W</td><td>Carve for lower speed</td></tr>
<tr><td>1, 2, 3 (in mid-air)</td><td>Trick: Meow Roll, Whisker Twist, Cat Nap</td></tr>
<tr><td>Space</td><td>Start, retry, or resume</td></tr>
<tr><td>P or Esc</td><td>Pause</td></tr>
<tr><td>R</td><td>Restart the current run</td></tr>
</tbody>
</table>
<p>On phones and tablets, an on-screen joystick replaces the keyboard. The trick buttons sit alongside the joystick and a SWAP button lets you put either control on whichever side fits your thumb.</p>
<h2 id="about">About cat-ski</h2>
<p>Cat Ski 1989 is a free browser game inspired by <a href="https://en.wikipedia.org/wiki/SkiFree" target="_blank" rel="noopener noreferrer">SkiFree</a>, the 1991 release that shipped in Microsoft Entertainment Pack 3 for Windows 3.0. The original presented an endless downhill ski run with trees, jumps, and an unkillable abominable yeti that eventually appeared and ended every run. Cat Ski 1989 recreates the same loop with a cat as the player character. The game includes three regional mountain themes (Alps, Rockies, Himalayas), four cat color variants, an in-air trick system with chained scoring, gold escape ramps that spawn alongside each dog wave, and a snowman that takes the role of the original yeti. Difficulty is tunable through two sliders, with four named presets for casual players.</p>
<p>The game ships with a 9-lives casual mode enabled by default. Each crash, dog grab, or snowman chomp consumes a life, with a brief invincibility window before the next hit registers. A toggle on the picker row drops the cat back to 1-life classic for players who want the original SkiFree edge. A top-3 leaderboard sits at the top of the page; it splits into separate boards for 9-lives and 1-life so the two modes stay apples-to-apples. Scores are kept in the browser, so each player builds their own board. Entering a name is optional and only happens after a qualifying run. The UI is bilingual (English and rioplatense Spanish) via a slider on the controls row.</p>
<p>The project is open source at <a href="https://github.com/ashtonmorrow/cat-ski" target="_blank" rel="noopener noreferrer">github.com/ashtonmorrow/cat-ski</a>. It was developed by <a href="https://www.linkedin.com/in/mikelee89/" target="_blank" rel="noopener noreferrer">Mike Lee</a> using Claude as a coding collaborator. The full game is one self-contained HTML file with no build step, framework, or backend. The two fonts are self-hosted. It is installable as a Progressive Web App for offline play. Best scores and the leaderboard both persist in the browser, so the game runs with no network at all after the first load.</p>
<h2 id="play">Gameplay details</h2>
<p>Before starting, the player chooses a cat color (black, tabby, calico, or orange) and a mountain theme (Alps, Rockies, or Himalayas). Both selections persist across sessions through localStorage.</p>
<p>The score increases with distance traveled, plus bonuses for collected flags, completed jumps, and chained tricks. Each subsequent trick within a single jump compounds the bonus, so chaining several tricks before landing produces a higher reward than performing them across separate jumps.</p>
<p>After a fixed time, dogs begin to appear at the top of the screen and chase the cat down the slope. Each dog crosses the cat's screen position at a calculable moment determined by its descent rate. The player can dodge horizontally before the crossing, or launch from a gold escape ramp to be airborne while the dog passes. After the configured number of dog waves are cleared, a snowman appears within a randomized window and ends the run on contact. The snowman is the project's tribute to the original SkiFree yeti.</p>
<p>Difficulty is set with two sliders. Intensity (0.8x to 5x) scales speed, obstacle density, spawn cadence, and dog tracking. Dog count (1 to 10) sets how many dog waves appear before the snowman. Four named presets (EASY, MEDIUM, HARD, and INSTA DEATH) snap both sliders to canonical positions for players who do not want to tune individual parameters.</p>
<p>Lives mode (the <strong>9</strong> button on the picker row) gives the cat nine lives instead of one. Each crash, dog grab, or snowman chomp consumes a life with a brief invincibility window before the next hit can register. The leaderboard splits by mode, so 9-lives runs and 1-life runs are scored on separate boards.</p>
<h2 id="build">How the game is built</h2>
<p>The game is contained in a single HTML file of approximately 140 KB, with a separate service worker for offline support and a manifest file for PWA installability. There is no build step, framework, or third-party runtime dependency. The two fonts are served from the same origin. The constraint of keeping the game in one file is partly a tribute to the original SkiFree, which fit in 60 KB on a 3.5-inch floppy disk. The practical benefit is that every change is deployable in under a minute. A push to the GitHub repository triggers an automatic Vercel rebuild, and the new version is served from the CDN with no build pipeline to wait for.</p>
<table>
<thead>
<tr><th>Component</th><th>Implementation</th></tr>
</thead>
<tbody>
<tr><td>Rendering</td><td>HTML5 Canvas at 360x270 internal resolution, scaled to viewport with image-rendering: pixelated</td></tr>
<tr><td>Game loop</td><td>Vanilla JavaScript, requestAnimationFrame</td></tr>
<tr><td>Audio</td><td>Web Audio API, all sounds synthesized at runtime via OscillatorNode and filtered BufferSource noise</td></tr>
<tr><td>Sprites</td><td>Procedural drawing with ctx.fillRect calls; no image assets at runtime</td></tr>
<tr><td>State persistence</td><td>localStorage for cat color, theme, difficulty, and best score</td></tr>
<tr><td>Hosting</td><td>Vercel, deployed from the GitHub repository on push</td></tr>
<tr><td>PWA</td><td>Single service worker, app shell precache, network-first for HTML</td></tr>
</tbody>
</table>
<h2 id="design">Design criteria for each game element</h2>
<p>Each system in the game started with a written criteria question before any code was written. The cat sprite needed to read as a cat at 16 pixels wide and support four color variants without forking the drawing code. The solution was a procedural drawing function parameterized by a palette object, which produces all four cat colors from a single set of geometry.</p>
<p>The dog chase needed to be escapable through skill but not through extended tucking. Dogs in the game move down the screen at a constant rate relative to the camera, so the moment they cross the cat's screen position is mathematically predictable. The player has one timing window per dog to either dodge horizontally or launch from a bonus ramp. The bonus ramps spawn at distances calculated from the player's current speed and the dog's descent rate, so the airborne window aligns with the crossing moment regardless of whether the player is carving slowly or tucking fast.</p>
<p>Difficulty needed to satisfy both casual players who want presets and players who want to fine-tune individual parameters. The solution is two continuous sliders (intensity 0.8x to 5x, dog count 1 to 10) paired with four preset buttons that snap the sliders to canonical positions. New visitors land on EASY by default, which matches the casual experience. Players who want more challenge use the sliders or pick HARD or INSTA DEATH.</p>
<h2 id="colors">Selecting colors from real photographs</h2>
<p>The first version of the three mountain themes used generic palettes: a pinky-cream Rockies, a default cool-blue Alps, and a default-cool Himalayas. The result felt synthetic. To make each region read as the actual mountain range, the palettes were rebuilt from descriptions of how each range typically photographs.</p>
<table>
<thead>
<tr><th>Range</th><th>Description used as input</th><th>Palette character</th></tr>
</thead>
<tbody>
<tr>
<td>Alps</td>
<td>Cool crisp blue snow, deep emerald Norway spruce, granite-gray peaks fading blue with atmospheric haze</td>
<td>Cool-saturated, classic Swiss alpine</td>
</tr>
<tr>
<td>Rockies</td>
<td>Warm afternoon light on sandstone and exposed granite, slightly bluer-green lodgepole pine, rust-toned distant peaks</td>
<td>Warm earth tones, golden-hour reading</td>
</tr>
<tr>
<td>Himalayas</td>
<td>Deep saturated alpine sky, brilliant white snow, near-black slate rock, sparse dusty juniper at low elevations</td>
<td>High contrast, austere, dramatic</td>
</tr>
</tbody>
</table>
<p>The same description-first approach extended to the obstacles. Norway spruce silhouettes were drawn for the Alps, lodgepole pine (taller and narrower, with a visible trunk between branch tiers) for the Rockies, and stunted krummholz juniper (short and gnarled) for the Himalayas. The Rockies use aspen logs with white bark and black eye scars, since aspen is widely associated with the American Rocky Mountain landscape. The Himalayas use granite boulders rather than logs, because trees do not occur above the tree line where the game is conceptually set. Each species was researched, the silhouette was described in plain language, and Claude produced approximately 30 lines of fillRect calls per variant.</p>
<h2 id="claude">Working with Claude as a design collaborator</h2>
<p>The project was built with Claude functioning as a design collaborator rather than purely a code-writing assistant. The pattern that worked across the build was three questions asked before any new feature.</p>
<ol class="numbered">
<li>What is the criteria for this feature? What does it need to do, look like, and feel like?</li>
<li>What is the simplest implementation that satisfies the criteria? This question often surfaced parts of the spec that did not need to be built.</li>
<li>What are the trade-offs in the chosen approach? Procedural sprites avoid an asset pipeline but slow visual iteration. Web Audio synthesis avoids audio files but produces synthetic-sounding output. Knowing the trade-off makes the decision deliberate.</li>
</ol>
<p>For tasks involving aesthetic judgment, the prompt approach mattered. Asking Claude to produce a Rockies palette returned generic results. Describing the visual character of the source ("warm afternoon light on sandstone, slightly bluer-green pine, rust-toned distant peaks") returned palettes that read as the place. The same approach worked for sounds. Describing the intent ("a dog double-bark for a dramatic entrance"; "a trick chain that ascends in pitch as the chain grows") produced OscillatorNode envelopes that matched the description.</p>
<p>The constraint that made the iteration loop effective was the no-build deployment. Each change reached the live site in under a minute, which made the cycle of pushing, observing, and refining fast enough that the production environment doubled as the development environment. Across approximately 50 commits, this cadence affected the final quality more than any individual technical choice.</p>
<h2 id="diy">How you can build this yourself</h2>
<p>The full toolchain for a project like this is free and small enough to learn in an afternoon. You need a code editor, a browser, a Git client, a GitHub account, and a Vercel account. Claude is the writing tool; you can use the web interface at <a href="https://claude.ai" target="_blank" rel="noopener noreferrer">claude.ai</a> or the Claude Code CLI from a terminal. No frameworks, build systems, or asset pipelines are required for a single-file game.</p>
<p>The workflow starts in a Claude conversation. Describe what you want to build in plain language, including the visual character, gameplay mechanics, audience, and any constraints such as a single-file output or a maximum file size. Claude produces a starting scaffold. From there, the iteration loop is short: describe a change, get the updated code, paste it into the editor, refresh the browser, and decide what to refine. Commit each working change to Git. Push to a GitHub repository connected to a Vercel project, and the new version is live within a minute. To add a custom subdomain, create a CNAME record at your DNS provider that points the subdomain to cname.vercel-dns.com, then assign the domain in the Vercel project settings.</p>
<p>Three practices kept the build moving. First, treat the conversation with Claude as design work rather than code generation. State the criteria, propose the simplest approach, and identify the trade-offs before any feature is built. Second, deploy frequently. Ship each working commit so the live site stays current with the project. Third, when working on aesthetic decisions, describe the source material in concrete terms. Asking Claude for a palette inspired by "warm afternoon light on sandstone" produces hex codes that read as the place; asking for a palette that feels "rugged" returns generic results. The same pattern holds for sounds, sprites, and animation timing: the more specific the input, the more usable the output.</p>
<h2 id="privacy">Privacy notes</h2>
<p>Cat Ski 1989 is a free, single-page browser game with no accounts, no signups, and no email collection. The data the site does touch is described in full below so a player can decide what to consent to.</p>
<h3>Anonymous analytics</h3>
<p>The site loads Google Analytics 4 to count plays and surface basic usage patterns (page views, country-level location, device type). GA is configured with Consent Mode v2 and defaults to denied, which means no GA cookies are written and no data is sent until the visitor accepts the cookie banner. Declining the banner leaves analytics off for that browser; the decision is stored in localStorage and respected on subsequent visits. GA does not collect names, email addresses, IP addresses (Google anonymizes the last octet by default), or any personally identifying information.</p>
<h3>Local game state</h3>
<p>The game writes the following to the browser's localStorage on the device it runs on. None of it leaves the device:</p>
<ul>
<li>Selected cat color and mountain theme</li>
<li>Difficulty slider positions and lives-mode preference</li>
<li>Audio on/off, control mode (arrows or WASD), touch mode</li>
<li>Language preference (English or Spanish)</li>
<li>Best score per intensity / dog count / lives-mode combination</li>
<li>The top-3 leaderboard for each mode (this is the only place scores are stored)</li>
<li>The cookie consent decision</li>
<li>The most recent name entered for the leaderboard</li>
</ul>
<p>All localStorage entries are prefixed with <code>skior_</code>. Clearing browser storage for this site removes them entirely.</p>
<h3>Leaderboard</h3>
<p>The leaderboard is local to the browser. When a player enters a name after a qualifying run, that name and score are written to localStorage and go nowhere else. Nothing is uploaded, no database receives the run, and one player's board is never visible to another. Clearing browser storage for this site erases the board, and the strip returns to its starting three scores.</p>
<h3>Third parties</h3>
<p>The site is hosted on Vercel, which receives standard HTTP request metadata (URL, user agent, referrer, IP address) for the purposes of serving the page and protecting against abuse. Vercel's privacy policy applies to that traffic. Fonts are self-hosted, so loading the page makes no font request to any third party. There is no game database and no backend. No advertising networks, social trackers, or fingerprinting libraries are loaded.</p>
<h3>Children's privacy</h3>
<p>The game is appropriate for all ages and does not knowingly collect personal information from anyone, including children under 13.</p>
<h3>Changes</h3>
<p>If this notice changes in a substantive way, the date next to <strong>Last updated</strong> below will move forward. Last updated: 26 April 2026.</p>
<h2 id="faq">Common questions</h2>
<h3>Is Cat Ski free?</h3>
<p>Yes. Cat Ski is free to play, with no account, no ads, and no in-game purchases. The line on the start screen, no quarter required, is literal.</p>
<h3>Is there a version of SkiFree you can play online?</h3>
<p>Cat Ski is a browser tribute to <a href="https://en.wikipedia.org/wiki/SkiFree" target="_blank" rel="noopener noreferrer">SkiFree</a>. It runs in any modern browser on desktop or phone, with nothing to download. The core loop matches the 1991 original. You ski downhill, dodge trees, and keep ahead of the chaser that appears when you slow down.</p>
<h3>What is Cat Ski based on?</h3>
<p>SkiFree, written by Chris Pirih and released in 1991 as part of Microsoft Entertainment Pack 3 for Windows 3.0. Cat Ski keeps the downhill feel and the chaser, swaps the skier for a cat and the yeti for a snowman, and adds mountain themes, mid-air tricks, and a leaderboard.</p>
<h3>Does the snowman catch you, like the SkiFree yeti?</h3>
<p>Yes. If you slow down or ski too far, a snowman starts chasing and will catch you. In 9-lives mode you can afford a few mistakes. In 1-life classic mode, one catch ends the run.</p>
<h3>Can I install Cat Ski and play offline?</h3>
<p>Yes. Cat Ski is a Progressive Web App. Use the INSTALL button in the game, or your browser's Add to Home Screen option, and it runs offline afterward.</p>
<h3>What are the controls?</h3>
<p>Steer with the arrow keys or WASD, and press Space to start, pause, or restart. On a phone, use the on-screen joystick and the trick buttons.</p>
<h2 id="resources">Further resources</h2>
<ul>
<li><a href="https://en.wikipedia.org/wiki/SkiFree" target="_blank" rel="noopener noreferrer">SkiFree on Wikipedia</a>: background on the original 1991 game.</li>
<li><a href="https://github.com/ashtonmorrow/cat-ski" target="_blank" rel="noopener noreferrer">github.com/ashtonmorrow/cat-ski</a>: full source code, issues, and pull requests.</li>
<li><a href="https://www.linkedin.com/in/mikelee89/" target="_blank" rel="noopener noreferrer">Mike Lee on LinkedIn</a>: contact for the project author.</li>
</ul>
<div class="footer">
<span class="blink">▸</span> <a href="/">PLAY</a> • Made by <a href="https://www.linkedin.com/in/mikelee89/" target="_blank" rel="noopener noreferrer">Whisker Leaks</a>
</div>
</div>
<!-- Cookie consent banner (mirrors index.html). Both pages share the
'skior_cookies' localStorage key so a decision on either page
persists to the other. -->
<div id="cookie-banner" class="cookie-banner" hidden role="dialog" aria-label="Cookie consent">
<span class="cb-text">
This site uses anonymous analytics cookies to help improve the game.
See the <a href="#privacy">privacy notes</a> below for details.
</span>
<button class="cb-btn cb-accept" id="cookie-accept" type="button">OK</button>
<button class="cb-btn cb-decline" id="cookie-decline" type="button">NO THANKS</button>
</div>
<script>
(function() {
var KEY = 'skior_cookies';
var saved = null;
try { saved = localStorage.getItem(KEY); } catch (_) {}
var banner = document.getElementById('cookie-banner');
if (!banner) return;
if (saved !== 'accepted' && saved !== 'declined') banner.hidden = false;
function close(decision) {
try { localStorage.setItem(KEY, decision); } catch (_) {}
banner.hidden = true;
if (decision === 'accepted' && typeof gtag === 'function') {
gtag('consent', 'update', { 'analytics_storage': 'granted' });
}
}
var ok = document.getElementById('cookie-accept');
var no = document.getElementById('cookie-decline');
if (ok) ok.addEventListener('click', function() { close('accepted'); });
if (no) no.addEventListener('click', function() { close('declined'); });
})();
</script>
</body>
</html>