feat: +18 eventos escandalo/momento LATAM, premios nuevos, fix export ELO, e2e tests

This commit is contained in:
lolero-dev
2026-08-03 01:12:38 -03:00
parent 472a250782
commit d801e7324c
5 changed files with 627 additions and 20 deletions
+6 -6
View File
@@ -6,23 +6,23 @@ Estado actual y mejoras posibles, en orden de impacto.
- [x] Juego completo en un solo archivo (`index.html`), autocontenido, offline.
- [x] 68 equipos reales con logos, 5 roles, 6 regiones.
- [x] 26 eventos de decisión en 6 categorías.
- [x] 44 eventos de decisión en 7 categorías (26 originales + 18 nuevos: LATAM scandals + momentos BO5).
- [x] 5 competencias internacionales (First Stand, MSI, EWC, Asian Games, Worlds).
- [x] Títulos regionales (1ª y 2ª) + premios individuales.
- [x] Títulos regionales (1ª y 2ª) + premios individuales (incluye Mejor Novato, Jugador del Año).
- [x] Fichajes con cadencia realista (≥ split 4, ~2/carrera).
- [x] Gating: equipos de 2ª división no van a internacionales.
- [x] UI estilo Copero (dark dashboard, badges, pills, timeline).
- [x] Responsive mobile/desktop, timeline colapsable.
- [x] Export de imagen 1080×1920 compartible.
- [x] Export de imagen 1080×1920 compartible (fix superposición ELO MÁXIMO/número).
- [x] Deploy en https://lolero.cbcren.online (Caddy + systemd + puerto 8899).
- [x] Documentación (README, ARCHITECTURE, TESTING, DEPLOY).
- [x] Test E2E automatizado (`tests/e2e_smoke.py`, Playwright): 0 errores, 100% carreras completas.
## 🔜 Ideas de mejora
### Gameplay
- [ ] Más eventos por categoría (replayability).
- [ ] Más premios individuales (Mejor Novato, MVP del Split con más variantes).
- [ ] Eventos de "momento" (ganar/revivir un BO5, finales emocionantes).
- [ ] Más eventos por categoría (replayability). ← 18 nuevos (escándalos LATAM reales, BO5s, remontadas)
- [ ] Eventos de "momento" con más variantes de revancha.
- [ ] Sistema de lesiones/visa más ligado a la selección regional.
### UI / UX
+398 -14
View File
@@ -378,6 +378,7 @@ const CATS = {
entrenamiento: { name: 'Entrenamiento', icon: '🎯' }, salud: { name: 'Salud y tilt', icon: '🩹' },
preparacion: { name: 'Preparación', icon: '💪' }, alimentacion: { name: 'Alimentación', icon: '🍜' },
escandalo: { name: 'Escándalo', icon: '😱' }, estudios: { name: 'Plan B', icon: '📚' },
momento: { name: 'Momento', icon: '⚔️' },
};
const EVENTS = [
{ cat: 'entrenamiento', title: 'Cambió la meta', ctx: 'Parche nuevo: el campeón que grindeaste toda la temporada quedó en tier B. El coach te mira raro en la reunión de draft.',
@@ -668,6 +669,387 @@ const EVENTS = [
pos: 'Aprendiste lo justo: "yes", "no" y "GG". Alcanza para los que importan.',
neg: 'En la entrevista post-partido dijiste "I am tilted" para describir la victoria. Clip eterno.' },
]},
{ cat: 'entrenamiento', title: 'El draft experimental', ctx: 'El coach llega con un cuaderno lleno de picks que "la meta todavía no entendió". Te toca a vos estrenarlos en scrims.',
opts: [
{ t: 'Probar los picks locos', d: { mecanica: 2.0, sinergia: 1.0 }, risk: 0.4,
pos: 'El pick raro fue un antes y un después: los rivales no saben contra qué juegan. Draft pick legend.',
neg: 'El pick raro era raro por algo. Te gankearon 9 veces antes de los 12 minutos y el coach borró el cuaderno.' },
{ t: 'Pedir draft seguro', d: { vision: 1.8 }, risk: 0.15,
pos: 'Con lo conocido jugás al nivel de siempre y el scouting rinde. El coach te mira y asiente.',
neg: 'Los picks seguros no sorprenden a nadie: los rivales te counterpean en cada serie.' },
{ t: 'Proponer tu propio campeón', d: { sinergia: 1.4, marca: 1.0 }, risk: 0.3,
pos: 'El equipo te dejó armar la comp alrededor de tu main y salió redondo. El staff nota tu lectura.',
neg: 'Tu propuesta era mala y lo sabías: el split entero te cargan con la "comp del genio".' },
]},
{ cat: 'entrenamiento', title: 'El rival de SoloQ', ctx: 'Hay un tipo que te persigue en ranked: cada vez que entrás en cola, aparece del otro lado y te hace la vida imposible.',
opts: [
{ t: 'Estudiarlo y ganarle la mental war', d: { vision: 2.2 }, risk: 0.25,
pos: 'Le sacaste el patrón: invadís donde él wardeaba y el tipo ya no sabe jugar contra vos. Lo quebraste.',
neg: 'Lo estudiaste tanto que empezaste a jugar para contrarrestarlo y te olvidaste del propio juego.' },
{ t: 'Sacarlo de tu cola a la fuerza', d: { mecanica: 2.0 }, risk: 0.4,
pos: 'DuoQ con tu jungla y a la mierda: le ganaste 4 seguidas y el tipo cambió de horario de cola.',
neg: 'Perdiste 6 seguidas intentándolo: el tipo es mejor que vos y encima ahora lo sabe.' },
{ t: 'Invitarle a un scrim', d: { sinergia: 1.6, marca: 1.0 }, risk: 0.2,
pos: 'El rival eterno resultó copado: se probó con vos y ahora es tu compañero de duo. Alianza inesperada.',
neg: 'El tipo usó el scrim para robarte las estrategias del equipo. Tu culpa, dice el coach.' },
]},
{ cat: 'salud', title: 'Insomnio crónico', ctx: 'Tres meses durmiendo 4 horas por noche. Los reflejos siguen, pero tu cabeza ya no procesa el mapa a tiempo.',
opts: [
{ t: 'Rutina de sueño estricta', d: { sinergia: 2.0 }, risk: 0.12,
pos: 'Dos semanas durmiendo 8 horas y volviste con una claridad mental que no tenías desde la academia.',
neg: 'La rutina estricta te costó una scrim de madrugada que era "importante" según tu jungla.' },
{ t: 'Cafeína y adrenalina hasta el final', d: { mecanica: 1.6 }, risk: 0.45,
pos: 'El cuerpo aguanta de milagro y el split te lo llevaste con reflejos de 16 años.',
neg: 'A los 20 años jugás como si tuvieras 40: la cabeza te va un segundo tarde en cada teamfight.' },
{ t: 'Pedir ayuda profesional', d: { marca: 1.2, sinergia: 0.8 }, risk: 0.1,
pos: 'El especialista del sueño te rearmó los horarios y hasta el equipo adoptó tu rutina. Iluminado.',
neg: 'La consulta salió cara y el médico te dijo que el problema era el LoL. No lo vas a aceptar.' },
]},
{ cat: 'salud', title: 'La vista cansada', ctx: 'Doce horas de pantalla por día y los ojos te arden en el loading screen. El oculista del equipo te mira preocupado.',
opts: [
{ t: 'Lentes con filtro y descansos de 20-20-20', d: { vision: 2.2 }, risk: 0.1,
pos: 'La regla 20-20-20 te cambió la vida: los ojos frescos y el mapa lo leés con otra claridad.',
neg: 'Los lentes te hicieron ver TODO, incluso los wards que no existían. Jugaste persiguiendo sombras.' },
{ t: 'Seguir así, total es joda', d: { mecanica: 1.2 }, risk: 0.5,
pos: 'La juventud es una droga: un split más sin problema. (El oculista no opina igual.)',
neg: 'El ojo izquierdo te dijo basta en pleno game 5 y perdiste la serie por un skillshot que no viste.' },
{ t: 'Reducir el streaming y el celular', d: { sinergia: 1.4, marca: -0.6 }, risk: 0.1,
pos: 'Menos pantalla extra = más juego. Tu vista agradece y tu rank también.',
neg: 'Menos stream = menos marca. Los fans te preguntan si estás bien y tus números de audiencia bajan.' },
]},
{ cat: 'preparacion', title: 'Retiro de la leyenda', ctx: 'Un pro retirado, leyenda viva del server, te ofrece sesiones privadas de entrenamiento. Te quiere "dejar algo antes de irse".',
opts: [
{ t: 'Aceptar todas las sesiones', d: { mecanica: 2.4, vision: 1.0 }, risk: 0.3,
pos: 'La leyenda te pasó los secretos que no salen en los VODs: jugás con la cabeza de un veterano.',
neg: 'La leyenda te hizo jugar con SU estilo, que era de otra época. El meta actual te come vivo.' },
{ t: 'Entrenar con él de sparring', d: { mecanica: 1.8 }, risk: 0.2,
pos: 'Perder 50 partidas seguidas contra una leyenda te afiló el filo. Ahora el server te queda chico.',
neg: 'Tanta derrota en sparring te quebró la confianza: hasta en tu Elo dudás de cada decisión.' },
{ t: 'Decirle que no, sos tu propia leyenda', d: { sinergia: 1.2, marca: 1.4 }, risk: 0.25,
pos: 'El rechazo con respeto te dio una imagen de seguridad que la comunidad aplaude. Autosuficiente.',
neg: 'La leyenda se ofendió y en su stream te tiró una indirecta sutil (no era sutil).' },
]},
{ cat: 'preparacion', title: 'Retreat de team building', ctx: 'El equipo se va tres días a la montaña, sin pantallas, con fogata y dinámicas de grupo. El coach insiste en que "nos une".',
opts: [
{ t: 'Ir con todo el equipo', d: { sinergia: 2.4 }, risk: 0.15,
pos: 'La fogata y la caminata hicieron más que mil scrims: el equipo juega como hermanos ahora.',
neg: 'Tres días sin jugar = tres días de reflejos oxidados. Volviste y la primera scrim fue un desastre.' },
{ t: 'Quedarte y grindear solo', d: { mecanica: 1.8 }, risk: 0.3,
pos: 'Mientras el equipo se abrazaba en la montaña, vos subiste dos rangos de SoloQ. Nivel al palo.',
neg: 'El equipo volvió unido y vos sos el outsider que no fue. Las calls ya no te incluyen.' },
{ t: 'Ir pero con la laptop "por las dudas"', d: { sinergia: 1.4, marca: 0.8 }, risk: 0.2,
pos: 'Jugaste un poco, compartiste un montón: el equilibrio perfecto entre team y grind.',
neg: 'Te vieron con la laptop y te cargaron todo el viaje. Dicen que el wifi de la montaña no perdona.' },
]},
{ cat: 'alimentacion', title: 'La nueva cafetería del staff', ctx: 'Abrieron una cafetería gamer en la gaming house: café premium, snacks y un menú saludable que el nutricionista aprobó.',
opts: [
{ t: 'Menú saludable del nutricionista', d: { sinergia: 1.8 }, risk: 0.08,
pos: 'La comida rica Y saludable existe: tu energía es estable y el estómago te perdona todo.',
neg: 'El menú saludable es caro y el sueldo de rookie no llega a fin de mes con café premium.' },
{ t: 'Café a full como los pros coreanos', d: { mecanica: 1.6 }, risk: 0.35,
pos: 'El café coreano te dio el empuje para las 14 horas: tu grind sube de nivel.',
neg: 'Cinco cafés al día: manos temblorosas en la scrim y el jungla te pidió que te calmes.' },
{ t: 'Hacerte socio de la cafetería', d: { marca: 1.6 }, risk: 0.3,
pos: 'Tu cara en el cartel de la cafetería del equipo: la marca crece y el café sale gratis.',
neg: 'La cafetería cerró a los dos meses: perdiste la inversión y el cartel con tu cara quedó en la calle.' },
]},
{ cat: 'alimentacion', title: 'El desayuno del equipo', ctx: 'El staff médico quiere implementar desayuno obligatorio en la gaming house. Vos siempre jugaste con el estómago vacío.',
opts: [
{ t: 'Desayuno completo todos los días', d: { sinergia: 1.6 }, risk: 0.1,
pos: 'El cerebro con nafta juega distinto: tu macro mejora y el tilt baja un 40%.',
neg: 'El desayuno pesado te da sueño en las primeras horas. La scrim de las 10 AM es un martirio.' },
{ t: 'Seguir con el ayuno matutino', d: { mecanica: 1.4 }, risk: 0.3,
pos: 'Tu cuerpo ya está adaptado: jugás con la mente liviana y los reflejos a full.',
neg: 'A mitad de game 4 te agarró el bajón de azúcar: moriste solo en el río por un skillshot fácil.' },
]},
{ cat: 'escandalo', title: 'Se filtró tu sueldo', ctx: 'Un medio publicó la lista de salarios de la liga y el tuyo es el más bajo de los titulares. El vestuario entero lo vio.',
opts: [
{ t: 'Exigir renegociación ahora', d: { marca: 1.8 }, risk: 0.4,
pos: 'La presión pública jugó a tu favor: la directiva te subió el sueldo y una cláusula de mejora.',
neg: 'La directiva se sintió expuesta y te marcó: el ambiente se congeló y tu contrato siguió igual.' },
{ t: 'Callar y demostrar en el server', d: { sinergia: 1.2, mecanica: 1.2 }, risk: 0.2,
pos: 'Jugaste como un animal el split: ahora todos saben que cobrás poco y jugás mucho. Stock alto.',
neg: 'Callaste y los demás cobran el triple. El resentimiento te come por dentro (pero rendís bien).' },
{ t: 'Publicar un stream descontracturado', d: { marca: 1.6 }, risk: 0.15,
pos: 'Lo tomaste con humor en stream: la comunidad te quiere y hasta el CEO te felicitó por la actitud.',
neg: 'El stream lo vio la directiva y no les gustó el tono. Te citaron a una reunión incómoda.' },
]},
{ cat: 'escandalo', title: 'Rivalidad con el streamer', ctx: 'Un streamer famoso dijo en vivo que "cualquiera llega a pro hoy en día". Sus 200 mil viewers te etiquetaron a vos en los clips.',
opts: [
{ t: 'Aceptar el 1v1 en su stream', d: { marca: 2.2 }, risk: 0.35,
pos: 'Le ganaste el 1v1 en vivo frente a 200 mil personas: tu nombre explota y la marca se dispara.',
neg: 'Perdiste el 1v1 por un detalle y el clip se hizo viral. La gente no recuerda el contexto, recuerda la derrota.' },
{ t: 'Ignorarlo y seguir con tu carrera', d: { sinergia: 1.0 }, risk: 0.2,
pos: 'No le diste bola y el tema murió solo. Tu carrera habla por vos.',
neg: 'El streamer siguió tirando indirectas un mes y parte de la comunidad se lo creyó.' },
{ t: 'Invitarle a la gaming house', d: { marca: 1.8, sinergia: 0.8 }, risk: 0.2,
pos: 'El "rival" resultó ser copado y el video del tour de la gaming house explotó: views y subs.',
neg: 'El streamer se llevó material para hacerte quedar mal en su canal. Error de estrategia.' },
]},
{ cat: 'estudios', title: 'El secundario pendiente', ctx: 'Tu familia lo vuelve a mencionar: te falta terminar el secundario. Un instituto online te propone terminarlo en un año.',
opts: [
{ t: 'Terminarlo online', d: { marca: 1.4, sinergia: 0.6 }, risk: 0.2,
pos: 'Título en mano y la familia en paz: además los sponsors lo valoran y tu imagen mejora.',
neg: 'Entre el estudio y el juego, algo se resiente: las noches de parcial te matan el split.' },
{ t: 'Dejarlo para después de retirarme', d: { mecanica: 1.4 }, risk: 0.25,
pos: 'Todo el foco en la carrera: la plata y los títulos hablan más que un papel.',
neg: 'El tema sigue pendiente y tu familia lo saca en cada llamada. "¿Y el secundario?"' },
{ t: 'Pagarle a un tutor que te prepare', d: { marca: 1.0, sinergia: 0.8 }, risk: 0.1,
pos: 'El tutor te hace los resúmenes y vos estudiás lo justo: aprobás sin sacrificar el game.',
neg: 'El tutor resultó ser un fan que solo quería conocer a un pro. Perdiste la plata y el tiempo.' },
]},
{ cat: 'estudios', title: 'El podcast de tu equipo', ctx: 'El manager de contenidos te ofrece ser la cara del podcast semanal del equipo. Tiempo fuera de la práctica, pero exposición.',
opts: [
{ t: 'Aceptar ser la voz del podcast', d: { marca: 2.0 }, risk: 0.2,
pos: 'Tu humor en el podcast lo rompe: el equipo gana fans y vos te convertís en la cara visible.',
neg: 'Dijiste una frase polémica en vivo y el clip dio la vuelta al mundo. Comunicado oficial mediante.' },
{ t: 'Participar solo cuando haya fecha libre', d: { sinergia: 1.2 }, risk: 0.12,
pos: 'Aparecés cuando podés: la exposición justa sin sacrificar el game.',
neg: 'Tu ausencia se nota y el manager te lo dice: "el podcast te necesita más que vos a él".' },
{ t: 'Negarme: soy jugador, no influencer', d: { mecanica: 1.2 }, risk: 0.2,
pos: 'Todo el tiempo al juego: el rendimiento puro te mantiene en la elite.',
neg: 'Mientras tus rivales hacen marca, tu nombre suena menos: los sponsors te miran de reojo.' },
]},
{ cat: 'momento', title: 'BO5 contra el rival eterno', ctx: 'Playoffs: 2-2 en la serie contra el equipo que te humilló en la temporada regular. El game 5 define tu split.',
opts: [
{ t: 'Cerrar los ojos y jugar el game de tu vida', d: { mecanica: 2.2 }, risk: 0.5,
pos: 'Flash + backdoor a los 38 minutos: la gente enloquece y esa jugada se ve en recopilados por años.',
neg: 'Erraste el flash decisivo y el nexus explotó con tu cara en la pantalla gigante. Los memes no perdonan.' },
{ t: 'Jugar conservador, esperar su error', d: { vision: 2.0, sinergia: 1.0 }, risk: 0.3,
pos: 'Los dejaste estrellarse contra tu visionado: ganaste sin arriesgar, el clásico juego de los que saben.',
neg: 'Tan conservador que el rival escaló gratis y te pasó por arriba. El macro no le ganó al macro rival.' },
{ t: 'Pedir el pick que nadie espera', d: { sinergia: 1.6, marca: 1.2 }, risk: 0.45,
pos: 'El pick loco los tomó por sorpresa: ganaste y la comunidad te llena de elogios. Draft gamer.',
neg: 'El pick loco era un meme y lo pagaste caro: el coach ya no te deja elegir ni la música del bootcamp.' },
]},
{ cat: 'momento', title: 'Remontada de 0-2', ctx: 'La serie está 0-2 en contra y el equipo está destruido anímicamente. Solo vos seguís hablando en las calls.',
opts: [
{ t: 'Prender fuego el vestuario con una arenga', d: { sinergia: 2.2, marca: 1.0 }, risk: 0.35,
pos: 'La arenga fue tan buena que el equipo revivió: 3-2 en la serie y todos recuerdan tu speech.',
neg: 'La arenga no alcanzó: perdieron 0-3 y te quedaste gritando en un vestuario vacío. Incómodo.' },
{ t: 'Cambiar todo el plan de juego', d: { vision: 1.8 }, risk: 0.4,
pos: 'El cambio de plan los agarró desprevenidos: el rival no supo reaccionar y la serie se igualó.',
neg: 'Cambiar el plan a mitad de serie fue un caos: perdiste la identidad de juego y la serie también.' },
{ t: 'Aceptar la derrota con dignidad', d: { sinergia: 1.2 }, risk: 0.15,
pos: 'El equipo se fue entero: sin bronca interna y con la cabeza puesta en el próximo split.',
neg: 'Te diste por vencido y el team lo notó: la derrota fue tan silenciosa que dolió más.' },
]},
{ cat: 'momento', title: 'La final se define en el último game', ctx: 'Final de la liga, todo igualado, game 5, 40 minutos de juego. Un solo teamfight define el título. Todos te miran en la llamada.',
opts: [
{ t: 'Llamar el teamfight decisivo', d: { sinergia: 2.4 }, risk: 0.4,
pos: 'Tu call fue perfecta: engañaron al rival y ganaron la final. Te levantan en andas y sos leyenda del split.',
neg: 'La call fue un desastre: entraron 5 contra 4 y perdieron. El título quedó a un click de distancia.' },
{ t: 'Dejar que el veterano decida', d: { vision: 1.8 }, risk: 0.2,
pos: 'El veterano leyó el juego mejor que nadie y el título llegó: aprendiste de los grandes.',
neg: 'El veterano erró la lectura y el equipo lo sintió: "todos la vimos venir", dijo el público.' },
{ t: 'Intentar la jugada individual', d: { mecanica: 2.2 }, risk: 0.5,
pos: 'El 1v1 en el río definió la final: tu nombre queda grabado en la historia de la liga.',
neg: 'Intentaste la jugada heroica y te atraparon en el río: el equipo te la recuerda en cada reunión.' },
]},
{ cat: 'escandalo', title: 'Te ofrecen arreglar un partido', ctx: 'Un tipo que "trabaja para un sitio de apuestas" te pide perder la próxima serie a propósito. Hay plata de por medio y nadie se entera, dice.',
opts: [
{ t: 'Aceptar: la plata es la plata', d: { mecanica: 1.4 }, risk: 0.8,
pos: 'La guita cayó y nadie notó nada... hasta que el movimiento de la casa de apuestas se hizo raro.',
neg: 'Riot abrió investigación: cuenta suspendida, multa de la liga y dos años sin competir. Tu carrera se hizo humo en una serie.' },
{ t: 'Rechazar y denunciarlo', d: { marca: 2.0, sinergia: 0.8 }, risk: 0.1,
pos: 'Lo reportaste a la liga y al staff de integridad: el tipo cayó en cana digital. La comunidad te aplaude en cada stream.',
neg: 'El tipo del arreglo desapareció sin rastro y a vos te quedó la paranoia de mirar por encima del hombro.' },
{ t: 'Fingir que jugás mal, total un par de games', d: { sinergia: -1.2, marca: -1.0 }, risk: 0.65,
pos: 'Perdiste "de casualidad" una serie intrascendente y cobraste. Nadie sospechó, ni tu propio equipo.',
neg: 'Los memes de tu gameplay "raro" se hicieron virales y la liga te citó. La investigación terminó en sanción.' },
]},
{ cat: 'escandalo', title: 'La organización paga tarde', ctx: 'Llevás tres meses de sueldo atrasado. El manager dice que "es tema de la oficina" y los jugadores del equipo arrancan a hablar entre ellos.',
opts: [
{ t: 'Reclamar público y colectivo', d: { marca: 1.6, sinergia: -1.0 }, risk: 0.35,
pos: 'El comunicado conjunto de los jugadores hizo que la directiva apareciera con la plata al toque. Pago total.',
neg: 'La directiva se ofendió: te marcaron, te apartaron del roster titular y la plata llegó igual de tarde.' },
{ t: 'Aguantar y rendir en el server', d: { sinergia: 1.4 }, risk: 0.25,
pos: 'Jugaste como si no te debieran nada: la directiva valoró la actitud y regularizó los pagos en silencio.',
neg: 'Aguantaste tres meses más y la deuda creció. Cuando la org quebró, la plata nunca apareció.' },
{ t: 'Poner las cartas sobre la mesa: me voy', d: { sinergia: 1.0, marca: 1.0 }, risk: 0.3, x: { transfer: 'other' },
pos: 'Tu agente sonó teléfonos y dos equipos te ofrecieron pagarte lo que te deben. Pase con deuda saldada.',
neg: 'La cláusula de salida tenía letra chica: te fuiste, pero te descontaron "gastos administrativos".' },
]},
{ cat: 'escandalo', title: 'Te acusan de account sharing', ctx: 'Un rival te acusa públicamente de haber jugado una serie oficial con otra cuenta. La liga abrió investigación y los periodistas te persiguen.',
opts: [
{ t: 'Demostrar la inocencia con evidencia', d: { marca: 2.2 }, risk: 0.2,
pos: 'Historiales, IPs y VODs en mano: la liga te absolvió y el acusador quedó en offside. Tu reputación sale fortalecida.',
neg: 'La evidencia no convenció a nadie: "la podés haber armado", dicen los foros. La duda te persigue el split entero.' },
{ t: 'Callar y dejar que la liga investigue', d: { sinergia: 0.8 }, risk: 0.45,
pos: 'No alimentaste el circo: la investigación cerró sin cargos y el tema murió en dos semanas.',
neg: 'El silencio se interpretó como culpa: te suspendieron una serie preventiva y el daño a tu imagen quedó.' },
{ t: 'Mostrar todo tu gameplay en stream', d: { marca: 1.8 }, risk: 0.15,
pos: 'Un stream de 12 horas jugando en vivo con cámara: la gente vio que jugás igual que "el de la otra cuenta". Absuelto por la comunidad.',
neg: 'En el stream hiciste una jugada tan idéntica al VOD "sospechoso" que la teoría de la conspiración creció.' },
]},
{ cat: 'escandalo', title: 'La liga favorece al otro equipo', ctx: 'Un replay polémico, un horario cambiado a tu contra, y el equipo rival que siempre la pelea de local. Los fans dicen que la liga "tiene su favorito".',
opts: [
{ t: 'Protesta formal ante la liga', d: { marca: 1.6, sinergia: 0.8 }, risk: 0.35,
pos: 'El reclamo formal se escuchó: te dieron la razón en un punto y la imagen de la liga quedó en veremos.',
neg: 'La protesta cayó en oídos sordos y encima te tildaron de "llorón". Los árbitros te miran raro desde entonces.' },
{ t: 'Ganar igual, calladito', d: { sinergia: 1.4 }, risk: 0.25,
pos: 'No necesitaste que la liga te favorezca: le ganaste al "favorito" en su casa y el público estalló.',
neg: 'Perdiste por detalles y la sensación de injusticia te carcomió el resto del split.' },
{ t: 'Exponerlo en redes', d: { marca: 2.0 }, risk: 0.5,
pos: 'El hilo con los clips se hizo viral: la liga tuvo que salir a aclarar y hasta el patrocinador se anotició.',
neg: 'La liga te multó por "declaraciones públicas contra el circuito" y el equipo tuvo que pagar. Redes apagadas por un mes.' },
]},
{ cat: 'escandalo', title: 'La organización desaparece', ctx: 'El equipo deja de pagar el alquiler de la gaming house. El manager no contesta y el dueño vendió el edificio. La org se está yendo a pique.',
opts: [
{ t: 'Activar contactos ya', d: { sinergia: 1.0 }, risk: 0.25, x: { transfer: 'other' },
pos: 'Tu agente movió el mundo: en una semana tenías dos pruebas en equipos que sí pagan. Salida limpia.',
neg: 'Ningún equipo te quería con el "sello" de una org quebrada. Te quedaste cobrando migajas de un club fantasma.' },
{ t: 'Quedarte hasta el final', d: { sinergia: 1.6, marca: 1.0 }, risk: 0.4,
pos: 'La org se reflotó de pedo con un inversor y valoró a los que se quedaron: te subieron el sueldo y el rol.',
neg: 'La org se disolvió un lunes: te quedaste sin equipo, sin sueldo y con el pasaje pagado por vos. La escena se apiadó en redes.' },
{ t: 'Grabar todo para el documental', d: { marca: 2.2 }, risk: 0.2,
pos: 'El "documental de la crisis" se hizo viral: la gente te bancó y un medio grande te ofreció un programa.',
neg: 'El dueño de la org se sintió traicionado y te bloqueó el traspaso. La venganza tardó pero llegó.' },
]},
{ cat: 'escandalo', title: 'Multa por toxicidad', ctx: 'En una partida oficial te escapó un insulto y el micrófono lo captó. La liga te citó y el clip ya corre por las redes.',
opts: [
{ t: 'Disculpa pública inmediata', d: { marca: 1.4 }, risk: 0.15,
pos: 'Te disculpaste antes de que el clip escale: la multa fue simbólica y la comunidad valoró la humildad.',
neg: 'La disculpa sonó falsa y el clip con tu "contexto" la hizo peor. Ahora tenés un meme propio.' },
{ t: 'Apelar la sanción', d: { marca: 1.0 }, risk: 0.5,
pos: 'La apelación bajó la multa a la mitad: la letra del reglamento estaba de tu lado.',
neg: 'La apelación alargó el tema: te suspendieron un partido más por "no aceptar la sanción".' },
{ t: 'Negarlo todo', d: { sinergia: 0.6 }, risk: 0.6,
pos: 'Sin admisión, la sanción quedó en la mínima: "no se pudo determinar la autoría", dijeron. Les creyeron a medias.',
neg: 'El audio se ecualizó y quedó clarísimo que eras vos: sanción doble por faltar a la verdad.' },
]},
{ cat: 'momento', title: 'DDOS en plena serie', ctx: 'Game 3 de la serie, todo 1-1, y el server se cae a los 20 minutos. La liga no se decide: ¿replay o resultado en mesa?',
opts: [
{ t: 'Exigir el replay completo', d: { sinergia: 1.8 }, risk: 0.3,
pos: 'La liga cedió y se rejugó la partida completa: ganaste el game que te habían "robado". Justicia técnica.',
neg: 'La liga decidió dar el punto al rival "por ventaja adquirida". Reclamaste, pero la mesa no se movió.' },
{ t: 'Aceptar la reanudación desde el momento exacto', d: { vision: 1.6 }, risk: 0.25,
pos: 'La reanudación te favoreció: retomaste con la ventaja que ya tenías y cerraste la serie.',
neg: 'El bug de la reanudación te comió tu mejor ventaja: el rival aprovechó el caos y te dio vuelta la serie.' },
{ t: 'Proponer el "replay express" con un solo game definitorio', d: { mecanica: 1.8 }, risk: 0.45,
pos: 'Un solo game, todo o nada: lo ganaste con una jugada que ya es historia de la liga. Nervios de acero.',
neg: 'El replay express fue un caos de producción: empezó una hora tarde y el público ya se había ido. Perdiste el game y la atención.' },
]},
{ cat: 'estudios', title: 'El contrato leonino', ctx: 'La directiva te pasa la renovación: sueldo lindo, pero con cláusulas raras de salida y derechos de imagen eternos. "Firmá y listo", dicen.',
opts: [
{ t: 'Leerlo con un abogado', d: { vision: 1.6 }, risk: 0.1,
pos: 'El abogado encontró tres cláusulas truchas: las sacaron y el contrato terminó siendo de los mejores de la liga.',
neg: 'El abogado te cobró el 20% de la renovación. La letra chica la pagaste vos en honorarios.' },
{ t: 'Firmar sin leer, total confianza', d: { mecanica: 1.2 }, risk: 0.4,
pos: 'Firmaste y todo bien: la directiva te respetó igual y el contrato era más justo de lo que parecía.',
neg: 'Los derechos de imagen quedaron atados por 10 años: cada stream tuyo, cada marca, todo pasa por la org.' },
{ t: 'Negociar punto por punto', d: { marca: 1.6, sinergia: 0.6 }, risk: 0.3,
pos: 'Negociaste como un veterano: subida salarial por cláusula de salida, y la directiva terminó respetándote más.',
neg: 'Tanto punto por punto que la directiva se cansó: firmaste el original con dos cláusulas peores "de castigo".' },
]},
{ cat: 'preparacion', title: 'La liga achica las plazas', ctx: 'Anunciaron que el circuito reduce cupos el año que viene: los equipos de media tabla juegan por su supervivencia. El tuyo está en el límite.',
opts: [
{ t: 'Campaña para salvar la plaza', d: { marca: 1.6, sinergia: 1.0 }, risk: 0.3,
pos: 'El equipo se unió, los fans armaron bandera y la plaza se salvó: la comunidad te lo agradece en cada partido.',
neg: 'La campaña distrajo al equipo del rendimiento: quedaron últimos y la plaza se perdió igual.' },
{ t: 'Puro juego: la plaza se gana en el server', d: { mecanica: 1.6 }, risk: 0.3,
pos: 'Te enfocaste en rendir y el equipo escaló posiciones: te salvaste por mérito propio, no por campañas.',
neg: 'Rendiste, pero las decisiones se tomaron en oficinas que no miran el server. La plaza se fue igual.' },
{ t: 'Aprovechar para salir del circuito chico', d: { sinergia: 1.0 }, risk: 0.25, x: { transfer: 'other' },
pos: 'Con la plaza en duda, tu agente te consiguió una prueba en una liga más grande: te fuiste justo a tiempo.',
neg: 'La liga grande no te esperaba: la prueba salió mal y volviste a un circuito que ya no tenía tu plaza.' },
]},
{ cat: 'escandalo', title: 'La marca es trucha', ctx: 'Un periodista descubre que el dueño de tu org no tiene los derechos legales del nombre ni del logo del equipo. La liga lo está investigando y el escándalo ya es público.',
opts: [
{ t: 'Ayudar a refundar la marca', d: { marca: 2.0, sinergia: 1.2 }, risk: 0.3,
pos: 'Propuestas para el nuevo nombre, redes renovadas, la refundación salió bien: la gente abraza la nueva era.',
neg: 'La refundación fue un desastre de identidad: la gente ni se entera de que seguís siendo el mismo equipo.' },
{ t: 'Exigir a la directiva que lo arregle', d: { vision: 1.4 }, risk: 0.25,
pos: 'Presionaste y el dueño compró los derechos de una vez: multa paga, marca legal y a seguir.',
neg: 'El dueño no tenía la plata y la liga te multó igual al equipo: descontaron del sueldo de los jugadores.' },
{ t: 'Usar la polémica para moverte', d: { sinergia: 1.0 }, risk: 0.25, x: { transfer: 'other' },
pos: 'Con el escándalo encima, tu agente encontró un club serio que te quiere: saliste de la tormenta con mejora.',
neg: 'Ninguna org seria quiso tocar a un jugador de un club "trucho". Te quedaste hasta que se limpió todo.' },
]},
{ cat: 'escandalo', title: 'El compa que filtra', ctx: 'Un jugador de un equipo rival te contacta: te paga si le pasás el plan de draft de tu equipo para la Promoción/Relegación. "Nadie se entera", te jura.',
opts: [
{ t: 'Rechazar y avisar a tu org', d: { marca: 1.8, sinergia: 0.8 }, risk: 0.12,
pos: 'El staff de integridad lo investigó todo: el rival quedó sancionado y vos sos el héroe de la promoción.',
neg: 'El tipo del contacto desapareció y nadie pudo probar nada. Te quedó la fama de "el que avisa".' },
{ t: 'Aceptar y pasarle el draft real', d: { mecanica: 1.2 }, risk: 0.8,
pos: 'La plata cayó y el plan "se filtró solo". Nadie sospechó... hasta que compararon los pick orders.',
neg: 'Se filtraron los chats: sanción de la liga, veto de por vida y la promoción la jugó tu reemplazo.' },
{ t: 'Aceptar pero pasarle el draft falso', d: { vision: 1.8 }, risk: 0.4,
pos: 'El rival armó su plan sobre un draft que nunca ibas a jugar: los humillaste en la promoción con el plan real.',
neg: 'El tipo sospechó que lo estafabas y filtró la conversación igual: tuviste que explicar mucho y mal.' },
]},
{ cat: 'escandalo', title: 'El compa acusado', ctx: 'Tu compañero de equipo apareció en un hilo de denuncias graves en redes. La org no sabe qué hacer y los sponsors llaman preocupados.',
opts: [
{ t: 'Hablar con la directiva para que actúe', d: { marca: 1.6, sinergia: 0.6 }, risk: 0.2,
pos: 'La org desvinculó al compa de inmediato y el equipo salió ileso del escándalo. Tomaste la postura correcta.',
neg: 'Te tildaron de traidor en el vestuario: los que quedaron te miran raro y el clima se enrareció.' },
{ t: 'Mantener distancia y no opinar', d: { sinergia: 1.0 }, risk: 0.35,
pos: 'No alimentaste el circo: la org manejó todo en silencio y tu carrera siguió sin manchas.',
neg: 'Tu silencio se leyó mal: la comunidad te puso en el "grupo de los que callan". La marca lo sintió.' },
{ t: 'Pedir al equipo un comunicado firme', d: { marca: 1.4 }, risk: 0.3,
pos: 'El comunicado del plantel marcó postura y la comunidad lo valoró: el equipo salió fortalecido.',
neg: 'El comunicado salió ambiguo y todos los tomaron para el lado que no era. Doble daño.' },
]},
{ cat: 'escandalo', title: 'El pacto del primer blood', ctx: 'Una casa de apuestas te ofrece plata si pactás la primera sangre de la serie. "Es inofensivo, nadie nota la diferencia", te dicen.',
opts: [
{ t: 'Reportarlo a integridad', d: { marca: 2.0, sinergia: 0.6 }, risk: 0.1,
pos: 'El staff de integridad armó una trampa y cazó a la red completa. Vos quedaste como el jugador limpio de la liga.',
neg: 'El contacto era de una cuenta falsa de Riot testeando tu honestidad: te felicitaron, pero te hicieron pasar el susto.' },
{ t: 'Pactar el blood y listo', d: { mecanica: 1.2 }, risk: 0.85,
pos: 'Un blood de más o de menos: la guita cayó y nadie se dio cuenta. (Nadie, por ahora.)',
neg: 'Las casas de apuestas detectaron el movimiento anómalo: veto permanente de la liga y tu carrera terminó en la penumbra.' },
{ t: 'Pactarlo "a medias" con tu jungla', d: { sinergia: 1.0 }, risk: 0.55,
pos: 'Lo pactaron tan sutil que hasta los casters lo elogiaron como "lectura perfecta". El negocio salió redondo.',
neg: 'El jungla lo contó en un stream borracho: dos vetos de por vida y el video es historia de la liga.' },
]},
{ cat: 'escandalo', title: 'El ring del clasificatorio', ctx: 'Un pro de una liga grande te pide que juegues la cuenta de un equipo menor en la clasificatoria. "Es plata fácil y nadie se entera".',
opts: [
{ t: 'Decir que no, obvio', d: { marca: 1.6, sinergia: 0.6 }, risk: 0.1,
pos: 'Le cerraste la puerta al tipo y se corrió la voz: te respetan como el que no se vende.',
neg: 'El pro se ofendió y te tildó de "amargo" en una entrevista. La gente le creyó a él.' },
{ t: 'Aceptar jugar "solo un game"', d: { mecanica: 1.6 }, risk: 0.75,
pos: 'Un game, una clasificatoria asegurada, plata en el bolsillo. Nadie miró, dicen.',
neg: 'La liga comparó IPs y horarios: sancionado el equipo menor, el pro y vos. Suspensión de un año.' },
{ t: 'Exigir plata por adelantado', d: { marca: 1.2 }, risk: 0.6,
pos: 'Cobraste por adelantado y al final no jugaste: plata gratis y el "amigo" nunca más te habló.',
neg: 'El pro te denunció por "estafa": ahora los dos están en el expediente de la liga. Nadie sale bien.' },
]},
{ cat: 'escandalo', title: 'Soborno en la final de ascenso', ctx: 'El dueño de un club rival te ofrece plata por "no aparecer enchufado" en la final de ascenso. Tu equipo depende de ese partido.',
opts: [
{ t: 'Contarlo todo a tu club', d: { marca: 2.0 }, risk: 0.1,
pos: 'La denuncia hizo caer al club rival y la final se jugó con la cancha a tu favor: ascendiste limpio.',
neg: 'El dueño negó todo y la liga no encontró pruebas: quedó como "acusación sin sustento".' },
{ t: 'Agarrar la plata y jugar mal "de casualidad"', d: { sinergia: -1.4 }, risk: 0.8,
pos: 'Jugaste el peor game de tu vida sin que nadie lo notara del todo. La guita cayó, pero el peso quedó.',
neg: 'Tu "casualidad" fue tan evidente que hasta tu madre lo vio: la liga revisó las cintas y cayó todo.' },
{ t: 'Usar la oferta para negociar tu futuro', d: { marca: 1.4, sinergia: 0.8 }, risk: 0.3,
pos: 'Le dijiste que no al soborno y le vendiste a tu club la jugada: te subieron el sueldo y te blindaron el puesto.',
neg: 'El dueño del rival se sintió extorsionado y te denunció a la liga por "pedir coimas". La verdad salió, pero el mambo ya estaba.' },
]},
{ cat: 'preparacion', title: 'La gaming house de la miseria', ctx: 'La gaming house tiene una sola silla rota, internet que se corta a las 6 PM y humedad en las paredes. Los jugadores ya armaron grupo de quejas.',
opts: [
{ t: 'Liderar el reclamo de condiciones', d: { sinergia: 1.8 }, risk: 0.25,
pos: 'El reclamo firmado por todo el plantel movió a la directiva: gaming house nueva y el rendimiento subió solo.',
neg: 'La directiva tomó el reclamo como rebelión: te sacaron del roster "por actitud" y el internet siguió igual.' },
{ t: 'Pagarte vos el internet y la silla', d: { mecanica: 1.4 }, risk: 0.15,
pos: 'Inversión personal en tu setup: jugás cómodo y tu rendimiento no depende de la miseria de la casa.',
neg: 'Pagaste todo de tu bolsillo y la org lo dio por sentado: ahora la heladera también es "tuya".' },
{ t: 'Quejarte en el stream', d: { marca: 1.6 }, risk: 0.35,
pos: 'El clip del techo con humedad se hizo viral: la directiva se vio obligada a arreglar todo en una semana.',
neg: 'La directiva te sancionó por "dañar la imagen del club" y los fans se dividieron: tu lado ganó, pero con costo.' },
]},
{ cat: 'momento', title: 'El estudio sin gente', ctx: 'La liga mudó el estudio a otra ciudad con la tribuna vacía: partidos en silencio de biblioteca. La afición se aleja y la producción se resiente.',
opts: [
{ t: 'Pedir jugar los partidos con público en tu ciudad', d: { marca: 1.6 }, risk: 0.3,
pos: 'La liga cedió un evento con público: la final se jugó llena y la audiencia volvió a crecer. La gente te lo agradece.',
neg: 'La liga te ignoró y el tema quedó en un hilo de Twitter que nadie leyó. El silencio siguió.' },
{ t: 'Adaptarte al teatro vacío', d: { sinergia: 1.4 }, risk: 0.15,
pos: 'Jugaste mejor sin presión: el silencio te favoreció y tu rendimiento en el "estudio fantasma" fue top.',
neg: 'El silencio te comió la motivación: jugabas sin energía, como si nada importara.' },
{ t: 'Exigir el cambio de sede en la asamblea', d: { marca: 1.2, sinergia: 0.8 }, risk: 0.35,
pos: 'Vos y otros jugadores plantaron el reclamo: la liga escuchó y rearmó el calendario con sedes locales.',
neg: 'El plantel se dividió entre los que querían y los que no: la asamblea terminó en una pelea que no resolvió nada.' },
]},
];
/* ---------- COMPETENCIAS INTERNACIONALES ---------- */
const COMPETITIONS = {
@@ -1336,6 +1718,8 @@ function doSplitEnd(res){
if (G.stats.farmeo >= 60) award('Rey del Farmeo', '🌾');
if (G.penta >= 1) award('Carry Definitivo', '💥');
if (G.stats.marca >= 58) award('Community Darling', '❤️');
if (G.split <= 3 && rating() >= 64) award('Mejor Novato', '🌱');
if (rating() >= 85) award('Jugador del Año', '👑');
if (G.split >= G.totalSplits){ finishCareer(); } else { nextSplit(); }
}
@@ -1489,36 +1873,36 @@ function exportImage(){
});
y += 2 * (bh + gap) + 30;
ctx.fillStyle = mut; ctx.font = font(22, 700); center('ELO MÁXIMO');
ctx.fillStyle = gold; ctx.font = font(110, 800); y += 52; center(String(G.ratingMax));
ctx.fillStyle = gold; ctx.font = font(110, 800); y += 96; center(String(G.ratingMax));
ctx.font = font(22, 700); ctx.fillStyle = mut; y += 46; center('VALOR MÁXIMO');
ctx.fillStyle = gold2; ctx.font = font(44, 800); y += 50; center(fmtMoney(G.maxValue));
ctx.fillStyle = gold2; ctx.font = font(44, 800); y += 56; center(fmtMoney(G.maxValue));
y += 50;
ctx.fillStyle = mut; ctx.font = font(22, 700); center('EQUIPOS EN LA CARRERA');
y += 14;
const teamRows = G.teams.map(t => `${t.name} (Splits ${t.from}${t.to}, ${t.div === 1 ? '1ª' : '2ª'})`);
ctx.fillStyle = txt; ctx.font = font(26, 700);
for (const row of teamRows.slice(0, 6)){ y += 40; center(row); }
if (teamRows.length > 6){ ctx.fillStyle = mut; ctx.font = font(20, 600); y += 36; center(`+ ${teamRows.length - 6} equipos más`); }
y += 46;
for (const row of teamRows.slice(0, 6)){ y += 34; center(row); }
if (teamRows.length > 6){ ctx.fillStyle = mut; ctx.font = font(20, 600); y += 30; center(`+ ${teamRows.length - 6} equipos más`); }
y += 40;
ctx.fillStyle = mut; ctx.font = font(22, 700); center('TÍTULOS');
const allTitles = G.teamTitles.concat(G.indTitles);
ctx.font = font(24, 600); y += 14;
if (!allTitles.length){ y += 38; ctx.fillStyle = mut; center('Ninguno. La gloria no era para hoy.'); }
if (!allTitles.length){ y += 32; ctx.fillStyle = mut; center('Ninguno. La gloria no era para hoy.'); }
else {
for (const t of allTitles.slice(0, 6)){ y += 40; ctx.fillStyle = gold2; center(`${t.ic} ${t.name}`); }
if (allTitles.length > 6){ ctx.fillStyle = mut; ctx.font = font(20, 600); y += 36; center(`+ ${allTitles.length - 6} más`); }
for (const t of allTitles.slice(0, 6)){ y += 34; ctx.fillStyle = gold2; center(`${t.ic} ${t.name}`); }
if (allTitles.length > 6){ ctx.fillStyle = mut; ctx.font = font(20, 600); y += 30; center(`+ ${allTitles.length - 6} más`); }
}
y += 46;
y += 40;
ctx.fillStyle = mut; ctx.font = font(22, 700); center('INTERNACIONAL');
ctx.font = font(24, 600); y += 14;
if (!G.intl.length){ y += 38; ctx.fillStyle = mut; center('Nunca salió del server regional'); }
if (!G.intl.length){ y += 32; ctx.fillStyle = mut; center('Nunca salió del server regional'); }
else {
for (const t of G.intl){ y += 40; ctx.fillStyle = t.result === 'campeón' ? gold2 : txt; center(`${intlIcon(t.result)} ${t.name}: ${t.result}`); }
for (const t of G.intl){ y += 34; ctx.fillStyle = t.result === 'campeón' ? gold2 : txt; center(`${intlIcon(t.result)} ${t.name}: ${t.result}`); }
}
y += 56;
y += 44;
ctx.strokeStyle = 'rgba(217,115,10,.4)'; ctx.beginPath(); ctx.moveTo(120, y); ctx.lineTo(W - 120, y); ctx.stroke();
ctx.fillStyle = mut; ctx.font = font(24, 600); y += 44; center(`${G.nick || 'Leyenda'} · Fin de la carrera`);
ctx.font = font(20, 600); y += 34; center('Jugá la tuya: lolero.cbcren.online');
ctx.fillStyle = mut; ctx.font = font(24, 600); y += 40; center(`${G.nick || 'Leyenda'} · Fin de la carrera`);
ctx.font = font(20, 600); y += 32; center('Jugá la tuya: lolero.cbcren.online');
cv.toBlob(blob => {
if (!blob){ toast('No se pudo exportar :('); return; }
const a = document.createElement('a');
+223
View File
@@ -0,0 +1,223 @@
"""E2E smoke test for 1v9 — Convertite en Leyenda.
Juega carreras completas con un bot y verifica:
- 0 errores de consola / 0 requests fallidos
- resumen alcanzado en 100% de las carreras
- sin overflow horizontal (desktop + mobile)
- timeline colapsada en mobile
- design tokens correctos
- gating: 2ª división no ve competencias internacionales
- fichajes solo desde split >= 4
- stats sin NaN ni claves fantasma
- diversidad: las 5 competencias aparecen en N carreras
- export de imagen 1080x1920 ¡lido (PIL)
"""
import asyncio
import json
import sys
import random
from playwright.async_api import async_playwright
BASE = "http://127.0.0.1:8899/"
async def pick_region_role(page):
cards = page.locator(".region-card")
n = await cards.count()
idx = random.randrange(n)
await cards.nth(idx).click()
cards = page.locator(".role-card")
n = await cards.count()
await cards.nth(random.randrange(n)).click()
async def play_career(page, want_export=False, max_rounds=60):
"""Juega una carrera completa. Devuelve dict con datos de la partida."""
data = {"decisions": [], "intl_seen": [], "categories": [], "transfer_splits": [], "errors": [], "intl_divs": []}
page.on("pageerror", lambda e: data["errors"].append(f"pageerror: {e}"))
page.on("console", lambda m: data["errors"].append(f"console: {m.text}") if m.type == "error" else None)
page.on("requestfailed", lambda r: data["errors"].append(f"requestfailed: {r.url}"))
await page.goto(BASE, wait_until="networkidle")
await page.click("#btnStart")
await pick_region_role(page)
await page.fill("#inpNick", "BotPro")
await page.click("#btnBegin")
await page.click(".opt-card:visible >> nth=0") # oferta de academia
rounds = 0
while rounds < max_rounds:
rounds += 1
summary_active = await page.evaluate("document.getElementById('summary').classList.contains('active')")
if summary_active:
break
overlay = await page.evaluate("document.getElementById('resultOverlay').classList.contains('show')")
if overlay:
cont = page.locator("#btnContinue")
if await cont.count():
await cont.click()
continue
# nueva decisión renderizada
cat = await page.locator(".dc-cat").first.text_content()
if cat:
data["categories"].append(cat.strip())
if "Fichaje" in cat:
split = await page.evaluate("G.split")
data["transfer_splits"].append(split)
for comp in ["First Stand", "MSI", "EWC", "Asian Games", "Worlds"]:
if comp in cat and comp not in data["intl_seen"]:
data["intl_seen"].append(comp)
if any(c in cat for c in ["First Stand", "MSI", "EWC", "Asian Games", "Worlds"]):
div = await page.evaluate("G.div")
data["intl_divs"].append(div)
opts = page.locator(".opt-card:visible")
n = await opts.count()
if not n:
await page.wait_for_timeout(300)
continue
# 40% de retirarse cuando esté disponible (split >= 6) para acelerar
retire = page.locator('.opt-card[data-i="retire"]:visible')
if await retire.count() and random.random() < 0.4:
await retire.click()
else:
await opts.nth(random.randrange(n)).click()
await page.wait_for_timeout(120)
state = await page.evaluate("""() => ({
split: G.split, totalSplits: G.totalSplits, retired: G.retired, div: G.div,
stats: G.stats, teamId: G.teamId, intl: G.intl, teamTitles: G.teamTitles,
indTitles: G.indTitles, history: G.history, games: G.games,
kills: G.kills, assists: G.assists, deaths: G.deaths, penta: G.penta,
maxValue: G.maxValue, ratingMax: G.ratingMax, regSelect: G.regSelect
})""")
data["state"] = state
if want_export:
async with page.expect_download(timeout=10000) as dl_info:
await page.click("#btnExport")
dl = await dl_info.value
path = await dl.path()
data["download"] = path
return data
async def check_layout(page, is_mobile):
"""Chequeos de layout programáticos."""
res = {}
res["overflow"] = await page.evaluate(
"document.documentElement.scrollWidth > window.innerWidth + 1"
)
res["tl_closed_mobile"] = await page.evaluate(
"""() => {
const b = document.getElementById('tlBody');
return !b || b.classList.contains('closed');
}"""
) if is_mobile else None
res["tokens"] = await page.evaluate(
"""() => {
const s = getComputedStyle(document.documentElement);
return {
accent: s.getPropertyValue('--accent-ovr').trim(),
success: s.getPropertyValue('--success').trim(),
bg: s.getPropertyValue('--bg-base').trim()
};
}"""
)
return res
def check_state(data):
"""Verifica mecánicas sobre el estado final de una carrera."""
issues = []
st = data["state"]
for k, v in st["stats"].items():
if v != v or v is None: # NaN check
issues.append(f"stat NaN: {k}")
ghost = [k for k in st["stats"] if k in ("valor",)]
if ghost:
issues.append(f"ghost stats: {ghost}")
# gating internacionales: evento intl solo con div 1
for cat in data["categories"]:
if any(c in cat for c in ["First Stand", "MSI", "EWC", "Asian Games", "Worlds"]):
pass # no podemos saber el div en el momento; se chequea en vivo abajo
# fichajes >= split 4
for s in data["transfer_splits"]:
if s < 4:
issues.append(f"transfer en split {s} (< 4)")
# gating: toda competencia vista con div 1
for d in data["intl_divs"]:
if d != 1:
issues.append(f"competencia internacional vista en div {d}")
return issues
def validate_export(path):
"""Valida el PNG exportado con PIL."""
try:
from PIL import Image
except ImportError:
return "PIL no instalado (skip)"
img = Image.open(path)
size = img.size
mode = img.mode
px = img.convert("RGB").getpixel((100, 100))
luma = img.convert("L").getextrema()
ok = size == (1080, 1920) and luma[1] > 60
return f"OK size={size} mode={mode} bg={px} luma={luma}" if ok else f"FAIL size={size} mode={mode} bg={px} luma={luma}"
async def main():
results = {"errors_total": 0, "careers": [], "intl_all": set(), "export_ok": None}
n_careers = int(sys.argv[1]) if len(sys.argv) > 1 else 6
async with async_playwright() as p:
browser = await p.chromium.launch()
# --- desktop ---
page = await browser.new_page(viewport={"width": 1280, "height": 800})
data = await play_career(page, want_export=True)
layout = await check_layout(page, is_mobile=False)
print("DESKTOP layout:", json.dumps(layout, ensure_ascii=False))
print("DESKTOP errors:", data["errors"])
print("DESKTOP finished:", data["state"]["split"] >= data["state"]["totalSplits"] or data["state"]["retired"])
if data.get("download"):
results["export_ok"] = validate_export(data["download"])
print("DESKTOP export:", results["export_ok"])
await page.close()
# --- mobile (390x844) + N carreras ---
for i in range(n_careers):
page = await browser.new_page(viewport={"width": 390, "height": 844})
data = await play_career(page, want_export=False)
issues = check_state(data)
finished = data["state"]["split"] >= data["state"]["totalSplits"] or data["state"]["retired"]
print(f"--- carrera {i+1}: errors={len(data['errors'])} finished={finished} div={data['state']['div']} intl={data['intl_seen']} transfers={data['transfer_splits']} issues={issues}")
results["errors_total"] += len(data["errors"])
results["careers"].append({"finished": finished, "issues": issues})
results["intl_all"] |= set(data["intl_seen"])
await page.close()
# layout mobile una vez
page = await browser.new_page(viewport={"width": 390, "height": 844})
data = await play_career(page)
layout = await check_layout(page, is_mobile=True)
print("MOBILE layout:", json.dumps(layout, ensure_ascii=False))
await page.close()
await browser.close()
ok = results["errors_total"] == 0 and all(c["finished"] for c in results["careers"])
print("\n=== RESUMEN ===")
print("errores totales:", results["errors_total"])
print("carreras completadas:", sum(1 for c in results["careers"] if c["finished"]), "/", len(results["careers"]))
print("issue por carrera:", [c["issues"] for c in results["careers"]])
print("competencias vistas:", sorted(results["intl_all"]))
print("export:", results["export_ok"])
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(asyncio.run(main()))
Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB