From WhatsApp to the Play Store: How I built a game from scratch using only AI and code

As a web developer, my world has always been that of CMS, ERPs and SaaS. I always saw video games as something foreign, a complex terrain full of heavy engines like Unity or Unreal that I never wanted to touch. But what happens when a nephew's curiosity and current AI tools intersect in a WhatsApp chat?

A promise and a WhatsApp chat.

My nephew always asked me: “¿Cuándo vas a sacar tu propio juego?”. Among my home and work responsibilities, time was our greatest enemy. We couldn't physically meet, but we could chat. That's how it was born CIRCLE: a project built in the spare time, between voice messages and code snippets.

My Stack: Vibe Coding with AI

I didn't seek to be a GameDev expert, I sought to be a Project Manager of my own code. I used Gemini to advise me on architecture and technical decisions, and Cursor para “vibe codear” toda la lógica en React Native. Fue una experiencia fascinante: yo definía la estrategia y la IA ejecutaba, mientras yo supervisaba la calidad y la estructura.

Local to Global

The magic of Firebase At first, the game was going to be a simple local pastime. But when I heard my nephew say, “Por fin seré un top global”, I understood that competition was the lifeblood of the project.

To make the ranking real, I integrated Cloud Firestore in your free plan. Now, each game is saved in the cloud, allowing a global experience, in real time and without its own server. We use anonymous authentication so no one wastes time signing up.

The result? Aún estamos puliendo detalles, pero el juego ya es funcional y divertido. Ha sido una lección de que hoy, con las herramientas correctas, la barrera entre “ser desarrollador de apps” y “crear un juego” es más delgada que nunca.

Do you want to try it? Here's the APK so you can be one of the first to try CIRCLE. I look forward to your feedback before officially launching it on the Play Store!

CIRCLE

Real-time chat with Socket.IO, Redis and Mongo DB

This project is a proof of concept of a real-time chat system with support for multiple rooms, message history and notifications. It is developed with technologies designed to scale in a microservices-based architecture.

Technologies Applied

  • Node.js + Express: HTTP server and route management.
  • Socket.IO: Real-time communication with WebSockets.
  • MongoDB: Persistence of messages and rooms.
  • Redis: Pub/sub system to distribute messages between multiple server instances.
  • HTML + JS: Cliente web simple.
  • PrimeFlex (opcional): Visual enhancement of the interface.

Why Redis

Redis is used to enable the posting and subscription of messages between multiple processes or server instances. This is necessary when the server is running in a distributed or balanced environment, as Socket.IO alone does not share information between instances.

Using Redis ensures that all connected users receive messages even if they are served by different processes or servers.

Project Structure: chat_0.1

This project has an organized architecture for developing a chat application using Node.js with TypeScript and Redis. The main directories and files are described below:

Directorios

  • .git/
    Git repository configuration folder. Contains version history and version control settings.
  • node_modules/
    Directory automatically generated by npm, contains all project dependencies.
  • src/
    Main folder of the application source code.
    • public/
      Directory optionally used for public files (such as HTML, CSS or JS accessible directly).
    • index.html
      This file is the main page displayed when opening the application in the browser. It serves as a visual entry point and typically contains the basic design of the interface, in addition to the scripts needed for the application to work.
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8" />
  <title>Chat en Tiempo Real</title>
  <link rel="stylesheet" href="https://unpkg.com/primeflex@3.3.1/primeflex.min.css">
  <link rel="stylesheet" href="https://unpkg.com/primeicons/primeicons.css">
  <style>
    body {
      font-family: sans-serif;
      padding: 2rem;
    }
    #chatBox {
      border: 1px solid #ccc;
      height: 250px;
      overflow-y: auto;
      padding: 1rem;
      background: #f4f4f4;
      border-radius: 6px;
    }
    #chatBox p {
      margin: 0.5rem 0;
    }
  </style>
