Saltar al contenido
Docs

Caso de estudio: Circuito Luz Verde

Recorrido comentado del juego de referencia de Green Games: la idea, el manifiesto, la escena, el script por partes con fragmentos reales, el guion del bot y qué prueba cada cosa.

Corresponde aFormato 1, API 1 Para quien escribe código

En esta página

Circuito Luz Verde es el juego de referencia de Green Games: una carrera de obstáculos contra el reloj, hecha para usar todos los servicios de la API en poco código. Esta página lo lee por dentro. Puedes jugarlo en https://play.green.games.

ArchivoTamaño
juego.jsonEl manifiesto, con 32 textos por idioma
escenas/principal.json109 líneas: 79 piezas, 5 apariciones, 7 zonas, 4 luces
scripts/principal.ts340 líneas
pruebas/bot.json34 pasos
El .ggx resultante9,0 KB

La idea en una frase#

Corre hasta la meta lo más rápido que puedas; ganas al llegar, pierdes si se acaba el tiempo; el puntaje es tu tiempo, menos un segundo por cada moneda.

Todo el diseño sale de ahí: una tabla de tiempos (gana el menor), un límite por dificultad y monedas que tientan a desviarse.

El manifiesto#

Lo que declara y por qué (games/circuito-luz-verde/juego.json):

juego.json (fragmento)
	"jugadores": {
		"min": 1,
		"max": 4
	},
	"controles": {
		"teclado": true,
		"tactil": true,
		"mando": true,
		"acciones": {
			"saltar": {
				"es": "Saltar",
				"en": "Jump"
			},
			"accion": {
				"es": "Llamar el ascensor",
				"en": "Call the lift"
			}
		}
	},
  • jugadores.max: 4 permite jugar con hasta tres bots. El script está escrito para una lista de jugadores, no para uno.
  • Dos acciones. saltar y accion. El texto de accion dice lo que hace en este juego, "Llamar el ascensor", y eso es lo que el jugador lee en la tabla de controles. accion2 no se declara, así que no tiene botón táctil ni aparece en la ayuda.
juego.json (fragmento)
	"puntajes": [
		{
			"id": "tiempo",
			"nombre": {
				"es": "Mejores tiempos",
				"en": "Best times"
			},
			"orden": "menor",
			"formato": "tiempo"
		}
	],
  • Una tabla, de tiempos. "orden": "menor" porque gana el tiempo más bajo, y "formato": "tiempo" para que se muestre como 1:23.45.
  • Tres dificultades (facil, normal, dificil) con normal como inicial.
  • Todos los textos están en textos, en español y en inglés, con variables: "monedas": "{n} de {total}", "sinTiempo": "Se acabó el tiempo. Llegaste al control {n} de {total}.".

La escena#

Un solo archivo, escenas/principal.json. El recorrido avanza por +Z, de z = 0 a z = 131, en siete tramos:

TramozQué hayQué enseña
Salida0 a 5Plataforma, semáforo de tres luces y una barreraPiezas con id que el script enciende y mueve
Losas9 a 22Cuatro losas sobre el vacíoPiezas distintas por dificultad
Plataforma móvil32 a 38Una plataforma que va y vienemovimiento declarado en la escena
Trampolín43Un disco neón que lanza al nivel de arribaEtiqueta más jugador.impulsar
Barredora63Un plato con un brazo que giraUna pieza movida por el script cada cuadro, con datos
Corredor74 a 106Un pasillo con semáforo y tres parapetosZona, rayo y etiquetas que cambian
Ascensor y meta115 a 131Cajas sueltas, un ascensor y el arco de metaPiezas dinámicas, acción del jugador, zona de meta

Dificultades en la escena#

Cada losa existe dos veces en el mismo lugar: ancha para facil y normal, angosta para dificil. Y en facil hay, además, puentes de madera entre losa y losa.

escenas/principal.json (fragmento)
		{ "id": "losa-1", "forma": "caja", "posicion": [0, -0.5, 9], "tamano": [3.4, 1, 3], "material": { "color": "verde", "patron": "rejilla" }, "dificultades": ["facil", "normal"] },
		{ "id": "losa-1-d", "forma": "caja", "posicion": [0, -0.5, 9], "tamano": [2.2, 1, 2.2], "material": { "color": "verde", "patron": "rejilla" }, "dificultades": ["dificil"] },
