Crea tu juego para el marketplace
De cero a publicado - construye un juego HTML5, comunicate con la plataforma via postMessage, pruebalo en local y publicalo en el marketplace de SouthGames.
Esta guia te lleva de cero a un juego publicado en el marketplace de SouthGames. No necesitas ningun otro documento: aca esta el protocolo completo, un juego minimo funcional, un harness para probar en local y el ciclo de publicacion paso a paso.
Que es un juego de marketplace
Un juego de marketplace es un juego HTML5 que las organizaciones instalan y ofrecen a sus clientes dentro de sus campañas. Tu juego:
- Se empaqueta como un ZIP con un
index.html. - Se ejecuta dentro de un iframe sandboxed (
sandbox="allow-scripts allow-same-origin allow-downloads",allow="autoplay; fullscreen"). - Se comunica con la plataforma exclusivamente via
postMessage— no hay acceso directo a APIs de SouthGames ni cookies de sesion. - Es servido por la plataforma como un unico HTML autocontenido: al publicar, el servidor descarga tu ZIP, inserta tus JS/CSS inline y convierte tus assets binarios en data URLs. Tu no haces nada de esto — pero condiciona como debes referenciar tus archivos (ver "El bundle").
El flujo de una partida siempre es el mismo:
- El juego avisa que cargo (
ready). - Lee la configuracion que definio la organizacion (
getConfig). - El cliente juega.
- El juego envia el resultado (
submitResult) y recibe, si gano, un codigo promocional. - El juego avisa que termino (
close).
El bundle (ZIP)
Reglas duras — el servidor valida cada una al subir la version y rechaza el ZIP si no se cumplen:
| Regla | Detalle |
|---|---|
| Formato | ZIP valido (si no parsea, se rechaza) |
index.html obligatorio | Debe existir un index.html en el ZIP. Puede estar en la raiz o dentro de una carpeta, pero recomendamos la raiz: todas las rutas de assets se resuelven relativas a la carpeta del index.html |
| Tamaño maximo | 50 MB (limite de la subida; apunta a mucho menos — ver presupuesto movil) |
| Version | Semver estricto X.Y.Z (por ejemplo 1.0.0); una version ya usada no se puede repetir |
| Changelog | Obligatorio, entre 5 y 2000 caracteres |
| Integridad | El servidor calcula el SHA-256 del ZIP al recibirlo y lo sella; ese hash se re-verifica al aprobar y cada vez que el juego se sirve |
Estructura recomendada:
mi-juego/
index.html <- punto de entrada (obligatorio)
game.js <- logica del juego
style.css <- estilos (opcional)
assets/
sprite.png
coin.wav
Para crear el ZIP con index.html en la raiz, comprime el contenido de la carpeta, no la carpeta. El -x excluye el harness de prueba (ver "Probar en local") — pasa la validacion igual, pero no embarques un archivo de test en la version sellada:
cd mi-juego
zip -r ../mi-juego.zip . -x "harness*.html"
Que hace la plataforma con tu ZIP (y por que te importa)
Al servir el juego, la plataforma construye un solo HTML:
- Los
script src="..."ylink rel="stylesheet"locales se insertan inline. - Todo asset binario del ZIP (
.png .jpg .jpeg .gif .webp .svg .mp3 .wav .ogg .mp4 .woff .woff2 .ttf) se convierte en data URL, y toda referencia entre comillas a ese archivo — en HTML, CSS o JS — se reemplaza por la data URL. Funciona con el nombre de archivo ("sprite.png"), la ruta relativa ("assets/sprite.png","./assets/sprite.png") y la ruta completa dentro del ZIP. - Las URLs externas (
http://,https://,//) se dejan tal cual.
Consecuencias practicas:
- Referencia tus assets como literales de string entre comillas:
img.src = "assets/sprite.png"funciona;img.src = "assets/" + nombre + ".png"no — la ruta compuesta en runtime no existe en el HTML final y el asset nunca carga. Si necesitas elegir assets dinamicamente, declara cada ruta como literal (por ejemplo en un objetoconst SPRITES = { perro: "assets/perro.png", gato: "assets/gato.png" }) y elige la clave, no el string. - Empaqueta todo en el ZIP. Un CDN externo puede caerse, cambiar o quedar bloqueado por el WebView de una app; tu juego autocontenido no.
- Los binarios crecen ~33% al pasar a base64 y todo termina en un solo documento HTML que el telefono del cliente parsea de una vez. El limite es 50 MB pero un buen juego de marketplace pesa menos de 2-3 MB; comprime imagenes (WebP), usa audio corto y evita videos.
El protocolo del bridge (postMessage)
Esta seccion es la referencia completa del protocolo, tal como lo implementa la plataforma. Si usas el paquete npm o lib.js no necesitas implementarlo — pero conviene entenderlo.
Sobres de mensajes
El juego envia requests al padre (window.parent.postMessage):
{ type: "southgames:request", id: "sg_1_1724700000000", method: "getConfig", payload: undefined }
id: string unico por request; la respuesta llega con el mismoid.method: uno de los metodos de la tabla de abajo.payload: argumento del metodo (solo lo usansubmitResultytrackEvent).
El padre responde con un mensaje response:
{ type: "southgames:response", id: "sg_1_1724700000000", success: true, payload: { ... } }
// o en error:
{ type: "southgames:response", id: "sg_1_1724700000000", success: false, error: "Permiso 'config' no concedido" }
El padre ademas puede empujar eventos al juego en cualquier momento:
{ type: "southgames:event", event: "pause", payload: undefined }
Y al cargar el iframe envia un mensaje init con su origin, para que el juego pueda dirigir sus mensajes a ese origin en vez de "*":
{ type: "southgames:init", origin: "https://app-de-la-org.example" }
Usar el init es opcional para un juego: el paquete npm lo captura automaticamente y dirige ahi sus mensajes; el mini-SDK del camino B lo hace en 3 lineas (ver abajo). Postear a "*" tambien funciona — cada request se casa con su respuesta por id, y el mensaje solo llega a la ventana padre de todos modos. Dirigirlo al origin del init es defensa en profundidad gratis: si la ventana padre navegara a otro sitio, tus mensajes (que incluyen resultados) ya no se entregarian a ese documento ajeno.
Si el padre no responde un request en 30 segundos, el SDK lo rechaza con timeout.
Metodos
| Metodo | Permiso requerido | Payload | Respuesta |
|---|---|---|---|
ready | ninguno | — | vacia. Avisa que el juego cargo. Llamalo primero y lo antes posible: el host muestra spinner hasta recibirlo y corta con error si no llega en ~15 segundos |
getConfig | config | — | objeto de configuracion definido por la organizacion segun tu configSchema |
getPlayer | user.displayName | — | displayName (string), locale (string) |
getSession | ninguno | — | campaignId (string), attemptId (string), remainingAttempts (number) |
submitResult | scoring | score (number, opcional), won (boolean, obligatorio), metadata (objeto, opcional) | promoCode (string o null), rewardType (string o null), message (string o null) |
trackEvent | analytics | eventName (string), data (objeto, opcional) | vacia |
close | ninguno | — | vacia. Avisa que el juego termino; el host cierra o transiciona |
Un metodo desconocido responde success: false. Un metodo cuyo permiso no fue concedido al juego responde success: false con un error tipo Permiso 'x' no concedido — pide solo los permisos que usas (ver "Permisos").
Eventos del padre
El host puede emitir eventos como pause y resume (por ejemplo cuando la app pasa a segundo plano). Con el paquete npm, suscribete con on/off:
SouthGames.on("pause", () => { pausado = true; });
SouthGames.on("resume", () => { pausado = false; });
Sin el paquete, despacha los mensajes southgames:event tu mismo — el mini-SDK del camino B ya lo trae (funcion sgOn).
Dos caminos para integrar el SDK
Camino A — paquete npm (si usas bundler)
npm install @southgames-sdk/game-sdk
import { SouthGames } from "@southgames-sdk/game-sdk";
await SouthGames.ready();
const config = await SouthGames.getConfig();
const session = await SouthGames.getSession();
// ... el cliente juega ...
const res = await SouthGames.submitResult({
score: 42,
won: true,
metadata: { nivel: 3 },
});
if (res.promoCode) mostrarCodigo(res.promoCode);
await SouthGames.close();
Todos los metodos devuelven promesas y estan tipados (TypeScript incluido). El paquete pesa menos de 2 KB y no tiene dependencias.
Camino B — postMessage puro (sin bundler, sin dependencias)
Si tu juego es un index.html plano, no necesitas npm: el protocolo completo — requests, eventos del padre e init — cabe en unas 35 lineas.
const pendientes = new Map();
const escuchas = new Map(); // eventos del padre: pause, resume, ...
let seq = 0;
let origenPadre = "*"; // se afina al recibir southgames:init
function sg(method, payload) {
return new Promise((resolve, reject) => {
const id = "sg_" + (++seq) + "_" + Date.now();
pendientes.set(id, { resolve, reject });
setTimeout(() => {
if (pendientes.delete(id)) reject(new Error("timeout: " + method));
}, 30000);
window.parent.postMessage({ type: "southgames:request", id, method, payload }, origenPadre);
});
}
function sgOn(evento, fn) {
if (!escuchas.has(evento)) escuchas.set(evento, []);
escuchas.get(evento).push(fn);
}
window.addEventListener("message", (e) => {
const d = e.data;
if (!d) return;
if (d.type === "southgames:init" && d.origin) { origenPadre = d.origin; return; }
if (d.type === "southgames:event") {
(escuchas.get(d.event) || []).forEach((fn) => fn(d.payload));
return;
}
if (d.type !== "southgames:response") return;
const p = pendientes.get(d.id);
if (!p) return;
pendientes.delete(d.id);
d.success ? p.resolve(d.payload) : p.reject(new Error(d.error || "error"));
});
// Uso: identico al camino A
// await sg("ready");
// const config = await sg("getConfig");
// await sg("submitResult", { score: 42, won: true });
// sgOn("pause", () => { pausado = true; });
// sgOn("resume", () => { pausado = false; });
Juego completo minimo: un clicker
Un juego funcional de verdad en un solo archivo. Copia esto como index.html, comprimelo en un ZIP y ya tienes algo publicable: inicializa, lee la configuracion de la organizacion, juega, envia el resultado y cierra.
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Clicker</title>
<style>
body { margin: 0; min-height: 100vh; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 16px;
font-family: system-ui, sans-serif; background: #111; color: #fff;
padding: env(safe-area-inset-top) env(safe-area-inset-right)
env(safe-area-inset-bottom) env(safe-area-inset-left); }
#tap { font-size: 2rem; padding: 24px 48px; border: 0; border-radius: 16px;
background: #e91e63; color: #fff; }
</style>
</head>
<body>
<h1 id="titulo"></h1>
<p id="hud">0</p>
<button id="tap">TAP</button>
<script>
// Mini SDK (version minima del camino B: solo requests; un juego real
// agrega tambien sgOn/init del snippet completo para pause/resume)
const pendientes = new Map(); let seq = 0;
function sg(method, payload) {
return new Promise((resolve, reject) => {
const id = "sg_" + (++seq) + "_" + Date.now();
pendientes.set(id, { resolve, reject });
setTimeout(() => { if (pendientes.delete(id)) reject(new Error("timeout")); }, 30000);
window.parent.postMessage({ type: "southgames:request", id, method, payload }, "*");
});
}
window.addEventListener("message", (e) => {
const d = e.data;
if (!d || d.type !== "southgames:response") return;
const p = pendientes.get(d.id);
if (!p) return;
pendientes.delete(d.id);
d.success ? p.resolve(d.payload) : p.reject(new Error(d.error || "error"));
});
// Juego
const DEMO = new URLSearchParams(location.search).get("demo") === "1";
let taps = 0, terminado = false;
const hud = document.getElementById("hud");
async function main() {
await sg("ready");
const config = await sg("getConfig").catch(() => ({}));
const meta = Number(config.meta) || 20; // toques para ganar
const segundos = Number(config.segundos) || 10; // duracion de la partida
// textContent, nunca innerHTML: la config la escribe un tercero
document.getElementById("titulo").textContent =
String(config.titulo || "Toca " + meta + " veces");
document.getElementById("tap").addEventListener("click", () => {
if (!terminado) hud.textContent = String(++taps);
});
setTimeout(async () => {
terminado = true;
const gano = taps >= meta;
hud.textContent = (gano ? "Ganaste" : "Perdiste") + " con " + taps + " toques";
if (DEMO) return; // en demo jamas se registra nada
const res = await sg("submitResult", { score: taps, won: gano, metadata: { meta } });
if (res.promoCode) hud.textContent += " - Tu codigo: " + res.promoCode;
await sg("close"); // recien DESPUES de que el resultado quedo guardado
}, segundos * 1000);
}
main();
</script>
</body>
</html>
Su configSchema (se define al crear el juego en el portal, ver "Ciclo de publicacion"):
{
"titulo": { "type": "text", "label": "Titulo", "default": "" },
"meta": { "type": "number", "label": "Toques para ganar", "min": 5, "max": 100, "default": 20 },
"segundos": { "type": "number", "label": "Duracion (segundos)", "min": 5, "max": 60, "default": 10 }
}
Referencia del configSchema
El configSchema es un objeto JSON: cada clave es un campo de configuracion y el portal le genera a la organizacion un formulario a partir de el (al instalar el juego y al asociarlo a una campaña). Tipos soportados:
type | Control que ve la organizacion | Atributos propios | Valor que llega a getConfig |
|---|---|---|---|
text | Campo de texto | — | string |
number | Campo numerico | min, max (number) | number |
boolean | Switch on/off | — | boolean |
select | Lista desplegable | options (array de strings; el valor guardado es la opcion elegida) | string |
Atributos comunes a todos los tipos:
type— uno de los cuatro de la tabla. Untypedesconocido (u omitido) se trata comotext.label— etiqueta del campo en el formulario; si falta se muestra la clave.default— valor inicial del campo y el que se usa cuando la organizacion no lo cambia.description— texto de ayuda que se muestra bajo el campo (opcional).
Un ejemplo con los cuatro tipos:
{
"titulo": { "type": "text", "label": "Titulo", "default": "Mi juego" },
"meta": { "type": "number", "label": "Toques para ganar", "min": 5, "max": 100, "default": 20,
"description": "Cuantos toques necesita el cliente para ganar" },
"sonido": { "type": "boolean", "label": "Sonido activado", "default": true },
"dificultad": { "type": "select", "label": "Dificultad", "options": ["facil", "normal", "dificil"], "default": "normal" }
}
Lo que NO existe (y como diseñar alrededor):
- No hay
required: ningun campo es obligatorio para la organizacion. Daledefaulta todo y aplica tus propios defaults al leer (Number(config.meta) || 20), como hace el clicker. - El servidor no valida la config contra tu schema: el formulario del portal es la unica barrera, y en algunas vistas la organizacion puede editar la config como JSON libre. Tu juego debe tolerar valores ausentes, de otro tipo o fuera de rango — normaliza y acota (
clamp) al leer. - No hay listas de objetos ni campos anidados. Si necesitas una estructura mas rica (por ejemplo una lista de premios), modela con varios campos planos o pide el dato como
texty parsealo tu con validacion defensiva.
Probar en local: el harness
Tu juego necesita un padre que responda al protocolo. Este harness lo simula completo — requests, init y eventos. Guardalo como harness.html junto a tu index.html (recuerda excluirlo del ZIP al publicar):
<!doctype html>
<meta charset="utf-8">
<title>Harness SouthGames</title>
<style>body{margin:0} iframe{width:100vw;height:100vh;border:0}</style>
<iframe id="game" src="./index.html" sandbox="allow-scripts allow-same-origin" allow="autoplay"></iframe>
<script>
const frame = document.getElementById("game");
// Cada entrada es un valor fijo, o una funcion (payload) => respuesta:
const respuestas = {
ready: undefined,
getConfig: { titulo: "Partida de prueba", meta: 5, segundos: 10 },
getPlayer: { displayName: "Tester", locale: "es" },
getSession: { campaignId: "camp_local", attemptId: "att_1", remainingAttempts: 3 },
// Como en produccion: hay codigo SOLO si el juego reporta won: true
submitResult: (p) => p && p.won === true
? { promoCode: "TEST-1234", rewardType: "promo_code", message: "Premio de prueba" }
: { promoCode: null, rewardType: null, message: "Sin premio esta vez" },
trackEvent: undefined,
close: undefined,
};
window.addEventListener("message", (e) => {
const d = e.data;
if (!d || d.type !== "southgames:request") return;
console.log("[harness]", d.method, d.payload ?? "");
const conocido = d.method in respuestas;
const r = respuestas[d.method];
frame.contentWindow.postMessage({
type: "southgames:response", id: d.id, success: conocido,
payload: typeof r === "function" ? r(d.payload) : r,
error: conocido ? undefined : "Metodo '" + d.method + "' no soportado",
}, "*");
});
frame.addEventListener("load", () => {
frame.contentWindow.postMessage({ type: "southgames:init", origin: location.origin }, "*");
});
// Para probar pause/resume desde la consola del navegador: sgEvento("pause")
window.sgEvento = (evento) =>
frame.contentWindow.postMessage({ type: "southgames:event", event: evento }, "*");
</script>
Sirvelo con cualquier servidor estatico (los file:// no funcionan con iframes):
npx serve .
# o
python3 -m http.server 8080
Abre http://localhost:8080/harness.html. En la consola del navegador veras cada mensaje del protocolo; cambia respuestas.getConfig para probar distintas configuraciones, ejecuta sgEvento("pause") / sgEvento("resume") en la consola para probar tus manejadores de eventos, y agrega ?demo=1 al src del iframe para probar tu modo demo. Prueba ambos finales de partida: al perder, submitResult responde promoCode: null — igual que en produccion — asi que tu pantalla de derrota no debe asumir que hay codigo.
Dos advertencias:
- Abre siempre el harness, no tu
index.htmlsuelto: sin un padre que responda el protocolo, cada request del juego muere a los 30 segundos por timeout. - El harness es una herramienta de desarrollo: no lo incluyas en el ZIP (el comando de "El bundle" ya lo excluye con
-x "harness*.html").
Permisos
Al crear el juego declaras que permisos necesita. La organizacion los ve antes de instalar. Hay dos clases:
Permisos funcionales — gatean metodos del bridge en runtime. Llamar a un metodo sin haber declarado su permiso responde success: false con Permiso 'x' no concedido:
| Permiso | Metodo que habilita |
|---|---|
scoring | submitResult — enviar el resultado de la partida |
config | getConfig — leer la configuracion de la organizacion |
user.displayName | getPlayer — nombre visible y locale del jugador |
analytics | trackEvent — eventos analiticos personalizados |
Permisos declarativos — rewards ("distribuye premios"), timer ("mecanicas con limite de tiempo") y audio ("reproduce audio") no gatean ningun metodo ni cambian el comportamiento del bridge. Son etiquetas informativas que la organizacion ve en la ficha del juego antes de instalar. Declararlos es opcional y no afecta el funcionamiento: un juego con countdown corre exactamente igual con o sin timer, y nada se rechaza por omitirlos. Declaralos si quieres que la ficha refleje esas caracteristicas; si dudas, omitelos.
El default al crear un juego es scoring + config — suficiente para la mayoria, incluidos juegos con timer o audio. Pide solo los funcionales que uses: menos permisos, mas facil de aprobar e instalar.
Buenas practicas (aprendidas en produccion)
Evalua "estoy embebido" al momento de usarlo, jamas en una const al cargar. En algunos WebViews nativos el bridge se inyecta un instante despues de que tu script arranca; una const EMBEBIDO = window.parent !== window evaluada al cargar puede quedar en false para siempre, y tu juego descartaria submitResult en silencio — partidas ganadas que nunca llegan al servidor. Usa una funcion:
function estaEmbebido() {
return window.parent !== window || !!window.SouthGamesNative;
}
El cierre espera al guardado. Siempre await el submitResult antes de llamar close: si cierras primero, el host puede desmontar el iframe antes de que el resultado llegue.
const res = await sg("submitResult", { score, won });
mostrarResultado(res);
await sg("close");
Implementa el modo demo (?demo=1). El dashboard previsualiza los juegos en "attract mode" agregando ?demo=1 a la URL del juego; leelo de tu propio location.search. En demo el juego se muestra jugando solo (o al menos jugable) y jamas llama submitResult ni trackEvent — un bot de demo contaminando puntajes o analitica reales es un incidente, no un detalle. Silencia tambien el audio: en demo no hubo gesto del usuario y el navegador lo bloquearia igual.
En attract mode el host mantiene el bridge completo activo: responde ready, getConfig, getPlayer y getSession con normalidad, asi que inicializa igual que siempre — await sg("ready") no se queda colgado y no necesitas fallbacks ni timeouts especiales para el modo demo. Dos diferencias si importan:
- La vista previa concede solo los permisos
configyuser.displayName: unsubmitResultotrackEventen demo seria rechazado conPermiso 'x' no concedido(otra razon para no llamarlos). getConfigpuede devolver{}(la vista previa de campaña no pasa config): tus defaults tienen que bastar para que el juego se vea bien solo, como hace el clicker.
Quien NO responde el protocolo es un index.html abierto suelto en el navegador, sin plataforma ni harness — ahi todo request muere por timeout, con o sin ?demo=1. Para probar en local usa siempre el harness (siguiente seccion).
viewport-fit=cover y safe-areas. Tu juego corre en WebViews de apps moviles, con notch y barras gestuales. Usa viewport-fit=cover en el meta viewport y respeta env(safe-area-inset-*) en el layout (el clicker de arriba lo hace).
textContent para todo texto que venga de la config. Los strings de getConfig los escribe la organizacion, no tu. Nunca los interpoles en innerHTML.
Presupuesto movil. Tu ZIP se convierte en un solo HTML con assets en base64 que un telefono de gama media debe descargar y parsear de una vez. Apunta a menos de 2-3 MB de ZIP, texturas WebP, audio corto. El limite duro de 50 MB es para no bloquearte, no una meta.
Llama ready() inmediatamente, antes de cargar assets pesados: el host muestra un spinner hasta recibirlo y declara error si tarda ~15 segundos. Carga tus assets despues de ready y muestra tu propia pantalla de carga si la necesitas.
Ciclo de publicacion
Todo se gestiona desde el portal, en Developer (menu lateral) — la seccion Mis juegos vive en /developer/games.
- Registrate como desarrollador. La primera vez, el portal te pide crear tu perfil de desarrollador (nombre visible, datos de contacto). Sin perfil activo no puedes crear juegos.
- Crea el juego. En "Mis juegos", crea un juego nuevo: nombre visible,
slugunico (minusculas, numeros y guiones), descripcion corta y larga, categoria (arcade,puzzle,trivia,luck,strategy,sports,other), tags, permisos (ver "Permisos"), locales soportados, configSchema (JSON — ver "Referencia del configSchema"), pricing y visibilidad (publicoprivate). El juego nace en estado borrador. - Sube una version. En el detalle del juego: numero de version semver (
1.0.0), changelog y el archivo ZIP. El portal sube el ZIP, calcula su hash SHA-256 y lo envia a revision. El servidor re-calcula el hash (si no coincide, la subida se corrompio — reintenta), valida que el ZIP parsee y contengaindex.html, y sella la version. La version queda en revision. Si es la primera version del juego, la ficha pasa a en revision; si el juego ya estaba publicado, la ficha no cambia y sigue sirviendo la version anterior mientras se revisa la nueva. - Revision. El equipo de SouthGames prueba la version. Puede aprobarla (la version queda sellada e inmutable, pasa a ser la version actual del juego y el juego queda publicado) o rechazarla con notas que veras en el detalle del juego — corriges y subes una version nueva. Un rechazo no baja un juego que ya estaba publicado: sigue sirviendo su version aprobada anterior. Aprobar tampoco republica un juego que un admin haya suspendido o deprecado.
- Instalacion. Con el juego publicado, las organizaciones lo encuentran en el marketplace, lo instalan y completan la configuracion que definiste en el
configSchema. Eso es lo que tu juego recibe engetConfig. - Actualizaciones. Sube una version nueva con semver mayor (una version usada no se puede repetir) y su changelog; pasa por la misma revision. Enviarla no interrumpe el servicio: los jugadores siguen con la version publicada hasta que la nueva se apruebe. Si subes otra version antes de que revisen la anterior, la anterior queda marcada como reemplazada y la revision se hace sobre la ultima. Las versiones aprobadas son inmutables: un arreglo siempre es una version nueva.
Recursos
- Ejemplo completo descargable: el juego Snake, con la arquitectura
lib.js+game.js, en la pestaña Game SDK de la seccion Developer del portal. - API de partidas del lado del host: POST /games/play — como las apps de las organizaciones registran partidas.
- Paquete npm:
@southgames-sdk/game-sdk(tipos TypeScript incluidos).
Si trabajas con un asistente de IA
Existen dos archivos de texto plano, autocontenidos y pensados para pegarse en el contexto de un asistente de codigo. Traen la misma informacion que esta guia mas el detalle exhaustivo del protocolo, las reglas duras aprendidas en produccion, una tabla de modos de falla y un checklist de autoverificacion:
- southgames.ai/llms-game-sdk.txt — construir el juego: contrato del bridge, reglas del bundle, esqueleto copiable, harness y reglas duras.
- southgames.ai/llms-marketplace.txt — operar el marketplace: registro, creacion, envio de version, revision, publicacion, instalacion y actualizacion de la version instalada.
- southgames.ai/llms.txt — indice de todos los recursos de SouthGames para agentes.