</head>
<body>
  <h2 class="mb-3">💬 Chat en Tiempo Real</h2>

  <div class="flex flex-column gap-2 mb-3">
    <input id="username" class="p-inputtext p-component" placeholder="Tu nombre" />
    <input id="roomName" class="p-inputtext p-component" placeholder="Nombre de la sala" />
    <button class="p-button p-component" onclick="joinRoom()">Entrar</button>
  </div>

  <h3>Salas disponibles:</h3>
  <ul id="roomList" class="mb-3"></ul>

  <div id="chat" style="display:none;">
    <h3>Chat en Sala</h3>
    <div id="chatBox" class="mb-2"></div>
    <div class="flex gap-2">
      <input id="inputMsg" class="p-inputtext p-component flex-1" placeholder="Mensaje..." />
      <button class="p-button p-component" onclick="sendMessage()">Enviar</button>
    </div>
  </div>

  <audio id="notifSound" src="https://notificationsounds.com/storage/sounds/file-sounds-1154-pristine.mp3" preload="auto"></audio>

  <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
  <script>
    const socket = io();
    let currentRoom = '';

    // Pedir permiso para notificaciones
    if (Notification.permission !== 'granted') {
      Notification.requestPermission();
    }

    async function joinRoom() {
      const room = document.getElementById('roomName').value;
      const username = document.getElementById('username').value;
      if (!room || !username) return alert('Ingresa nombre y sala');

      currentRoom = room;
      document.getElementById('chat').style.display = 'block';
      clearMessages();

      socket.emit('joinRoom', room, username);

      socket.off('chatMessage');
      socket.on('chatMessage', (data) => {
        addMessage(`${data.user}: ${data.message}`);
        playNotificationSound();
        showNotification(data.user, data.message);
      });

      socket.off('notification');
      socket.on('notification', (msg) => {
        addMessage(`🔔 ${msg}`);
      });

      socket.off('chat-history');
      socket.on('chat-history', (messages) => {
        messages.forEach(msg => {
          addMessage(`${msg.user}: ${msg.message}`);
        });
      });

      addMessage(`🟢 Te uniste a la sala: ${room}`);
    }

    function sendMessage() {
      const message = document.getElementById('inputMsg').value;
      const user = document.getElementById('username').value;
      if (!message.trim()) return;
      socket.emit('chatMessage', { room: currentRoom, user, message });
      document.getElementById('inputMsg').value = '';
    }

    function addMessage(msg) {
      const box = document.getElementById('chatBox');
      const p = document.createElement('p');
      p.textContent = msg;
      box.appendChild(p);
      box.scrollTop = box.scrollHeight;
    }

    function clearMessages() {
      document.getElementById('chatBox').innerHTML = '';
    }

    async function fetchRooms() {
      try {
        const res = await fetch('/rooms');
        const rooms = await res.json();
        const list = document.getElementById('roomList');
        list.innerHTML = '';
        rooms.forEach(r => {
          const li = document.createElement('li');
          li.textContent = r.name;
          list.appendChild(li);
        });
      } catch (err) {
        console.error('Error al cargar salas:', err);
      }
    }

    function showNotification(user, message) {
      if (Notification.permission === 'granted') {
        new Notification(`💬 ${user}`, {
          body: message,
          icon: 'https://cdn-icons-png.flaticon.com/512/2331/2331942.png'
        });
      }
    }

    function playNotificationSound() {
      const sound = document.getElementById('notifSound');
      sound.currentTime = 0;
      sound.play().catch(() => {});
    }

    fetchRooms();
  </script>
</body>
</html>
  • db.ts
    Configuration file and connection to the database (possibly MongoDB, PostgreSQL or other).
import mongoose from 'mongoose';

await mongoose.connect('mongodb://localhost:27017/chat-app');

const messageSchema = new mongoose.Schema({
  room: String,
  user: String,
  message: String,
  timestamp: { type: Date, default: Date.now }
});

const roomSchema = new mongoose.Schema({
  name: { type: String, unique: true }
});

export const Message = mongoose.model('Message', messageSchema);
export const Room = mongoose.model('Room', roomSchema);
  • index.ts
    Main entry point of the application. It is where the server or system core is initialized and booted.
import express from 'express';
import http from 'http';
import { Server } from 'socket.io';
import { pub, sub } from './redis.js';
import { Message, Room } from './db.js';
import path from 'path';
import { fileURLToPath } from 'url';

const app = express();
const server = http.createServer(app);
const io = new Server(server);
const __dirname = path.dirname(fileURLToPath(import.meta.url));

app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());

