Serigy T-Rex
Pesos do classificador ordinal para o Chrome T-Rex Runner (estado do jogo em PT-BR).
Checkpoint raw (sem cola Safe/Unsafe). Sensor: Qwen/Qwen3-0.6B-Base.
| Arquivo | Função |
|---|---|
serigy_jev.safetensors |
Checkpoint |
serigy_jev.json |
Metadados (model_id, σ, dims) |
demo.mp4 |
Vídeo do experimento (3 lanes) |
classe_ordinal |
Ação |
|---|---|
0 |
abaixar |
1 |
fazer_nada |
2 |
pular |
Experimento (vídeo)
Serigy × Jev × Laya — skill pura (unassisted raw), seed 7, time_scale=0.15:
Referência: Serigy ≈ Jev ≈ 310 score · Laya ≈ 204.
O que o dinossauro “vê”
Antes de cada decisão o jogo monta um texto em PT (não pixels). A string tem 3 blocos:
Exemplo completo
Jogo do dinossauro. Velocidade 6. Posicao: no chao.
Obstaculo 'cacto grande' a 100 px a frente.
ZONA=JANELA (100 px → pular cacto/passaro baixo ou abaixar passaro alto).
Escolha uma acao: pular, abaixar ou fazer nada.
LONGE/>=160 ou no ar: fazer_nada.
JANELA 70-140 no chao: pular (cacto / passaro baixo) ou abaixar (SO passaro alto).
NUNCA abaixar em cacto nem passaro baixo.
Bloco A — observação (muda a cada frame)
Jogo do dinossauro. Velocidade 6. Posicao: no chao.
Obstaculo 'cacto grande' a 100 px a frente.
ZONA=JANELA (100 px → pular cacto/passaro baixo ou abaixar passaro alto).
| Pedaço | Significado |
|---|---|
Velocidade 6 |
velocidade atual |
Posicao: no chao |
no chão / no ar / abaixado |
Obstaculo '…' a 100 px |
tipo + distância do obstáculo da frente |
ZONA=JANELA |
distância → VAZIO / LONGE (≥160) / JANELA (70–140) / PERTO / NO_AR |
Bloco B — pedido (fixo)
Escolha uma acao: pular, abaixar ou fazer nada.
Bloco C — regras fixas do treino raw (fixas)
LONGE/>=160 ou no ar: fazer_nada.
JANELA 70-140 no chao: pular (cacto / passaro baixo) ou abaixar (SO passaro alto).
NUNCA abaixar em cacto nem passaro baixo.
Neste exemplo: cacto na JANELA → o modelo deve devolver classe_ordinal = 2 (pular).
O raw não inclui Safe/Unsafe nem “melhor ação” do planner.
1. Inferência rápida
pip install serigy
from serigy import SerigyModel
model = SerigyModel("wendellperbar/serigy-trex")
texto = (
"Jogo do dinossauro. Velocidade 6. Posicao: no chao. "
"Obstaculo 'cacto grande' a 100 px a frente. "
"ZONA=JANELA (100 px → pular cacto/passaro baixo ou abaixar passaro alto). "
"Escolha uma acao: pular, abaixar ou fazer nada. "
"LONGE/>=160 ou no ar: fazer_nada. "
"JANELA 70-140 no chao: pular (cacto / passaro baixo) "
"ou abaixar (SO passaro alto). "
"NUNCA abaixar em cacto nem passaro baixo."
)
out = model.predict(texto)
print(out["classe_ordinal"], out["score_final"], out["probabilidades"])
2. Mini-arena autocontida (Serigy × Jev × Laya)
Script abaixo: física simplificada do T-Rex, prompt raw no mesmo formato do treino, e três jogadores. Não depende de nenhum outro código além dos pacotes listados.
Dependências
pip install serigy typesafe-sdk laya
export JEV_API_KEY=... # ou TYPESAFE_API_KEY — só para o player Jev
Laya e Jev são opcionais: sem chave/pacote, o script pula a lane.
Protocolo do experimento
| Parâmetro | Valor |
|---|---|
| prompt | raw (zonas Chrome; sem Safe/Unsafe) |
time_scale |
0.15 (ablacão de latência) |
| seed | 7 |
| decide a cada | 6 frames |
| cacto / pássaro baixo na JANELA (70–140 px) | pular |
| pássaro alto na JANELA | abaixar |
| LONGE (≥160) ou no ar | fazer_nada |
Script completo
Salve como mini_arena_trex.py e rode python mini_arena_trex.py.
"""Mini-arena T-Rex: Serigy (Hub) × Jev (TypeSafe) × Laya — autocontida."""
from __future__ import annotations
import math
import os
import random
import time
from dataclasses import dataclass, field
# ---------------------------------------------------------------------------
# Física mínima (60 FPS, y para cima a partir do chão)
# ---------------------------------------------------------------------------
FPS = 60
GRAVIDADE = 0.6
VEL_PULO = 10.0
DINO_X, DINO_W, DINO_H, DINO_H_DUCK = 50.0, 44.0, 47.0, 25.0
VEL0, VEL_MAX, ACEL = 6.0, 13.0, 0.001
JANELA_MIN, JANELA_MAX, LONGE = 70.0, 140.0, 160.0
# tipo → (w, h, y_base, nome_pt)
OBS = {
"cacto": (25.0, 50.0, 0.0, "cacto grande"),
"cacto_x3": (75.0, 50.0, 0.0, "tres cactos grandes"),
"passaro_alto": (46.0, 40.0, 40.0, "passaro alto"),
"passaro_baixo": (46.0, 40.0, 10.0, "passaro baixo"),
}
@dataclass
class Obstaculo:
tipo: str
x: float
y: float
w: float
h: float
@dataclass
class Game:
seed: int
time_scale: float = 0.15
frame: int = 0
vel: float = VEL0
score: float = 0.0
dino_y: float = 0.0
dino_vy: float = 0.0
abaixado: bool = False
vivo: bool = True
obstaculos: list[Obstaculo] = field(default_factory=list)
_next_x: float = 600.0
_rng: random.Random = field(default_factory=random.Random)
def __post_init__(self) -> None:
self._rng = random.Random(self.seed)
self._spawn()
@property
def no_ar(self) -> bool:
return self.dino_y > 0.5
def _spawn(self) -> None:
tipos = ["cacto", "cacto", "cacto_x3", "passaro_alto", "passaro_baixo"]
# curso simples: alterna tipos com gap generoso
tipo = tipos[self._rng.randrange(len(tipos))]
if self.vel < 7.0 and tipo.startswith("passaro"):
tipo = "cacto"
w, h, y, _ = OBS[tipo]
gap = w * self.vel + 120.0 + self._rng.random() * 80.0
self._next_x = max(self._next_x, 600.0) + gap
self.obstaculos.append(
Obstaculo(tipo, self._next_x, y, w, h)
)
def frente(self) -> Obstaculo | None:
cand = [o for o in self.obstaculos if o.x + o.w > DINO_X]
return min(cand, key=lambda o: o.x) if cand else None
def dist(self) -> float | None:
o = self.frente()
return None if o is None else max(0.0, o.x - DINO_X)
def step(self, acao: str) -> None:
if not self.vivo:
return
ts = max(1e-6, self.time_scale)
scroll = self.vel * ts
# tecla
if acao == "pular" and not self.no_ar:
self.dino_vy = VEL_PULO
self.abaixado = False
elif acao == "abaixar" and not self.no_ar:
self.abaixado = True
elif acao == "fazer_nada":
if not self.no_ar:
self.abaixado = False
# física dino
if self.no_ar or self.dino_vy > 0:
self.dino_y += self.dino_vy
self.dino_vy -= GRAVIDADE
if self.dino_y <= 0:
self.dino_y, self.dino_vy = 0.0, 0.0
# scroll obstáculos
for o in self.obstaculos:
o.x -= scroll
self.obstaculos = [o for o in self.obstaculos if o.x + o.w > -20]
if not self.obstaculos or self.obstaculos[-1].x < 500:
self._spawn()
# colisão AABB
h = DINO_H_DUCK if self.abaixado and not self.no_ar else DINO_H
dx0, dy0, dx1, dy1 = DINO_X, self.dino_y, DINO_X + DINO_W, self.dino_y + h
for o in self.obstaculos:
if dx0 < o.x + o.w and dx1 > o.x and dy0 < o.y + o.h and dy1 > o.y:
self.vivo = False
break
self.frame += 1
self.score += self.vel * ts * 0.1
self.vel = min(VEL_MAX, self.vel + ACEL * ts)
# ---------------------------------------------------------------------------
# Prompt raw (mesmo contrato do treino)
# ---------------------------------------------------------------------------
def formatar_estado(g: Game) -> str:
if g.no_ar:
pos = "Posicao: no ar."
elif g.abaixado:
pos = "Posicao: abaixado."
else:
pos = "Posicao: no chao."
o = g.frente()
dist = g.dist()
if o is None or dist is None:
desc = "Nenhum obstaculo proximo (mais de 300 px)."
zona = "ZONA=VAZIO (fazer_nada)."
else:
nome = OBS[o.tipo][3]
desc = f"Obstaculo '{nome}' a {dist:.0f} px a frente."
if g.no_ar:
zona = "ZONA=NO_AR (so fazer_nada)."
elif dist >= LONGE:
zona = f"ZONA=LONGE ({dist:.0f} px → fazer_nada)."
elif dist >= JANELA_MIN:
zona = (
f"ZONA=JANELA ({dist:.0f} px → pular cacto/passaro baixo "
"ou abaixar passaro alto)."
)
else:
zona = f"ZONA=PERTO ({dist:.0f} px → na duvida fazer_nada)."
return (
f"Jogo do dinossauro. Velocidade {g.vel:.0f}. {pos} {desc} {zona} "
"Escolha uma acao: pular, abaixar ou fazer nada. "
"LONGE/>=160 ou no ar: fazer_nada. "
"JANELA 70-140 no chao: pular (cacto / passaro baixo) "
"ou abaixar (SO passaro alto). "
"NUNCA abaixar em cacto nem passaro baixo."
)
def acao_cinematica(proposta: str, g: Game) -> str:
"""Decide cedo; só executa pular/abaixar na JANELA (timing)."""
if g.no_ar:
return "fazer_nada"
dist = g.dist()
if dist is None:
return "fazer_nada"
if proposta == "fazer_nada":
return "fazer_nada"
if dist > JANELA_MAX:
return "fazer_nada" # ainda longe — espera
if dist < JANELA_MIN and proposta == "pular":
return "fazer_nada" # passou a janela de takeoff
return proposta
# ---------------------------------------------------------------------------
# Players
# ---------------------------------------------------------------------------
INSTRUCOES = (
"Skill pura (sem cola), timing Chrome: "
"(1) ZONA=NO_AR → fazer_nada. "
"(2) ZONA=LONGE (>=160) → fazer_nada. "
"(3) ZONA=JANELA (70-140) no chao: pular (cacto/passaro baixo) "
"ou abaixar (SO passaro alto). "
"(4) NUNCA abaixar em cacto nem passaro baixo."
)
CRITERIOS = {
"pular": (
"Pular em cacto / tres cactos / passaro baixo na ZONA=JANELA. "
"Nunca em passaro alto; nunca em LONGE."
),
"abaixar": (
"Abaixar SOMENTE em passaro alto na ZONA=JANELA. "
"VETO: nunca abaixar em cacto."
),
"fazer_nada": (
"Fazer nada em LONGE, vazio, no ar, ou fora da janela."
),
}
def decidir_serigy(model, texto: str) -> str:
out = model.predict(texto)
return {0: "abaixar", 1: "fazer_nada", 2: "pular"}[
int(out["classe_ordinal"])
]
def decidir_jev(client, Choice, texto: str) -> str:
q = {
"acao": Choice(instructions=INSTRUCOES, criteria=CRITERIOS),
}
result = client.system_one(texto, q, model="jev-latest")
key = None
if hasattr(result, "choices") and "acao" in result.choices:
ans = result.choices["acao"]
key = getattr(ans, "choice", None) or getattr(ans, "key", None)
if key not in {"pular", "abaixar", "fazer_nada"}:
key = "fazer_nada"
return key
def decidir_laya(router, texto: str) -> str:
questions = {
"acao": {
"type": "choice",
"instructions": INSTRUCOES
+ " VETO: cacto → nunca abaixar.",
"criteria": CRITERIOS,
}
}
result = router.predict(texto, questions, model="multilingual")
key = None
if hasattr(result, "choices") and "acao" in result.choices:
ans = result.choices["acao"]
key = getattr(ans, "choice", None) or getattr(ans, "key", None)
elif isinstance(result, dict):
key = (result.get("acao") or {}).get("choice")
if key not in {"pular", "abaixar", "fazer_nada"}:
key = "fazer_nada"
return key
# ---------------------------------------------------------------------------
# Loop
# ---------------------------------------------------------------------------
def rodar(nome: str, decidir, seconds: float = 20.0, seed: int = 7) -> dict:
g = Game(seed=seed, time_scale=0.15)
max_frames = int(seconds / g.time_scale * FPS)
decide_every = 6
proposta = "fazer_nada"
decisoes = 0
t_lat = []
while g.vivo and g.frame < max_frames:
if g.frame % decide_every == 0:
texto = formatar_estado(g)
t0 = time.perf_counter()
proposta = decidir(texto)
t_lat.append((time.perf_counter() - t0) * 1000.0)
decisoes += 1
acao = acao_cinematica(proposta, g)
g.step(acao)
lat = sorted(t_lat)
p50 = lat[len(lat) // 2] if lat else 0.0
return {
"player": nome,
"score": round(g.score, 1),
"frames": g.frame,
"vivo": g.vivo,
"decisoes": decisoes,
"lat_p50_ms": round(p50, 1),
}
def main() -> None:
from serigy import SerigyModel
serigy = SerigyModel("wendellperbar/serigy-trex", warm=True)
resultados = [
rodar("serigy", lambda t: decidir_serigy(serigy, t)),
]
key = (
os.environ.get("JEV_API_KEY")
or os.environ.get("TYPESAFE_API_KEY")
or ""
).strip()
if key:
os.environ.setdefault("TYPESAFE_API_KEY", key)
from typesafe_sdk import Choice, TypeSafeClient
jev = TypeSafeClient()
resultados.append(
rodar("jev", lambda t: decidir_jev(jev, Choice, t))
)
else:
print("skip jev (defina JEV_API_KEY ou TYPESAFE_API_KEY)")
try:
from laya import Router
laya = Router(preload=True, default="multilingual")
resultados.append(
rodar("laya", lambda t: decidir_laya(laya, t))
)
except ImportError:
print("skip laya (pip install laya)")
for r in resultados:
print(r)
if __name__ == "__main__":
main()
Cada player vê o mesmo seed (mesmo curso). Ajuste seconds no rodar(...) para partidas mais longas.
O que o Serigy faz aqui
- O jogo monta o texto do estado (velocidade, posição, obstáculo, zona).
SerigyModel.predict(texto)→classe_ordinal{0,1,2}.- A cinemática só aplica pular/abaixar dentro da JANELA 70–140 px.
É o mesmo contrato de entrada/saída do checkpoint deste Hub.
Model tree for wendellperbar/serigy-trex
Base model
Qwen/Qwen3-0.6B-Base