escenas/principal.json (fragmento)
		{ "forma": "caja", "posicion": [0, -0.35, 6.75], "tamano": [1.6, 0.5, 1.7], "material": "madera", "dificultades": ["facil"], "etiquetas": ["puente"] },

La sala carga solo las piezas de la dificultad elegida: el script no tiene que ocultar ni crear nada.

Etiquetas e ids#

  • Con id: lo que el script o el bot nombran uno por uno. barrera, brazo, ascensor, semaforo-corredor, luz-control-1.
  • Con etiqueta: lo que se atiende en grupo. peligro (el brazo), trampolin, banda (las dos franjas de luz del corredor), y control en las cuatro zonas de control.
  • Sin nada: postes, decorado y los bloques que flotan a los lados, todos con "fisica": "ninguna".

El brazo de la barredora lleva en datos lo que el script necesita para moverlo, en vez de tener esos números escritos en el código:

escenas/principal.json (fragmento)
		{ "id": "brazo", "forma": "caja", "posicion": [2.4, 4.75, 63.5], "tamano": [4, 0.7, 0.5], "material": { "color": "coral", "acabado": "neon" }, "etiquetas": ["peligro"], "datos": { "centro": [0, 4.75, 63.5], "radio": 2.4 } },

Puntos de control#

Cada control son tres cosas con nombres que calzan: una aparición control-1, una zona zona-control-1 con la etiqueta control, y una esfera luz-control-1 que el script enciende. Las monedas no están en la escena: las crea el script.

Los primeros segundos de una partida de demostración. La plataforma celeste es la pieza movil-1; el disco verde, el trampolín. Arriba, el HUD que pinta el script: monedas, cronómetro y control.

El script, por partes#

scripts/principal.ts tiene 340 líneas. Los fragmentos que siguen están copiados del archivo; // ... marca lo recortado.

Ajustes por dificultad#

scripts/principal.ts (líneas 5 a 11)
// --- Ajustes por dificultad ----------------------------------------------------------------------------------------
// juego.nivelDificultad es el indice de la dificultad elegida en juego.json: 0 facil, 1 normal, 2 dificil.
const AJUSTES = [
	{ limite: 300, giro: 50, verde: 3.2, ambar: 0.9, roja: 1.5, monedas: 24 },
	{ limite: 240, giro: 72, verde: 2.6, ambar: 0.8, roja: 1.8, monedas: 20 },
	{ limite: 180, giro: 95, verde: 2.0, ambar: 0.6, roja: 2.2, monedas: 16 }
][Math.max(0, juego.nivelDificultad)];

Una fila por dificultad y todo lo que cambia en un solo lugar: el límite de tiempo, la velocidad del brazo, cuánto dura cada luz del corredor y cuántas monedas hay. La escena cambia la forma del nivel; esta tabla, sus tiempos.

Estado de la partida y estado de cada jugador#

scripts/principal.ts (líneas 26 a 38)
let fase: 'menu' | 'cuenta' | 'carrera' | 'fin' = 'menu';
let libre = false;
let totalMonedas = 0;
let crono: Cronometro;
let luz: 'verde' | 'ambar' | 'roja' = 'verde';
let rojaDesde = 0;
let musica = 0;
let ascensorOcupado = false;
let juez: Actor;

/** Estado de cada jugador. Vive en jugador.datos para que cada uno tenga el suyo. */
interface Estado { control: number; monedas: number; llego: boolean; cubierto: boolean }
const estado = (j: Jugador) => j.datos as unknown as Estado;

Lo que es de todos (la fase, la luz del corredor, si el ascensor está ocupado) son variables del archivo. Lo que es de cada uno (su control, sus monedas, si ya llegó) vive en jugador.datos. La función estado solo le pone tipo. Con esa separación, el mismo script sirve con uno o con cuatro jugadores.

fase es una máquina de estados mínima: casi todos los eventos empiezan preguntando en qué fase está la partida.

El inicio: estado, menú y eventos#

Todo se registra dentro de juego.alIniciar. Primero, el estado de cada jugador:

scripts/principal.ts (líneas 50 a 54)
	jugadores.alEntrar((j) => {
		j.datos = { control: 0, monedas: 0, llego: false, cubierto: false };
		j.congelado = true; // nadie se mueve hasta la luz verde
		pintarHud(j);
	});

Después, un menú que ofrece dos modos y recuerda el mejor tiempo guardado con progreso:

scripts/principal.ts (líneas 56 a 73)
	// El menu de inicio se abre para todos; solo cuenta lo que elige una persona.
	const humano = jugadores.humanos()[0];
	const mejor = humano ? progreso.leer<number | null>(humano, `mejor-${juego.dificultad}`, null) : null;
	interfaz.menu({
		id: 'inicio',
		titulo: interfaz.t('menuTitulo'),
		texto: mejor === null ? interfaz.t('menuTexto') : interfaz.t('menuTextoMejor', { tiempo: mejor.toFixed(2) }),
		opciones: [
			{ id: 'carrera', texto: interfaz.t('menuCarrera') },
			{ id: 'paseo', texto: interfaz.t('menuPaseo') }
		]
	});
	interfaz.alElegir((menu, opcion, jugador) => {
		if (menu !== 'inicio' || jugador.esBot || fase !== 'menu') return;
		libre = opcion === 'paseo';
		interfaz.cerrarMenu('inicio');
		cuentaRegresiva();
	});

Tres detalles: un menú no pausa la partida, por eso los jugadores están congelados; la elección de un bot de relleno se ignora (jugador.esBot); y la clave de progreso lleva la dificultad, así cada una tiene su mejor tiempo.

Monedas: azar con semilla y toque por etiqueta#

Las monedas se crean al iniciar. Hay 31 lugares posibles y cada partida usa algunos:

scripts/principal.ts (líneas 161 a 177)
function sembrarMonedas(): void {
	// azar sale de la semilla de la partida: con la misma semilla, las monedas quedan en los mismos lugares.
	const elegidos = azar.mezclar(LUGARES).slice(0, AJUSTES.monedas);
	totalMonedas = elegidos.length;
	for (const posicion of elegidos) {
		const moneda = mundo.crear({
			forma: 'cilindro',
			posicion,
			rotacion: [90, 0, 0],
			tamano: [0.9, 0.16, 0.9],
			material: { color: 'ambar', acabado: 'brillante' },
			solida: false,
			etiquetas: ['moneda']
		});
		moneda.girar([0, 160, 0]);
	}
}

mundo.crear recibe el mismo formato que una pieza de la escena. Como el azar sale de azar, dos partidas con la misma semilla tienen las mismas monedas: por eso el bot puede probar el juego y el resultado se repite.

Un solo evento atiende a todas:

scripts/principal.ts (líneas 75 a 82)
	mundo.alTocar('#moneda', (moneda, j) => {
		interfaz.flotante(interfaz.t('menosUno'), moneda.posicion, { tipo: 'exito', para: j });
		moneda.eliminar();
		estado(j).monedas++;
		j.puntos++;
		sonido.tocar('moneda', { para: j, tono: 1 + estado(j).monedas * 0.02 });
		pintarHud(j);
	});

El sonido sube un poco de tono con cada moneda, y todo lleva para: j: solo lo ve y lo oye quien la recogió. interfaz.flotante es el "-1 s" que sube desde la moneda.

Peligros, trampolín y puntos de control#

scripts/principal.ts (líneas 84 a 106)
	mundo.alTocar('#peligro', (_pieza, j) => {
		if (fase !== 'carrera' || estado(j).llego) return;
		sonido.tocar('golpe', { para: j });
		interfaz.mensaje(interfaz.t('golpe'), { tipo: 'peligro', para: j });
		j.reaparecer();
	});

	mundo.alTocar('#trampolin', (_pieza, j) => {
		j.impulsar([0, 21, 5]);
		sonido.tocar('poder', { para: j });
	});

	mundo.alEntrar('#control', (zona, j) => {
		const n = CONTROLES.indexOf(zona.id.replace('zona-', '')) + 1;
		if (n <= estado(j).control) return;
		estado(j).control = n;
		j.aparicion = CONTROLES[n - 1];
		pintarHud(j);
		if (j.esBot) return;
		mundo.pieza(`luz-control-${n}`).material = { color: 'verde', acabado: 'neon' };
		sonido.tocar('control', { para: j });
		interfaz.mensaje(interfaz.t('controlAlcanzado', { n }), { tipo: 'exito', duracion: 1.6, para: j });
	});
  • j.reaparecer() devuelve al jugador a su aparición actual. j.aparicion = ... la cambia: eso es un punto de control.
  • El evento de control aprovecha los nombres de la escena: de la zona zona-control-2 sale la aparición control-2 y la luz luz-control-2.
  • if (n <= estado(j).control) return; evita que volver atrás "desactive" un control.

La barredora: una pieza movida cada cuadro#