// Listar salas existentes
app.get('/rooms', async (_req, res) => {
  const rooms = await Room.find().select('name -_id');
  res.json(rooms);
});

io.on('connection', (socket) => {
  console.log('Cliente conectado');

  socket.on('joinRoom', async (room, username) => {
    socket.join(room);
    // Enviar historial al nuevo usuario
    const history = await Message.find({ room }).sort({ timestamp: 1 }).lean();
    socket.emit('chat-history', history);

    // Crear sala si no existe
    const exists = await Room.findOne({ name: room });
    if (!exists) {
      await new Room({ name: room }).save();
    }

    socket.to(room).emit('notification', `${username} se unió al chat`);
  });

  socket.on('chatMessage', async (data) => {
    const { room, user, message } = data;
    const chatMessage = { room, user, message };

    await pub.publish('chat', JSON.stringify(chatMessage));
    await new Message(chatMessage).save();
  });
});

await sub.subscribe('chat', (msg) => {
  const parsed = JSON.parse(msg);
  io.to(parsed.room).emit('chatMessage', parsed);
});

server.listen(3000,'0.0.0.0', () => {
  console.log('Servidor en http://localhost:3000');
});
  • redis.ts
    Configuration and connection logic with Redis, probably used for caching or real-time session/chat management.
import { createClient } from 'redis';

export const pub = createClient();
export const sub = createClient();

await pub.connect();
await sub.connect();
  • types.ts
    File that defines custom TypeScript types and structures to keep typing strong and organized.
export interface ChatMessage {
  room: string;
  user: string;
  message: string;
}

export interface ChatRoom {
  name: string;
}

Configuration files

  • .gitignore
    List of files and folders that Git should ignore (for example: node_modules, logs, claves, etc.).
  • docker-compose.yml
    Configuration file for Docker Compose. Defines the services and containers required to run the application in a container environment (such as Node, Redis, etc.).
  • package.json
    Main configuration file of the Node.js project. Contains project information, scripts, and dependencies.
  • package-lock.json
    Automatically generated file to lock down exact versions of dependencies, ensuring consistent environments.
  • README.md
    Project documentation file. It usually contains instructions for installation, use and purpose of the system.
  • tsconfig.json
    Configuration of the TypeScript compiler. Defines how the project should be compiled (paths, target, module, etc.).

Funcionalidades

  • Create or join rooms from the frontend
  • Listar salas disponibles
  • Sending and receiving messages in real time
  • Receive message history when entering a room
  • MongoDB Message Persistence
  • Notifications within the chat
  • Local network exposure using 0.0.0.0

Use source code:

GITHUB

Create a directory tree in Typescript projects easily with AI

A veces necesitamos crear de manera rápida la estructura de directorios o carpetas para un proyecto de Angular, Vue o cualquier otro tipo, ya sea de backend o frontend. En este laboratorio usaremos la IA de Chat GPT y la consola de PowerShell para hacer esto de manera más automatizada.

Para comenzar, enviamos el siguiente Promp a chat gpt:
crea un comando para powershell que haga este arbol de directorios:

src/app/
├── core/
├── shared/
├── features/
│ ├── auth/
│ ├── boards/
├── store/ (NgRx)

A lo cual (en mi caso) responde lo siguiente:

Aquí tienes un comando de PowerShell que crea el árbol de directorios especificado:

# Ruta base
$basePath = "src/app"

# Lista de directorios a crear
$directories = @(
    "$basePath/core",
    "$basePath/shared",
    "$basePath/features",
    "$basePath/features/auth",
    "$basePath/features/boards",
    "$basePath/store" # NgRx
)

# Crear los directorios
foreach ($dir in $directories) {
    New-Item -ItemType Directory -Path $dir -Force | Out-Null
}

Write-Host "Árbol de directorios creado exitosamente en $basePath"

Cómo usarlo:

  1. Abre PowerShell.
  2. Navega al directorio donde deseas crear el árbol (por ejemplo, el directorio raíz de tu proyecto).
  3. Copia y pega el script en PowerShell y presiona Enter.

Este script creará el árbol de directorios especificado bajo la carpeta src/app. Si alguna carpeta ya existe, no se duplicará.

Realizamos los pasos dichos, y efectivamente se crea el árbol de directorios.

¡Mensaje enviado!