Simulacion concurrente multihilo en C#/.NET 8 con dashboard web en tiempo real.
Una historia de amor entre ratones, contada con hilos, semaforos y TCP.
En el reino de Roedalia, la princesa Elisabetha vive en su castillo rodeada de sus cuatro damas del lazo, mientras el caballero Lance patrulla la guarnicion con sus cuatro caballeros del Porton Norte. Ninguno sabe que el destino los unira.
Mientras tanto, los Alquimistas conspiran desde sus torres para impedirlo, preparando pociones que debilitan la Chispa — la fuerza magica que mide el vinculo entre ambos protagonistas.
La simulacion termina cuando ambos alcanzan Chispa 100 y se reunen. Los alquimistas intentan impedirlo. El destino decide.
┌──────────────────────────────────┐
│ Browser (Dashboard) │
│ Canvas pixel-art + SignalR WS │
└──────────┬───────────────────────┘
│ WebSocket :8080
┌──────────▼───────────────────────┐
│ ASP.NET Core Server │
│ ┌─────────────────────────────┐ │
│ │ GameServer (TCP) │ │
│ │ MessageRouter + Handlers │ │
│ └──────────┬──────────────────┘ │
│ ┌──────────▼──────────────────┐ │
│ │ 5 State Singletons │ │
│ │ Simulation │ Tavern │ Market │ │
│ │ NorthGate │ Positions │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ EmbeddedClientManager │ │
│ │ (arranca clientes in-proc) │ │
│ └─────────────────────────────┘ │
└──────────┬───────────────────────┘
│ TCP :5000
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ Elisabetha │ │ Lance │ │ Alquimistas │
│ 1 conexion │ │ 1 conexion │ │ N conexiones │
│ 5 hilos │ │ 5 hilos │ │ 1 hilo/alquim. │
│ (princesa + │ │ (caballero + │ │ (Mordecai, │
│ 4 damas) │ │ 4 caballeros) │ │ Vespertino) │
└───────────────┘ └─────────────────┘ └──────────────────┘
El proyecto consta de 5 proyectos .NET con una separacion limpia de responsabilidades:
| Proyecto | Tipo | Descripcion |
|---|---|---|
Roedalia.Core |
Class Library | Modelos, protocolo TCP, serializacion, configuracion |
Roedalia.Server |
ASP.NET Core | Servidor TCP + Dashboard web + Estado de juego |
Roedalia.Client.Elisabetha |
Console App | Elisabetha + 4 Damas del Lazo |
Roedalia.Client.Lance |
Console App | Lance + 4 Caballeros del Porton Norte |
Roedalia.Client.Alquimistas |
Console App | 2 Alquimistas (1 conexion TCP cada uno) |
Este proyecto es un laboratorio practico de programacion concurrente que demuestra:
| Concepto | Donde se usa |
|---|---|
| Hilos concurrentes | Cada personaje corre en su propio Task con RunAsync() |
| CancellationToken | Propagacion de cancelacion coordinada entre todos los hilos |
| SemaphoreSlim | Exclusion mutua en ElisabethaState.AttendLock y LanceState.AttendLock |
| ConcurrentQueue | Comunicacion productores-consumidores entre damas/caballeros y protagonistas |
| ConcurrentDictionary | Registro de clientes TCP, posiciones de personajes, caballeros heridos |
| async/await | Todo el modelo de concurrencia es asincronico, sin bloqueos |
| volatile | Flags de estado compartido (SimulationEnded, CharacterReady) |
| Task.WhenAll / WhenAny | Coordinacion de grupo: esperar a todos los hilos de un cliente |
| Productor-Consumidor | Damas producen mensajes, Elisabetha los consume cuando atiende |
| Reader-Writer Lock | Estado de Chispa protegido con semaforo para lecturas/escrituras atomicas |
| Graceful Shutdown | CancellationToken + OperationCanceledException para parada limpia |
La Chispa es el valor central de la simulacion (0-100) para cada protagonista. Representa el vinculo magico entre Elisabetha y Lance.
ELISABETHA LANCE
┌─────────────────┐ ┌─────────────────┐
│ Pergamino +20 │ │ Duelo limpio +7 │
│ Reencuentro+10 │ │ Reencuentro +10 │
│ ───────────── │ │ ────────────── │
│ Rumor dama -5 │ │ Duelo herida -5 │
│ Baile oblig -5 │ │ Pocion alq. -20 │
│ Pergam. mal -5 │ │ Amenaza alq.-30 │
│ Pocion alq.-20 │ │ │
└────────┬────────┘ └────────┬─────────┘
│ │
│ PRIMER ENCUENTRO │
│ (Taberna) ──────► ambos = 75
│ │
│ Pre-encuentro: │
│ Elisabetha cap 30 │
│ Lance cap 50 │
│ │
▼ ▼
Chispa = 100 Chispa = 100
│ │
└───────────┐ ┌─────────────────┘
▼ ▼
SIMULACION FINALIZADA
Antes de conocerse: Elisabetha no puede superar Chispa 30 y Lance no puede superar Chispa 50. El primer encuentro en la Taberna eleva a ambos a 75.
Los Alquimistas son la fuerza antagonista: sus pociones restan -20 o -30 de Chispa, intentando que los protagonistas nunca lleguen a 100.
Princesa del Castillo de Roedalia. Cada turno elige aleatoriamente entre:
- Atender a una dama — escucha rumores, confidencias o invitaciones al baile
- Asistir al baile — solo si una dama la convencio (Chispa -5)
- Leer pergaminos — 50% emocionante (+20) / 50% soporifero (-5)
- Escaparse — al Mercado (compra productos) o a la Taberna (posible encuentro con Lance)
Isadora, Celestina, Brunilda y Rosmerta. Cada una:
- Realiza labores — montar a caballo, esgrima, enterarse de rumores
- Intenta contactar a Elisabetha — espera a ser la dama seleccionada (patron productor-consumidor con
ConcurrentQueue)
Caballero de la Guarnicion. Cada turno:
- Habla con un companero — escucha confidencias u ofensas sobre Elisabetha
- Desafia a duelo — solo si un caballero ofendio a Elisabetha. 80% victoria limpia (+7), 20% hiere al oponente (-5 + herida 30s)
- Realiza guardia — en el Porton Norte (inspecciona carretas) o en la Taberna (posible encuentro con Elisabetha)
Roderick, Bramwell, Thorne y Gareth. Cada uno:
- Vigila un lugar — Porton Norte, muralla este o torres de vigia
- Intenta contactar a Lance — 75% confidencia, 25% ofensa sobre Elisabetha (provoca duelo)
Mordecai y Vespertino. Antagonistas que operan desde las Torres:
- Estudiar calderos (30s) — 30% pocion para Elisabetha, 30% para Lance, 40% fracaso
- Visitar a Elisabetha — 30% logra aplicar pocion (Chispa -20)
- Visitar a Lance — 80% intenta enganar con pocion (-20), 20% amenaza (-30)
| Ubicacion | Descripcion | Quien la visita |
|---|---|---|
| Castillo | Hogar de Elisabetha y las damas | Elisabetha, Damas, Alquimistas |
| Guarnicion | Cuartel de Lance y los caballeros | Lance, Caballeros, Alquimistas |
| Taberna | Punto de encuentro de Elisabetha y Lance | Elisabetha, Lance |
| Mercado | Comercio con 8 productos disponibles | Elisabetha |
| Porton Norte | Control de carretas (locales vs foraneas) | Lance |
| Torres | Laboratorio de los alquimistas | Alquimistas |
El dashboard en http://localhost:8080 muestra en tiempo real:
- Mapa pixel-art con 6 ubicaciones renderizadas en Canvas
- Sprites de personajes con animacion de movimiento, idle bounce y etiquetas de actividad
- Barras de Chispa con 4 colores segun nivel (rojo/azul/naranja/verde)
- Panel de estado con flags de encuentro y simulacion
- Panel de ubicaciones con estado de Taberna, Porton Norte y Mercado
- Log de eventos categorizado por colores (taberna, alquimista, duelo, mercado, epico...)
- Sistema de particulas con 9 efectos (primer encuentro, reencuentro, duelo, pocion, simulacion finalizada...)
- Panel de clientes embebidos para arrancar/detener los 3 grupos desde el browser
- Boton de reinicio para resetear toda la simulacion sin reiniciar el servidor
# Clonar
git clone https://github.com/redsaporo/Roedalia.git
cd Roedalia
# Compilar
dotnet build Roedalia.sln
# Arrancar servidor (TCP :5000 + HTTP :8080)
dotnet run --project src/Roedalia.ServerAbrir http://localhost:8080 en el navegador.
Pulsa "Iniciar Todos" en el panel CLIENTES del dashboard. Los personajes aparecen en el mapa y comienzan a moverse.
En terminales separadas:
# Terminal 2 - Elisabetha + 4 Damas
dotnet run --project src/Roedalia.Client.Elisabetha
# Terminal 3 - Lance + 4 Caballeros
dotnet run --project src/Roedalia.Client.Lance
# Terminal 4 - 2 Alquimistas
dotnet run --project src/Roedalia.Client.Alquimistas# Servidor con TCP en 6000 y HTTP en 9090
dotnet run --project src/Roedalia.Server -- 6000 9090
# Clientes conectando a puerto TCP personalizado
dotnet run --project src/Roedalia.Client.Elisabetha -- localhost 6000Roedalia/
├── Roedalia.sln
└── src/
├── Roedalia.Core/ # Libreria compartida
│ ├── GameConfig.cs # Configuracion centralizada
│ ├── Models/
│ │ ├── Character.cs # Enum CharacterType + nombres
│ │ ├── Location.cs # Enum LocationType
│ │ ├── Product.cs # Productos, carretas y reglas
│ │ └── SparkLevel.cs # Modelo de nivel de Chispa
│ ├── Network/
│ │ ├── TcpMessageClient.cs # Cliente TCP con framing
│ │ └── TcpMessageServer.cs # Servidor TCP multi-cliente
│ └── Protocol/
│ ├── Message.cs # Mensaje con Type + Payload JSON
│ ├── MessageSerializer.cs # Serializa/deserializa sobre NetworkStream
│ ├── MessageType.cs # Enum de tipos de mensaje
│ ├── Requests/ # DTOs de peticiones
│ └── Responses/ # DTOs de respuestas
│
├── Roedalia.Server/ # Servidor principal
│ ├── Program.cs # Entry point, DI, Kestrel
│ ├── GameServer.cs # TCP server + logica de simulacion
│ ├── Dashboard/
│ │ ├── DashboardHub.cs # Hub SignalR (estado + control clientes)
│ │ ├── WebDashboard.cs # Broadcast periodico de estado
│ │ └── EventEntry.cs # Modelo de evento
│ ├── Handlers/
│ │ ├── MessageRouter.cs # Enruta mensajes TCP a handlers
│ │ ├── TavernHandler.cs # Logica de la Taberna
│ │ ├── MarketHandler.cs # Logica del Mercado
│ │ ├── NorthGateHandler.cs # Logica del Porton Norte
│ │ ├── SparkHandler.cs # Modificacion de Chispa
│ │ └── AlchemistHandler.cs # Pociones y amenazas
│ ├── Services/
│ │ └── EmbeddedClientManager.cs # Lifecycle de clientes embebidos
│ ├── State/
│ │ ├── SimulationState.cs # Chispa, encuentros, fin
│ │ ├── TavernState.cs # Presencia en taberna
│ │ ├── MarketState.cs # Ultimo cliente/producto
│ │ ├── NorthGateState.cs # Cola de carretas
│ │ └── CharacterPositionState.cs # Posiciones en el mapa
│ └── wwwroot/
│ ├── index.html # SPA del dashboard
│ ├── css/style.css # Estilos pixel-art
│ └── js/
│ ├── dashboard.js # SignalR client + DOM updates
│ ├── map.js # Renderizado del mapa en Canvas
│ └── sprites.js # Sprites + sistema de particulas
│
├── Roedalia.Client.Elisabetha/ # Cliente: grupo Elisabetha
│ ├── Program.cs # Conexion + arranque de 5 hilos
│ ├── Characters/
│ │ ├── ElisabethaThread.cs # Hilo principal de Elisabetha
│ │ └── DamaDelLazo.cs # Hilo de cada dama
│ └── LocalState/
│ └── ElisabethaState.cs # Estado compartido del grupo
│
├── Roedalia.Client.Lance/ # Cliente: grupo Lance
│ ├── Program.cs # Conexion + arranque de 5 hilos
│ ├── Characters/
│ │ ├── LanceThread.cs # Hilo principal de Lance
│ │ └── CaballeroPortonNorte.cs # Hilo de cada caballero
│ └── LocalState/
│ └── LanceState.cs # Estado compartido del grupo
│
└── Roedalia.Client.Alquimistas/ # Cliente: alquimistas
├── Program.cs # N conexiones TCP independientes
└── Characters/
└── AlquimistaThread.cs # Hilo de cada alquimista
La comunicacion usa un protocolo binario simple con framing por longitud:
┌──────────┬──────────┬─────────────────────┐
│ Length │ Type │ JSON Payload │
│ (4 bytes)│ (4 bytes)│ (variable) │
└──────────┴──────────┴─────────────────────┘
Tipos de mensaje: RegisterClient, EnterTavern, LeaveTavern, EnterMarket, MarketPurchase, InspectCart, SparkUpdate, SparkQuery, AlchemistVisitElisabetha, AlchemistVisitLance, SimulationEnd, Heartbeat, Ack, entre otros.
Cada mensaje lleva un payload JSON serializado con los datos de la peticion/respuesta correspondiente.
| Componente | Tecnologia |
|---|---|
| Runtime | .NET 8 / C# 12 |
| Servidor web | ASP.NET Core + Kestrel |
| Comunicacion TCP | TcpListener / TcpClient con async streams |
| WebSocket | SignalR |
| Frontend | HTML5 Canvas + vanilla JavaScript |
| Fuente | Press Start 2P (pixel-art) |
| Concurrencia | Task, SemaphoreSlim, ConcurrentQueue, ConcurrentDictionary, CancellationToken |
MIT
"La Chispa ha triunfado en Roedalia."