scripts/principal.ts (líneas 132 a 140)
juego.cadaCuadro(() => {
	// La barredora: un brazo que da vueltas alrededor del eje. Se mueve a mano para mostrar posicion y rotacion.
	const brazo = mundo.pieza('brazo');
	const centro = brazo.datos.centro as Vec3;
	const radio = brazo.datos.radio as number;
	const grados = (reloj.ahora * AJUSTES.giro) % 360;
	const a = (grados * Math.PI) / 180;
	brazo.posicion = [centro[0] + Math.cos(a) * radio, centro[1], centro[2] - Math.sin(a) * radio];
	brazo.rotacion = [0, grados, 0];

El ángulo sale de reloj.ahora, el tiempo de partida, no de un contador propio: se detiene con la pausa y es igual en cada repetición. La posición se asigna como un vector nuevo. El centro y el radio vienen de datos de la escena.

Como el ángulo depende solo del tiempo, el bot puede esperar a que el brazo esté en un ángulo seguro (giroY), como se ve más abajo.

El corredor de la luz roja#

El tramo más elaborado. Un ciclo de temporizadores cambia la luz:

scripts/principal.ts (líneas 224 a 246, recortado)
function cicloDelCorredor(nueva: 'verde' | 'ambar' | 'roja'): void {
	if (fase === 'fin') return;
	luz = nueva;
	const semaforo = mundo.pieza('semaforo-corredor');
	// ...
	const color = nueva === 'verde' ? 'verde' : nueva === 'ambar' ? 'ambar' : 'coral';
	for (const banda of mundo.piezas('banda')) banda.material = { color, acabado: 'neon' };
	// Las etiquetas le sirven al bot de prueba para saber cuando partir (ver pruebas/bot.json).
	semaforo.desetiquetar('recien-verde');
	if (nueva === 'verde') {
		semaforo.etiquetar('recien-verde');
		reloj.despues(0.3, () => semaforo.desetiquetar('recien-verde'));
	}
	if (nueva === 'roja') rojaDesde = reloj.ahora;
	// ...
	const duracion = nueva === 'verde' ? AJUSTES.verde : nueva === 'ambar' ? AJUSTES.ambar : AJUSTES.roja;
	reloj.despues(duracion, () => cicloDelCorredor(nueva === 'verde' ? 'ambar' : nueva === 'ambar' ? 'roja' : 'verde'));
}

La función se vuelve a llamar a sí misma con reloj.despues: no hay setTimeout. Las bandas del piso cambian de color para que la luz se vea aunque el jugador no mire el semáforo. Y la etiqueta recien-verde, que dura 0,3 segundos, existe para el bot.

Cada cuadro se revisa quién se mueve con luz roja:

scripts/principal.ts (líneas 142 a 157)
	if (fase !== 'carrera') return;
	for (const j of mundo.zona('zona-corredor').jugadores()) {
		const cubierto = !loVeElSemaforo(j);
		if (cubierto !== estado(j).cubierto) {
			estado(j).cubierto = cubierto;
			interfaz.hud('cubierto', cubierto ? { texto: interfaz.t('aCubierto'), lugar: 'abajo-centro' } : null, j);
		}
		if (luz !== 'roja' || reloj.ahora - rojaDesde < 0.35 || cubierto) continue;
		const [vx, , vz] = j.velocidad;
		if (Math.hypot(vx, vz) > 1.2) {
			sonido.tocar('alerta', { para: j });
			interfaz.mensaje(interfaz.t('pillado'), { tipo: 'peligro', para: j });
			j.reaparecer();
		}
	}
});

Tres decisiones de justicia: solo se revisa a quien está dentro de la zona del corredor; hay 0,35 segundos de gracia desde que la luz se pone roja; y se tolera una velocidad de hasta 1,2 (el avatar frenando).

El corredor con luz verde. Las bandas del piso toman el color de la luz; los bloques rayados son los parapetos.

Los parapetos sirven de escondite gracias a un rayo:

scripts/principal.ts (líneas 248 a 256)
/** Un rayo desde el semaforo hasta el pecho del jugador: si choca antes con un parapeto, el jugador esta a cubierto. */
function loVeElSemaforo(j: Jugador): boolean {
	const ojo: Vec3 = [0, 7.9, 105.9];
	const p = j.posicion;
	const hacia: Vec3 = [p[0] - ojo[0], p[1] + 1 - ojo[1], p[2] - ojo[2]];
	const distancia = Math.hypot(hacia[0], hacia[1], hacia[2]);
	const impacto = mundo.rayo(ojo, hacia, distancia);
	return impacto === null || impacto.distancia > distancia - 0.6;
}

