De WhatsApp a la Play Store: Cómo creé un juego desde cero usando solo IA y código

Como desarrollador web, mi mundo siempre ha sido el de los CMS, ERPs y SaaS. Los videojuegos siempre los vi como algo ajeno, un terreno complejo lleno de motores pesados como Unity o Unreal que nunca quise tocar. Pero, ¿qué pasa cuando la curiosidad de un sobrino y las herramientas de IA actuales se cruzan en un chat de WhatsApp?

Una promesa y un chat de WhatsApp.

Mi sobrino siempre me preguntaba: «¿Cuándo vas a sacar tu propio juego?». Entre mis responsabilidades de hogar y trabajo, el tiempo era nuestro mayor enemigo. No podíamos reunirnos físicamente, pero sí podíamos chatear. Así nació CIRCLE: un proyecto construido en los ratos libres, entre mensajes de voz y fragmentos de código.

Mi stack: Vibe Coding con IA

No busqué ser un experto en GameDev, busqué ser un Project Manager de mi propio código. Utilicé Gemini para asesorarme en arquitectura y decisiones técnicas, y 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.

De lo local a lo global

La magia de Firebase Al principio, el juego iba a ser un simple pasatiempo local. Pero cuando escuché a mi sobrino decir: «Por fin seré un top global», entendí que la competencia era el alma del proyecto.

Para que el ranking fuera real, integré Cloud Firestore en su plan gratuito. Ahora, cada partida se guarda en la nube, permitiendo una experiencia global, en tiempo real y sin servidor propio. Usamos autenticación anónima para que nadie pierda tiempo registrándose.

¿El resultado? 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.

¿Quieres probarlo? Aquí te dejo el APK para que seas de los primeros en probar CIRCLE. ¡Espero tus comentarios antes de lanzarlo oficialmente a la Play Store!

CIRCLE

Chat en tiempo real con Socket.IO, Redis y Mongo DB

Este proyecto es una prueba de concepto de un sistema de chat en tiempo real con soporte para múltiples salas, historial de mensajes y notificaciones. Está desarrollado con tecnologías pensadas para escalar en una arquitectura basada en microservicios.

Tecnologías utilizadas

  • Node.js + Express: Servidor HTTP y manejo de rutas.
  • Socket.IO: Comunicación en tiempo real con WebSockets.
  • MongoDB: Persistencia de mensajes y salas.
  • Redis: Sistema pub/sub para distribuir mensajes entre múltiples instancias del servidor.
  • HTML + JS: Cliente web simple.
  • PrimeFlex (opcional): Mejora visual de la interfaz.

Por qué Redis

Redis se usa para habilitar la publicación y suscripción de mensajes entre múltiples procesos o instancias del servidor. Esto es necesario cuando el servidor se ejecuta en un entorno distribuido o balanceado, ya que Socket.IO por sí solo no comparte información entre instancias.

Usar Redis asegura que todos los usuarios conectados reciban los mensajes aunque estén atendidos por diferentes procesos o servidores.

Estructura del Proyecto chat_0.1

Este proyecto tiene una arquitectura organizada para el desarrollo de una aplicación de chat utilizando Node.js con TypeScript y Redis. A continuación se describen los directorios y archivos principales:

Directorios

  • .git/
    Carpeta de configuración del repositorio Git. Contiene el historial de versiones y configuraciones de control de versiones.
  • node_modules/
    Directorio generado automáticamente por npm, contiene todas las dependencias del proyecto.
  • src/
    Carpeta principal del código fuente de la aplicación.
    • public/
      Directorio opcionalmente usado para archivos públicos (como HTML, CSS o JS accesibles directamente).
    • index.html
      Este archivo es la página principal que se muestra al abrir la aplicación en el navegador. Sirve como punto de entrada visual y normalmente contiene el diseño básico de la interfaz, además de los scripts necesarios para que la aplicación funcione.
<!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
    Archivo de configuración y conexión a la base de datos (posiblemente MongoDB, PostgreSQL u otro).
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
    Punto de entrada principal de la aplicación. Es donde se inicializa y arranca el servidor o el core del sistema.
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
    Configuración y lógica de conexión con Redis, utilizado probablemente para almacenamiento en caché o manejo de sesiones/chat en tiempo real.
import { createClient } from 'redis';

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

await pub.connect();
await sub.connect();
  • types.ts
    Archivo que define tipos y estructuras TypeScript personalizadas para mantener el tipado fuerte y organizado.
export interface ChatMessage {
  room: string;
  user: string;
  message: string;
}

export interface ChatRoom {
  name: string;
}

Archivos de Configuración

  • .gitignore
    Lista de archivos y carpetas que Git debe ignorar (por ejemplo: node_modules, logs, claves, etc.).
  • docker-compose.yml
    Archivo de configuración para Docker Compose. Define los servicios y contenedores necesarios para correr la aplicación en un entorno de contenedores (como Node, Redis, etc.).
  • package.json
    Archivo principal de configuración del proyecto Node.js. Contiene información del proyecto, scripts y dependencias.
  • package-lock.json
    Archivo generado automáticamente para bloquear versiones exactas de las dependencias, garantizando entornos consistentes.
  • README.md
    Archivo de documentación del proyecto. Suele contener instrucciones de instalación, uso y propósito del sistema.
  • tsconfig.json
    Configuración del compilador TypeScript. Define cómo se debe compilar el proyecto (paths, target, módulo, etc.).

Funcionalidades

  • Crear o unirse a salas desde el frontend
  • Listar salas disponibles
  • Enviar y recibir mensajes en tiempo real
  • Recibir historial de mensajes al ingresar a una sala
  • Persistencia de mensajes en MongoDB
  • Notificaciones dentro del chat
  • Exposición en red local usando 0.0.0.0

Código fuente:

GITHUB

Crear un árbol de directorios en proyectos Typescript de manera sencilla con IA

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!