DEV Community

Cover image for Bulls & Cows Goes Offline — How I Turned My Game Into a PWA (and Skipped the $100 App Store Fee)
Cristina Rodriguez
Cristina Rodriguez

Posted on

Bulls & Cows Goes Offline — How I Turned My Game Into a PWA (and Skipped the $100 App Store Fee)

Remember my Bulls & Cows game? The one with no frameworks, no AI, just vanilla JS? Well, I made it installable. Like, "put it on your home screen and play offline" installable.

No App Store. No Google Play. No $99/year Apple Developer fee. No $25 Google registration. Just... a web app that acts like a native app.

And apparently, people like it? 1.3K unique players, 1.4K sessions, and 1.8K pageviews — with people spending almost 2 minutes per visit just thinking through a logic puzzle. That's enough traction that I gave it its own home.

🎮 Play it: picasyfijas.com

✨ What's a PWA, Really?

A Progressive Web App is just a regular website with two extra files that tell the browser: "Hey, I can work offline. Let me live on the home screen."

That's it. Two files. Let me show you.

1️⃣ The Manifest (Your App's ID Card)

{
  "name": "Bulls & Cows - Picas y Fijas",
  "short_name": "Bulls & Cows",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#EBEBD3",
  "theme_color": "#4281A4",
  "icons": [
    { "src": "icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "icon-512.png", "sizes": "512x512", "type": "image/png" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This tells the browser what to call your app, what icon to use, and that standalone means "hide the browser chrome and look like a real app."

Link it in your HTML:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}

Enter fullscreen mode Exit fullscreen mode

2️⃣ The Service Worker (Your Offline Brain)

const CACHE_NAME = 'Bulls-and-Cows-v4';
const FILES_TO_CACHE = [
  '/', '/index.html', '/styles.css', '/script.js',
  '/manifest.json', '/icon-192.png', '/icon-512.png'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(FILES_TO_CACHE))
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => response || fetch(event.request))
  );
});
Enter fullscreen mode Exit fullscreen mode

That's the entire service worker. It caches your files on install, then serves them from cache when offline. No npm packages. No build step.

Register it in your HTML:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}
Enter fullscreen mode Exit fullscreen mode

3️⃣ The Install Prompt

Browsers show an install button automatically, but I added my own banner because I wanted control over the UX:

let deferredPrompt = null;

window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  deferredPrompt = e;
  document.getElementById('install-banner').style.display = 'flex';
});

document.getElementById('install-btn').addEventListener('click', () => {
  deferredPrompt.prompt();
});
Enter fullscreen mode Exit fullscreen mode

💰 Why Not the App Stores?

Apple charges $99/year. Google charges $25 once. Both require review processes, screenshots, privacy policies, and jumping through hoops.

My game collects zero data. No accounts. No ads. Just privacy-friendly Plausible for basic stats. It's literally just a logic puzzle.

PWAs let me skip all of that. Users get an installable app. I keep my $124. Everyone wins.

🎯 The Result

  • ✅ Works offline
  • ✅ Installable on Android, iOS, and desktop
  • ✅ No app store fees
  • ✅ Updates instantly (no waiting for review)
  • ✅ Still just HTML, CSS, and vanilla JS

Total new code: ~50 lines.


👉 Play it: picasyfijas.com

👉 Source: github.com/Yosolita1978/bullsandcows

Have you turned a side project into a PWA? I'd love to hear about it 👇

Top comments (0)