mundo.rayo choca solo con piezas sólidas e ignora a los jugadores: si el rayo llega hasta el jugador sin chocar antes, el semáforo lo ve.

El ascensor: una acción del jugador#

scripts/principal.ts (líneas 117 a 120)
	jugadores.alAccion('accion', (j) => {
		if (ascensorOcupado || !mundo.zona('zona-ascensor').jugadores().includes(j)) return;
		subirAscensor();
	});

alAccion se dispara una vez por pulsación, esté donde esté el jugador. El script decide si cuenta: solo si está dentro de la zona del ascensor y el ascensor está libre. subirAscensor lo mueve con pieza.moverA y, al llegar, programa la bajada con alLlegar y reloj.despues. Las plataformas que se mueven llevan a quien está parado encima.

El final#

scripts/principal.ts (líneas 278 a 304, recortado)
function llegar(j: Jugador): void {
	const e = estado(j);
	if (e.llego || fase !== 'carrera') return;
	e.llego = true;
	j.congelado = true;
	if (j.esBot) {
		interfaz.mensaje(interfaz.t('botLlego', { nombre: j.nombre }));
		return;
	}
	fase = 'fin';
	crono.detener();
	sonido.detener(musica);
	sonido.tocar('victoria');
	const tiempo = crono.valor;
	const final = Math.max(0, Math.round((tiempo - e.monedas) * 100) / 100);
	// ...
	const mejor = progreso.leer<number | null>(j, `mejor-${juego.dificultad}`, null);
	if (mejor === null || final < mejor) progreso.guardar(j, `mejor-${juego.dificultad}`, final);
	reloj.despues(1.2, () => {
		juego.terminar({ resultado: 'victoria', puntaje: final, tabla: 'tiempo', de: j, detalle: interfaz.t('finDetalle', { tiempo: tiempo.toFixed(2), monedas: e.monedas }) });
	});
}
  • Si llega un bot de relleno, se anuncia y la partida sigue: la partida es de la persona.
  • El puntaje es el tiempo menos un segundo por moneda, redondeado a centésimas.
  • juego.terminar con tabla: 'tiempo' y de: j registra el puntaje. detalle es la línea que el panel de fin muestra bajo el resultado.
  • juego.terminar se llama 1,2 segundos después de llegar, con reloj.despues.

Si el tiempo se acaba, otra función termina con derrota, sin tabla:

scripts/principal.ts (líneas 306 a 313)
function sinTiempo(): void {
	if (fase !== 'carrera') return;
	fase = 'fin';
	sonido.detener(musica);
	sonido.tocar('derrota');
	const humano = jugadores.humanos()[0];
	juego.terminar({ resultado: 'derrota', detalle: interfaz.t('sinTiempo', { n: humano ? estado(humano).control : 0, total: CONTROLES.length }) });
}

Los dos caminos terminan en juego.terminar. Es la regla 8 de los scripts: toda partida debe terminar.

El panel de fin lo pone el reproductor. El script solo entrega resultado, puntaje, tabla y detalle: aquí, 41,67 segundos menos 15 monedas.

Los cuatro adornos#

Cuatro adornos del script usan lo que se agregó después de la primera versión del contrato:

QuéDóndePara qué
actores.crear y juez.seguirRutaLíneas 45 y 203Un juez que espera junto a la salida y se pasea cuando empieza la carrera.
camara.tomaLínea 186Durante la cuenta regresiva, la cámara mira el semáforo desde un costado y vuelve sola.
interfaz.rotuloLíneas 48, 265 y 270Un rótulo sobre el ascensor, que mientras sube muestra una barra de avance.
interfaz.flotanteLínea 76El "-1 s" que sale de cada moneda.

Ninguno cambia las reglas del juego: sin ellos la carrera es la misma.

El guion del bot#

pruebas/bot.json juega la carrera entera en 34 pasos. Empieza eligiendo el modo en el menú y esperando a que la barrera se levante:

pruebas/bot.json (comienzo)
{
	"$schema": "../../../packages/sdk/esquemas/bot.schema.json",
	"formato": 1,
	"tiempoMaximo": 170,
	"esperado": { "resultado": "victoria" },
	"pasos": [
		{ "elegir": "inicio:carrera" },
		{ "esperarPieza": "barrera", "cerca": [0, 3.4, 5], "radio": 0.3 },
		{ "ir": [0, 0, 9] },
		{ "ir": [1.5, 0, 13.5] },
		{ "ir": [-1.5, 0, 18] },
		{ "ir": [0, 0, 22.5] },
		{ "ir": "aparicion:control-1", "control": true },
  • Un ir por losa. Los cuatro puntos son los centros de las cuatro losas. El bot va en línea recta y salta solo ante el borde.
  • control: true en cada punto de control: si el juego devuelve al bot (lo barrió el brazo, se movió con luz roja), retoma desde ahí y no desde el principio.
  • tiempoMaximo es 170 y el límite de la dificultad más corta es 180: la prueba exige ganar con margen.

El resto del guion repite una idea: esperar a que el mundo esté en el estado correcto y después caminar.

pruebas/bot.json (el trampolín, la barredora y el ascensor)
		{ "ir": [0, 0, 41.9] },
		{ "ir": [0, 4, 52], "saltar": "no" },
json
		{ "esperarPieza": "brazo", "giroY": [115, 150] },
		{ "ir": [3, 4, 61.5] },
		{ "ir": [3, 4, 65.5] },
		{ "ir": [0, 4, 70.5] },
json
		{ "ir": [0, 4.4, 125] },
		{ "pulsar": "accion" },
		{ "esperarPieza": "ascensor", "cerca": [0, 10.2, 125], "radio": 0.1 },
		{ "ir": "zona:meta" }
  • En el trampolín, "saltar": "no": el impulso lo da el juego.
  • En la barredora espera un ángulo del brazo y rodea el plato por un costado en tres pasos.
  • En el ascensor se para encima, pulsa la acción, espera a que la pieza llegue arriba y camina a la meta.

El corredor se cruza en tres carreras, cada una después de esperar la etiqueta recien-verde. Esos pasos están comentados en Pruebas con el bot.

Qué prueba cada cosa#

Qué se compruebaCon qué
Los archivos son JSON válido y cumplen los esquemasgreengames validar
Cada pieza, zona, aparición, tabla y clave de texto que el script nombra existegreengames validar
El script no usa nada que no exista en la sala, y sus tipos calzangreengames validar
Las dos listas de textos tienen las mismas claves y variablesgreengames validar
La partida se puede ganar en las tres dificultadesgreengames probar, con el guion
Las losas angostas de dificil se pueden saltar; el corredor se puede cruzarEl guion, que pasa por ahí en cada dificultad
Ningún evento lanza errores y no se excede ningún límitegreengames probar
Con la misma semilla, la partida se repite igualgreengames probar, que repite la primera corrida
Las monedas cambian con la semillaLas dos semillas dan puntajes distintos

Salida real de greengames probar sobre una copia del juego:

greengames probar circuito-luz-verde
  OK    facil semilla 1: victoria, puntaje 26.67, 44.9 s de partida en 325 ms; script 0.018 ms/cuadro (max 0.84), 99 piezas
  OK    facil semilla 2: victoria, puntaje 27.67, 44.9 s de partida en 285 ms; script 0.019 ms/cuadro (max 0.82), 99 piezas
  OK    normal semilla 1: victoria, puntaje 30.83, 47.0 s de partida en 273 ms; script 0.016 ms/cuadro (max 0.81), 90 piezas
  OK    normal semilla 2: victoria, puntaje 31.83, 47.0 s de partida en 334 ms; script 0.017 ms/cuadro (max 1.02), 90 piezas
  OK    dificil semilla 1: victoria, puntaje 32.78, 45.0 s de partida en 279 ms; script 0.017 ms/cuadro (max 0.77), 86 piezas
  OK    dificil semilla 2: victoria, puntaje 33.78, 45.0 s de partida en 319 ms; script 0.017 ms/cuadro (max 1.37), 86 piezas
circuito-luz-verde: prueba aprobada (6 de 6 corridas, .ggx de 9.0 KB)

Qué se lee ahí:

  • En facil, la partida dura 44,9 segundos con las dos semillas y el puntaje es 26,67 con una y 27,67 con la otra: el mismo recorrido y un segundo de diferencia, que es una moneda.
  • Hay más piezas en facil (99) que en dificil (86): los puentes y las monedas extra.
  • El script gasta menos de 0,02 ms por cuadro en promedio, muy por debajo del milisegundo que recomienda la lista de calidad.

Lo que el guion no prueba: el modo paseo, la derrota por tiempo y la partida con bots de relleno. Para la derrota haría falta un segundo guion que espere perder.

Sigue con#