{"id":39,"date":"2026-09-22T15:20:34","date_gmt":"2026-09-22T15:20:34","guid":{"rendered":"https:\/\/ericguerrero.es\/?page_id=39"},"modified":"2026-09-23T07:32:32","modified_gmt":"2026-09-23T07:32:32","slug":"obra-final-lab","status":"publish","type":"page","link":"https:\/\/ericguerrero.es\/?page_id=39","title":{"rendered":"Como piensa un enemigo &#8211; Laboratorio Interactivo"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"39\" class=\"elementor elementor-39 elementor-bc-flex-widget\" data-elementor-post-type=\"page\">\n\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-sec_lab_arcade elementor-section-full_width elementor-section-height-default elementor-section-height-default\" data-id=\"sec_lab_arcade\" data-element_type=\"section\" data-e-type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-col_lab_arcade\" data-id=\"col_lab_arcade\" data-element_type=\"column\" data-e-type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-w_lab_arcade_html elementor-widget elementor-widget-html\" data-id=\"w_lab_arcade_html\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t\n<style>\n  @import url('https:\/\/fonts.googleapis.com\/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap');\n\n  \/* Global Container & Anti-Overflow Rules *\/\n  html, body {\n    overflow-x: hidden;\n    max-width: 100vw;\n    background-color: #f4f1ea;\n    background-image: radial-gradient(#dfd9cd 1px, transparent 1px);\n    background-size: 24px 24px;\n    background-attachment: fixed;\n  }\n\n  \/* Fondo tipo mesa de trabajo \/ tapete inmovil de puntos (background-attachment: fixed nativo) *\/\n  .cpue-lab {\n    position: relative;\n    font-family: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif;\n    background-color: #f4f1ea;\n    background-image: radial-gradient(#dfd9cd 1px, transparent 1px);\n    background-size: 24px 24px;\n    background-attachment: fixed;\n    color: #2b2927;\n    line-height: 1.65;\n    font-size: 16px;\n    padding: 0;\n    margin: 0;\n    -webkit-font-smoothing: antialiased;\n    overflow-x: hidden;\n    width: 100%;\n    box-sizing: border-box;\n  }\n  .cpue-lab * { box-sizing: border-box; margin: 0; padding: 0; }\n\n  \/* Allow text selection everywhere on the page *\/\n  .cpue-lab p, .cpue-lab h1, .cpue-lab h2, .cpue-lab h3, .cpue-lab h4, .cpue-lab span, .cpue-lab div {\n    user-select: text;\n    -webkit-user-select: text;\n  }\n\n  \/* Prevent image dragging *\/\n  .cpue-lab img {\n    user-select: none;\n    -webkit-user-select: none;\n    -webkit-user-drag: none;\n    pointer-events: none;\n  }\n\n  .cpue-lab h1, .cpue-lab h2, .cpue-lab h3, .cpue-lab h4 {\n    font-weight: 800;\n    letter-spacing: -0.02em;\n    color: #171615;\n  }\n  .cpue-lab .mono {\n    font-family: 'JetBrains Mono', monospace;\n  }\n\n  \/* ========================================================\n     HEADER\n     ======================================================== *\/\n  .lab-header {\n    background: #ffffff;\n    border-bottom: 2px solid #2b2927;\n    padding: 48px 24px 36px;\n    width: 100%;\n  }\n  .lab-header-inner {\n    max-width: 1040px;\n    margin: 0 auto;\n  }\n  .lab-tagline {\n    display: inline-block;\n    background: #e8e3d5;\n    padding: 4px 10px;\n    border-radius: 4px;\n    font-size: 12px;\n    font-weight: 700;\n    text-transform: uppercase;\n    letter-spacing: 0.08em;\n    color: #635d52;\n    margin-bottom: 12px;\n  }\n  .lab-header h1 {\n    font-size: clamp(1.85rem, 4.5vw, 3.2rem);\n    line-height: 1.15;\n    margin-bottom: 14px;\n  }\n  .lab-header p {\n    font-size: clamp(0.98rem, 2vw, 1.15rem);\n    color: #5c574e;\n    max-width: 720px;\n    line-height: 1.6;\n  }\n\n  \/* ========================================================\n     LAYOUT WRAPPER & CARDS\n     ======================================================== *\/\n  .lab-wrapper {\n    max-width: 1040px;\n    margin: 0 auto;\n    padding: 40px 24px 80px;\n    width: 100%;\n  }\n\n  .lab-module {\n    background: #ffffff;\n    border: 2px solid #2b2927;\n    border-radius: 12px;\n    box-shadow: 4px 4px 0px #2b2927;\n    padding: 28px;\n    margin-bottom: 40px;\n    width: 100%;\n  }\n  .lab-module-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: baseline;\n    border-bottom: 1.5px solid #eae5d8;\n    padding-bottom: 14px;\n    margin-bottom: 20px;\n    flex-wrap: wrap;\n    gap: 10px;\n  }\n  .lab-module-title {\n    font-size: clamp(1.15rem, 2.5vw, 1.45rem);\n    font-weight: 800;\n  }\n  .lab-module-badge {\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 11.5px;\n    font-weight: 700;\n    background: #2b2927;\n    color: #fff;\n    padding: 4px 10px;\n    border-radius: 4px;\n    white-space: nowrap;\n  }\n\n  \/* Token tags *\/\n  .tag {\n    display: inline-flex;\n    align-items: center;\n    gap: 4px;\n    padding: 2px 7px;\n    border-radius: 4px;\n    font-size: 0.88em;\n    font-weight: 700;\n    font-family: 'JetBrains Mono', monospace;\n    white-space: nowrap;\n  }\n  .tag-blinky { background: #fee2e2; color: #b91c1c; border: 1px solid #fca5a5; }\n  .tag-pinky  { background: #fce7f3; color: #be185d; border: 1px solid #f9a8d4; }\n  .tag-inky   { background: #e0f2fe; color: #0369a1; border: 1px solid #7dd3fc; }\n  .tag-clyde  { background: #fef3c7; color: #b45309; border: 1px solid #fcd34d; }\n  .tag-pacman { background: #fef9c3; color: #854d0e; border: 1px solid #fde047; }\n\n  \/* ========================================================\n     MODULE 1: PAC-MAN RESPONSIVE LAYOUT\n     ======================================================== *\/\n  .lab-grid-container {\n    display: grid;\n    grid-template-columns: 468px 1fr;\n    gap: 28px;\n    align-items: start;\n    width: 100%;\n  }\n\n  .canvas-focus-wrapper {\n    position: relative;\n    border-radius: 10px;\n    padding: 8px;\n    background: #181b24;\n    border: 2px solid #2b2927;\n    transition: all 0.2s ease;\n    outline: none;\n    box-shadow: 3px 3px 0px #2b2927;\n    max-width: 468px;\n    width: 100%;\n    margin: 0 auto;\n  }\n  .canvas-focus-wrapper:focus-within, .canvas-focus-wrapper.active-focus {\n    border-color: #facc15;\n    box-shadow: 0 0 0 3px rgba(250, 204, 21, 0.35), 4px 4px 0px #2b2927;\n  }\n\n  #lab-canvas, #mgs-canvas {\n    user-select: none;\n    -webkit-user-select: none;\n    -webkit-user-drag: none;\n    touch-action: none;\n  }\n\n  #lab-canvas {\n    display: block;\n    width: 100%;\n    max-width: 448px;\n    height: auto;\n    aspect-ratio: 448 \/ 496;\n    background: #080a10;\n    border-radius: 6px;\n    cursor: crosshair;\n    margin: 0 auto;\n    border: 1px solid #1e2538;\n    user-select: none;\n    -webkit-user-select: none;\n    -webkit-user-drag: none;\n    touch-action: none;\n  }\n\n  \/* Toolbar buttons *\/\n  .lab-toolbar {\n    display: flex;\n    gap: 8px;\n    margin-top: 10px;\n    flex-wrap: wrap;\n    width: 100%;\n  }\n  .tool-btn {\n    font-family: inherit;\n    font-size: 12.5px;\n    font-weight: 700;\n    padding: 8px 13px;\n    border-radius: 6px;\n    border: 1.5px solid #2b2927;\n    background: #fff;\n    color: #2b2927;\n    cursor: pointer;\n    transition: all 0.15s ease;\n    box-shadow: 2px 2px 0px #2b2927;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n  }\n  .tool-btn:hover { background: #f5efe6; transform: translateY(-1px); }\n  .tool-btn:active { transform: translateY(1px); box-shadow: 1px 1px 0px #2b2927; }\n  .tool-btn.active {\n    background: #2b2927;\n    color: #fff;\n    box-shadow: 2px 2px 0px #000;\n  }\n\n  \/* Formula explanation box *\/\n  .lab-formula-box {\n    background: #faf8f5;\n    border: 1.5px solid #2b2927;\n    border-radius: 8px;\n    padding: 18px;\n    box-shadow: 2px 2px 0px #2b2927;\n    width: 100%;\n  }\n  .lab-formula-row {\n    margin-bottom: 12px;\n    padding: 8px 10px;\n    border-radius: 6px;\n    transition: all 0.15s ease;\n    border: 1px solid transparent;\n  }\n  .lab-formula-row:hover {\n    background: #f1ede4;\n    border-color: #d6cfc2;\n  }\n  .lab-formula-row:last-of-type {\n    margin-bottom: 0;\n  }\n  .formula-title {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    font-weight: 700;\n    margin-bottom: 4px;\n    gap: 8px;\n    flex-wrap: wrap;\n  }\n  .formula-math {\n    font-size: 13px;\n    color: #555;\n    line-height: 1.5;\n  }\n\n  \/* Expandable Ghost Deep Dive Panel *\/\n  .ghost-deep-dive {\n    margin-top: 16px;\n    background: #ffffff;\n    border: 1.5px solid #2b2927;\n    border-radius: 8px;\n    padding: 16px;\n    box-shadow: 2px 2px 0px #2b2927;\n    display: none;\n    width: 100%;\n  }\n  .ghost-deep-dive.active {\n    display: block;\n    animation: fadeIn 0.2s ease;\n  }\n  @keyframes fadeIn {\n    from { opacity: 0; transform: translateY(-4px); }\n    to { opacity: 1; transform: translateY(0); }\n  }\n  .ghost-card {\n    background: #faf8f5;\n    border: 1px solid #d6cfc2;\n    border-radius: 6px;\n    padding: 12px 14px;\n    margin-bottom: 12px;\n    transition: all 0.15s ease;\n  }\n  .ghost-card:last-child {\n    margin-bottom: 0;\n  }\n  .ghost-card.highlighted {\n    border-color: #2b2927;\n    background: #fdfbf7;\n    box-shadow: 2px 2px 0px #2b2927;\n  }\n  .ghost-card-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: 6px;\n    flex-wrap: wrap;\n    gap: 6px;\n  }\n  .ghost-card-formula {\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 11.5px;\n    background: #f1ede4;\n    padding: 4px 8px;\n    border-radius: 4px;\n    margin-bottom: 6px;\n    color: #171615;\n    border: 1px solid #e2ddd3;\n    display: inline-block;\n    word-break: break-word;\n  }\n  .ghost-card-desc {\n    font-size: 12.5px;\n    color: #444;\n    line-height: 1.45;\n  }\n  .ghost-card-pedagogy {\n    font-size: 12px;\n    color: #0369a1;\n    background: #f0f9ff;\n    border-left: 3px solid #0ea5e9;\n    padding: 6px 8px;\n    margin-top: 6px;\n    border-radius: 0 4px 4px 0;\n    line-height: 1.4;\n  }\n\n  \/* ========================================================\n     MODULE 2: METAL GEAR SOLID (1998) STEALTH FSM SIMULATOR\n     ======================================================== *\/\n  .mgs-sim-container {\n    display: grid;\n    grid-template-columns: 1.15fr 1fr;\n    gap: 22px;\n    align-items: start;\n    margin-top: 16px;\n    width: 100%;\n  }\n  .mgs-viewport-col {\n    display: flex;\n    flex-direction: column;\n    gap: 10px;\n    width: 100%;\n  }\n  .mgs-canvas-wrapper {\n    position: relative;\n    background: #0f172a;\n    border: 2px solid #2b2927;\n    border-radius: 8px;\n    box-shadow: 3px 3px 0px #2b2927;\n    overflow: hidden;\n    width: 100%;\n    max-width: 480px;\n    margin: 0 auto;\n  }\n  #mgs-canvas {\n    display: block;\n    width: 100%;\n    max-width: 480px;\n    height: auto;\n    aspect-ratio: 480 \/ 340;\n    touch-action: none;\n    cursor: crosshair;\n    user-select: none;\n    -webkit-user-select: none;\n    -webkit-user-drag: none;\n  }\n  .mgs-status-bar {\n    position: absolute;\n    top: 8px;\n    left: 8px;\n    right: 8px;\n    display: flex;\n    justify-content: space-between;\n    pointer-events: none;\n    z-index: 2;\n    gap: 4px;\n    flex-wrap: wrap;\n  }\n  .mgs-hud-badge {\n    background: rgba(15, 23, 42, 0.90);\n    border: 1px solid #334155;\n    color: #94a3b8;\n    padding: 3px 8px;\n    border-radius: 4px;\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 11px;\n    font-weight: 700;\n    transition: all 0.2s ease;\n    white-space: nowrap;\n  }\n  .mgs-actions {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 8px;\n    width: 100%;\n    max-width: 480px;\n    margin: 0 auto;\n  }\n  .mgs-btn {\n    font-family: inherit;\n    font-size: 12px;\n    font-weight: 700;\n    padding: 7px 12px;\n    border-radius: 6px;\n    border: 1.5px solid #2b2927;\n    background: #ffffff;\n    color: #2b2927;\n    cursor: pointer;\n    box-shadow: 2px 2px 0px #2b2927;\n    transition: all 0.15s ease;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n  }\n  .mgs-btn:hover { background: #fef9c3; transform: translateY(-1px); }\n  .mgs-btn:active { transform: translateY(1px); box-shadow: 1px 1px 0px #2b2927; }\n  .mgs-btn.active-toggle {\n    background: #fde047;\n    border-color: #854d0e;\n    color: #854d0e;\n  }\n  .mgs-hint {\n    font-size: 11.5px;\n    color: #555;\n    line-height: 1.45;\n    background: #f8fafc;\n    border: 1px solid #e2e8f0;\n    padding: 8px 10px;\n    border-radius: 6px;\n    width: 100%;\n    max-width: 480px;\n    margin: 0 auto;\n  }\n\n  .mgs-fsm-col {\n    display: flex;\n    flex-direction: column;\n    gap: 14px;\n    width: 100%;\n  }\n  .mgs-fsm-card {\n    background: #fdfbf7;\n    border: 2px solid #2b2927;\n    border-radius: 8px;\n    padding: 16px;\n    box-shadow: 3px 3px 0px #2b2927;\n    width: 100%;\n  }\n  .mgs-fsm-card-title {\n    font-size: 13px;\n    font-weight: 800;\n    text-transform: uppercase;\n    letter-spacing: 0.05em;\n    color: #2b2927;\n    margin-bottom: 12px;\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n  }\n  .fsm-nodes-grid {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 10px;\n    margin-bottom: 14px;\n    width: 100%;\n  }\n  .fsm-state-card {\n    background: #ffffff;\n    border: 2px solid #cbd5e1;\n    border-radius: 8px;\n    padding: 10px 8px;\n    text-align: center;\n    box-shadow: 2px 2px 0px #cbd5e1;\n    transition: all 0.2s ease;\n  }\n  .fsm-state-card .fsm-card-chip {\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 9px;\n    font-weight: 800;\n    padding: 2px 5px;\n    border-radius: 3px;\n    display: inline-block;\n    margin-bottom: 4px;\n    background: #f1f5f9;\n    color: #64748b;\n  }\n  .fsm-state-card .fsm-card-name {\n    font-weight: 800;\n    font-size: 12.5px;\n    display: block;\n    color: #334155;\n  }\n  .fsm-state-card .fsm-card-sub {\n    font-size: 10px;\n    color: #64748b;\n    margin-top: 2px;\n    display: block;\n    line-height: 1.25;\n  }\n\n  \/* Active state glows *\/\n  .fsm-state-card.active-state.state-patrol {\n    border-color: #16a34a;\n    background: #f0fdf4;\n    box-shadow: 3px 3px 0px #16a34a;\n    transform: scale(1.03);\n  }\n  .fsm-state-card.active-state.state-patrol .fsm-card-name { color: #15803d; }\n  .fsm-state-card.active-state.state-patrol .fsm-card-chip { background: #dcfce7; color: #166534; }\n\n  .fsm-state-card.active-state.state-suspicious {\n    border-color: #ca8a04;\n    background: #fefce8;\n    box-shadow: 3px 3px 0px #ca8a04;\n    transform: scale(1.03);\n  }\n  .fsm-state-card.active-state.state-suspicious .fsm-card-name { color: #a16207; }\n  .fsm-state-card.active-state.state-suspicious .fsm-card-chip { background: #fef08a; color: #854d0e; }\n\n  .fsm-state-card.active-state.state-alert {\n    border-color: #dc2626;\n    background: #fef2f2;\n    box-shadow: 3px 3px 0px #dc2626;\n    transform: scale(1.03);\n  }\n  .fsm-state-card.active-state.state-alert .fsm-card-name { color: #b91c1c; }\n  .fsm-state-card.active-state.state-alert .fsm-card-chip { background: #fee2e2; color: #991b1b; }\n\n  .fsm-state-card.active-state.state-evasion,\n  .fsm-state-card.active-state.state-busqueda {\n    border-color: #ea580c;\n    background: #fff7ed;\n    box-shadow: 3px 3px 0px #ea580c;\n    transform: scale(1.03);\n  }\n  .fsm-state-card.active-state.state-evasion .fsm-card-name,\n  .fsm-state-card.active-state.state-busqueda .fsm-card-name { color: #c2410c; }\n  .fsm-state-card.active-state.state-evasion .fsm-card-chip,\n  .fsm-state-card.active-state.state-busqueda .fsm-card-chip { background: #ffedd5; color: #9a3412; }\n\n  .fsm-slider-group {\n    display: flex;\n    flex-direction: column;\n    gap: 8px;\n    margin-top: 10px;\n    border-top: 1px dashed #cbd5e1;\n    padding-top: 12px;\n  }\n  .fsm-slider-row {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    font-size: 11.5px;\n    font-weight: 600;\n    color: #475569;\n  }\n  .fsm-slider-row input[type=\"range\"] {\n    flex: 1;\n    margin: 0 10px;\n    accent-color: #2b2927;\n  }\n  .fsm-slider-val {\n    font-family: 'JetBrains Mono', monospace;\n    font-weight: 700;\n    min-width: 44px;\n    text-align: right;\n    color: #0f172a;\n  }\n\n  .mgs-codec-log {\n    background: #090d16;\n    color: #38bdf8;\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 11px;\n    line-height: 1.5;\n    padding: 12px 14px;\n    border-radius: 6px;\n    border: 1.5px solid #1e293b;\n    min-height: 54px;\n    word-break: break-word;\n    width: 100%;\n  }\n\n  \/* ========================================================\n     ROADMAP SECTION\n     ======================================================== *\/\n  .cpue-roadmap-section {\n    background: #ffffff;\n    border: 2px solid #2b2927;\n    border-radius: 12px;\n    box-shadow: 4px 4px 0px #2b2927;\n    padding: 32px 28px;\n    margin-bottom: 40px;\n    width: 100%;\n  }\n  .cpue-roadmap-section h2 {\n    font-size: clamp(1.3rem, 3vw, 1.6rem);\n    margin-bottom: 8px;\n  }\n  .cpue-roadmap-section > p {\n    color: #5c574e;\n    font-size: clamp(0.95rem, 2vw, 1.05rem);\n    margin-bottom: 24px;\n  }\n  .roadmap-card {\n    display: grid;\n    grid-template-columns: 90px 1fr auto;\n    gap: 16px;\n    align-items: center;\n    background: #faf8f5;\n    border: 1.5px solid #2b2927;\n    border-radius: 8px;\n    padding: 16px 20px;\n    margin-bottom: 12px;\n    box-shadow: 2px 2px 0px #2b2927;\n    transition: transform 0.15s ease;\n  }\n  .roadmap-card:hover { transform: translateY(-1px); }\n  .roadmap-num {\n    font-family: 'JetBrains Mono', monospace;\n    font-weight: 800;\n    font-size: 13px;\n    color: #78716c;\n  }\n  .roadmap-title {\n    font-weight: 800;\n    font-size: 15px;\n    color: #171615;\n    margin-bottom: 3px;\n  }\n  .roadmap-desc {\n    font-size: 13px;\n    color: #57534e;\n    line-height: 1.45;\n  }\n  .roadmap-badge {\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 11px;\n    font-weight: 700;\n    padding: 3px 8px;\n    border-radius: 4px;\n    text-transform: uppercase;\n    letter-spacing: 0.05em;\n    white-space: nowrap;\n    border: 1.5px solid transparent;\n  }\n  .badge-func { background: #dcfce7; color: #166534; border-color: #86efac; }\n  .badge-dev  { background: #fef9c3; color: #854d0e; border-color: #fde047; }\n  .badge-inv  { background: #f3f4f6; color: #4b5563; border-color: #d1d5db; }\n\n  \/* Footer & Contador de Visualizaciones *\/\n  .lab-footer {\n    text-align: center;\n    border-top: 2px solid #2b2927;\n    padding-top: 30px;\n    font-size: 13px;\n    color: #6a6459;\n    margin-top: 50px;\n  }\n  .lab-counter-container {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    margin-bottom: 20px;\n  }\n  .lab-counter-pill {\n    display: inline-flex;\n    align-items: center;\n    gap: 10px;\n    background: #ffffff;\n    border: 1.5px solid #2b2927;\n    border-radius: 999px;\n    padding: 6px 14px;\n    box-shadow: 2px 2px 0px #2b2927;\n    transition: transform 0.15s ease, box-shadow 0.15s ease;\n  }\n  .lab-counter-pill:hover {\n    transform: translateY(-1px);\n    box-shadow: 3px 3px 0px #2b2927;\n  }\n  .counter-live-dot {\n    width: 8px;\n    height: 8px;\n    background: #16a34a;\n    border-radius: 50%;\n    box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.25);\n    animation: counterPulse 2s infinite ease-in-out;\n    flex-shrink: 0;\n  }\n  @keyframes counterPulse {\n    0%, 100% { transform: scale(1); opacity: 1; }\n    50% { transform: scale(1.35); opacity: 0.6; }\n  }\n  .counter-label {\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 11px;\n    font-weight: 700;\n    color: #44403c;\n    letter-spacing: 0.05em;\n    text-transform: uppercase;\n  }\n  .counter-badge-img {\n    height: 20px;\n    width: auto;\n    display: block;\n    border-radius: 3px;\n    vertical-align: middle;\n  }\n  .counter-fallback {\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 11px;\n    font-weight: 700;\n    background: #2b2927;\n    color: #22c55e;\n    padding: 2px 8px;\n    border-radius: 4px;\n  }\n\n\n  \/* ========================================================\n     WEB AUDIO CONTROLS & BEHAVIOR TREE TEASER STYLES\n     ======================================================== *\/\n  html {\n    scroll-behavior: smooth;\n  }\n  .sound-controls-bar {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    flex-wrap: wrap;\n  }\n  .sound-preset-select {\n    background: #ffffff;\n    border: 1.5px solid #2b2927;\n    border-radius: 999px;\n    padding: 5px 12px;\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 11px;\n    font-weight: 700;\n    color: #2b2927;\n    cursor: pointer;\n    box-shadow: 2px 2px 0px #2b2927;\n    outline: none;\n    transition: all 0.15s ease;\n  }\n  .sound-preset-select:hover {\n    background: #faf8f5;\n    transform: translateY(-1px);\n    box-shadow: 3px 3px 0px #2b2927;\n  }\n  .sound-toggle-btn {\n    display: inline-flex;\n    align-items: center;\n    gap: 8px;\n    background: #ffffff;\n    border: 1.5px solid #2b2927;\n    border-radius: 999px;\n    padding: 6px 14px;\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 11px;\n    font-weight: 700;\n    color: #2b2927;\n    cursor: pointer;\n    box-shadow: 2px 2px 0px #2b2927;\n    transition: all 0.15s ease;\n    text-transform: uppercase;\n    letter-spacing: 0.05em;\n  }\n  .sound-toggle-btn:hover {\n    transform: translateY(-1px);\n    box-shadow: 3px 3px 0px #2b2927;\n    background: #faf8f5;\n  }\n  .sound-toggle-btn.muted {\n    color: #78716c;\n    border-color: #a8a29e;\n    background: #f5f5f4;\n    box-shadow: 1px 1px 0px #a8a29e;\n  }\n\n  \/* Teaser Toggle Button inside Roadmap Card 3 *\/\n  .bt-teaser-btn {\n    display: inline-flex;\n    align-items: center;\n    gap: 6px;\n    background: #f0fdf4;\n    border: 1.5px solid #16a34a;\n    border-radius: 6px;\n    padding: 6px 12px;\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 11px;\n    font-weight: 700;\n    color: #166534;\n    cursor: pointer;\n    box-shadow: 2px 2px 0px #16a34a;\n    transition: all 0.15s ease;\n  }\n  .bt-teaser-btn:hover {\n    background: #dcfce7;\n    transform: translateY(-1px);\n    box-shadow: 3px 3px 0px #16a34a;\n  }\n\n  \/* Behavior Tree Workbench Box *\/\n  .bt-workbench, .bt-teaser-box {\n    margin-top: 18px;\n    background: #ffffff;\n    border: 2px solid #2b2927;\n    border-radius: 10px;\n    padding: 24px;\n    box-shadow: 4px 4px 0px #2b2927;\n  }\n  .bt-teaser-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    flex-wrap: wrap;\n    gap: 14px;\n    border-bottom: 1.5px dashed #dfd9cd;\n    padding-bottom: 16px;\n    margin-bottom: 20px;\n  }\n  .bt-tag {\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 11px;\n    font-weight: 800;\n    color: #854d0e;\n    background: #fef9c3;\n    padding: 3px 8px;\n    border-radius: 4px;\n    border: 1px solid #fde047;\n    display: inline-block;\n    margin-bottom: 6px;\n  }\n  .bt-title {\n    font-size: 16px;\n    font-weight: 800;\n    color: #171615;\n    margin: 0;\n  }\n  .bt-controls {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 8px;\n  }\n  .bt-action-btn {\n    background: #faf8f5;\n    border: 1.5px solid #2b2927;\n    border-radius: 6px;\n    padding: 6px 12px;\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 12px;\n    font-weight: 700;\n    color: #2b2927;\n    cursor: pointer;\n    box-shadow: 2px 2px 0px #2b2927;\n    transition: all 0.15s ease;\n  }\n  .bt-action-btn:hover {\n    background: #f5f0e6;\n    transform: translateY(-1px);\n  }\n  .bt-action-btn.active-sim {\n    background: #2b2927;\n    color: #ffffff;\n  }\n  .bt-action-btn.pulse-btn {\n    background: #e0f2fe;\n    border-color: #0284c7;\n    color: #0369a1;\n    box-shadow: 2px 2px 0px #0284c7;\n  }\n  .bt-action-btn.pulse-btn:hover {\n    background: #bae6fd;\n  }\n\n  \/* Tree Nodes Visual Hierarchy *\/\n  .bt-tree-layout {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    width: 100%;\n  }\n  .bt-level {\n    display: flex;\n    justify-content: center;\n    width: 100%;\n  }\n  .bt-node {\n    background: #ffffff;\n    border: 2px solid #2b2927;\n    border-radius: 8px;\n    padding: 10px 16px;\n    box-shadow: 2px 2px 0px #2b2927;\n    text-align: center;\n    min-width: 180px;\n    max-width: 260px;\n    transition: all 0.25s ease;\n    position: relative;\n  }\n  .bt-node-chip {\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 9px;\n    font-weight: 800;\n    letter-spacing: 0.05em;\n    color: #78716c;\n    display: block;\n    margin-bottom: 3px;\n  }\n  .bt-node-title {\n    font-size: 13px;\n    font-weight: 800;\n    color: #171615;\n  }\n  .bt-node-sub {\n    font-size: 11px;\n    color: #78716c;\n    margin-top: 3px;\n    line-height: 1.35;\n  }\n  .bt-connector-v {\n    width: 2px;\n    height: 18px;\n    background: #2b2927;\n    margin: 0 auto;\n  }\n  .bt-branches-grid {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 20px;\n    width: 100%;\n    max-width: 620px;\n    margin-top: 4px;\n    position: relative;\n  }\n  .bt-branch-col {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n  }\n  .bt-leafs-stack {\n    display: flex;\n    flex-direction: column;\n    gap: 8px;\n    width: 100%;\n  }\n  .bt-leaf {\n    width: 100%;\n    max-width: 100%;\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    text-align: left;\n    padding: 8px 12px;\n  }\n  .bt-leaf .bt-node-title {\n    font-size: 12px;\n  }\n  .bt-leaf-res {\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 10px;\n    font-weight: 800;\n    padding: 2px 6px;\n    border-radius: 4px;\n    background: #f5f5f4;\n    color: #78716c;\n    border: 1px solid #d6d3d1;\n  }\n\n  \/* Evaluation States *\/\n  .bt-node.eval-success {\n    border-color: #16a34a !important;\n    background: #f0fdf4 !important;\n    box-shadow: 3px 3px 0px #16a34a !important;\n  }\n  .bt-node.eval-failure {\n    border-color: #dc2626 !important;\n    background: #fef2f2 !important;\n    box-shadow: 3px 3px 0px #dc2626 !important;\n  }\n  .bt-node.eval-running {\n    border-color: #0284c7 !important;\n    background: #f0f9ff !important;\n    box-shadow: 3px 3px 0px #0284c7 !important;\n  }\n  .bt-node.eval-skipped {\n    opacity: 0.45;\n  }\n\n  \/* Telemetry Box *\/\n  .bt-telemetry-box {\n    margin-top: 22px;\n    width: 100%;\n    background: #faf8f5;\n    border: 1.5px solid #2b2927;\n    border-radius: 6px;\n    padding: 12px 16px;\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 12px;\n    color: #2b2927;\n    line-height: 1.5;\n    box-shadow: 2px 2px 0px #2b2927;\n  }\n\n  \/* ========================================================\n     BREAKPOINT 1: TABLET (768px - 1024px)\n     ======================================================== *\/\n  @media (max-width: 1024px) {\n    .lab-wrapper {\n      padding: 32px 20px 60px;\n      max-width: 860px;\n    }\n    .lab-grid-container {\n      grid-template-columns: 1fr;\n      gap: 24px;\n    }\n    .canvas-focus-wrapper {\n      max-width: 468px;\n      margin: 0 auto;\n    }\n    .mgs-sim-container {\n      grid-template-columns: 1fr;\n      gap: 22px;\n    }\n    .mgs-viewport-col {\n      max-width: 520px;\n      margin: 0 auto;\n    }\n    .mgs-fsm-col {\n      max-width: 520px;\n      margin: 0 auto;\n    }\n    .roadmap-card {\n      grid-template-columns: 80px 1fr auto;\n      gap: 12px;\n      padding: 14px 16px;\n    }\n  }\n\n  \/* ========================================================\n     BREAKPOINT 2: MOBILE (< 768px)\n     ======================================================== *\/\n  @media (max-width: 768px) {\n    .lab-header {\n      padding: 32px 18px 24px;\n    }\n    .lab-header h1 {\n      font-size: clamp(1.75rem, 6.5vw, 2.2rem);\n      margin-bottom: 10px;\n    }\n    .lab-header p {\n      font-size: 0.98rem;\n      line-height: 1.5;\n    }\n    .lab-wrapper {\n      padding: 20px 14px 60px;\n    }\n    .lab-module {\n      padding: 18px 14px;\n      margin-bottom: 28px;\n      border-radius: 10px;\n    }\n    .lab-module-header {\n      margin-bottom: 14px;\n      padding-bottom: 10px;\n    }\n    .lab-toolbar {\n      display: grid;\n      grid-template-columns: 1fr;\n      gap: 8px;\n    }\n    .tool-btn {\n      width: 100%;\n      min-height: 42px;\n      padding: 10px;\n      font-size: 13px;\n    }\n    .mgs-actions {\n      display: grid;\n      grid-template-columns: 1fr 1fr;\n      gap: 8px;\n      max-width: 100%;\n    }\n    #mgs-btn-knock {\n      grid-column: span 2;\n    }\n    .mgs-btn {\n      padding: 10px 8px;\n      min-height: 42px;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      text-align: center;\n      font-size: 12px;\n    }\n    .mgs-hint {\n      max-width: 100%;\n      font-size: 11px;\n    }\n    .roadmap-card {\n      grid-template-columns: 1fr;\n      gap: 8px;\n      padding: 14px;\n    }\n    .roadmap-badge {\n      justify-self: start;\n    }\n  }\n\n  \/* ========================================================\n     BREAKPOINT 3: SMALL MOBILE (< 480px down to 320px)\n     ======================================================== *\/\n  @media (max-width: 480px) {\n    .lab-header {\n      padding: 24px 14px 18px;\n    }\n    .lab-wrapper {\n      padding: 16px 10px 50px;\n    }\n    .lab-module {\n      padding: 14px 10px;\n      margin-bottom: 20px;\n    }\n    .canvas-focus-wrapper {\n      padding: 5px;\n      border-radius: 8px;\n    }\n    .mgs-status-bar {\n      top: 5px;\n      left: 5px;\n      right: 5px;\n      gap: 3px;\n    }\n    .mgs-hud-badge {\n      font-size: clamp(8.5px, 2.7vw, 10.5px);\n      padding: 2px 5px;\n    }\n    .fsm-nodes-grid {\n      grid-template-columns: 1fr 1fr;\n      gap: 6px;\n      margin-bottom: 10px;\n    }\n    .fsm-state-card {\n      padding: 7px 4px;\n    }\n    .fsm-state-card .fsm-card-name {\n      font-size: 11.5px;\n    }\n    .fsm-state-card .fsm-card-sub {\n      font-size: 9px;\n    }\n    .fsm-slider-row {\n      font-size: 10.5px;\n    }\n    .fsm-slider-val {\n      min-width: 36px;\n      font-size: 11px;\n    }\n    .mgs-codec-log {\n      font-size: 10px;\n      padding: 9px 10px;\n      min-height: 48px;\n    }\n  }\n<\/style>\n<div class=\"cpue-lab\">\n  <header class=\"lab-header\">\n    <div class=\"lab-header-inner\">\n      <div style=\"display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px; margin-bottom:12px;\">\n        <span class=\"lab-tagline\" style=\"margin-bottom:0;\">Laboratorio Interactivo &middot; Obra Final APGI<\/span>\n        <div class=\"sound-controls-bar\">\n          <button class=\"sound-toggle-btn\" id=\"btn-sound-toggle\" type=\"button\" title=\"Conmutar efectos de audio sintetizados\">\n            <span id=\"sound-icon\">&#128266;<\/span> <span id=\"sound-text\">Audio: Activo<\/span>\n          <\/button>\n          <select class=\"sound-preset-select\" id=\"sound-preset-select\" title=\"Seleccionar perfil de sonido\">\n            <option value=\"subtle\" selected>Perfil: Mec&aacute;nico Sutil<\/option>\n            <option value=\"tactical\">Perfil: T&aacute;ctico Ac&uacute;stico<\/option>\n            <option value=\"arcade\">Perfil: Retro Arcade 8-bit<\/option>\n          <\/select>\n        <\/div>\n      <\/div>\n      <h1>C&oacute;mo piensa un enemigo<\/h1>\n      <p>\n        Un cuaderno explicable inspirado en <strong>Red Blob Games<\/strong> y <strong>Nicky Case<\/strong> para manipular las matem&aacute;ticas y la arquitectura detr&aacute;s de los enemigos de videojuegos sobre la cuadr&iacute;cula arcade original.\n      <\/p>\n    <\/div>\n  <\/header>\n\n  <main class=\"lab-wrapper\">\n\n    <!-- ESTRUCTURA DEL PROYECTO COMPLETO -->\n    <section class=\"cpue-roadmap-section\">\n      <h2>Estructura del Proyecto Completo<\/h2>\n      <p>\n        La web final combinar&aacute; este rigor de divulgaci&oacute;n visual interactiva a lo largo de cuatro d&eacute;cadas de tecnolog&iacute;a l&uacute;dica:\n      <\/p>\n\n      <div class=\"roadmap-card\">\n        <span class=\"roadmap-num\">CAP. 01<\/span>\n        <div>\n          <div class=\"roadmap-title\">Geometr&iacute;a de la persecuci&oacute;n: Pac-Man (1980)<\/div>\n          <div class=\"roadmap-desc\">Espacios discretos en cuadr&iacute;cula, casillas objetivo y la ilusi&oacute;n de la inteligencia en 8 bits.<\/div>\n        <\/div>\n        <div><span class=\"roadmap-badge badge-func\">Demo funcional<\/span><\/div>\n      <\/div>\n\n      <div class=\"roadmap-card\">\n        <span class=\"roadmap-num\">CAP. 02<\/span>\n        <div>\n          <div class=\"roadmap-title\">Aut&oacute;matas y M&aacute;quinas de Estados: Doom y Half-Life (1993&ndash;1998)<\/div>\n          <div class=\"roadmap-desc\">Patrulla, alerta, ataque y huida. El auge y los l&iacute;mites de las FSMs cl&aacute;sicas.<\/div>\n        <\/div>\n        <div><span class=\"roadmap-badge badge-func\">Demo funcional<\/span><\/div>\n      <\/div>\n\n      <div class=\"roadmap-card\" id=\"card-cap3\">\n        <span class=\"roadmap-num\">CAP. 03<\/span>\n        <div>\n          <div class=\"roadmap-title\">La revoluci&oacute;n jer&aacute;rquica: Behavior Trees en Halo 2 (2004)<\/div>\n          <div class=\"roadmap-desc\">Selectores, secuencias y decoradores. C&oacute;mo ordenar prioridades como una mente org&aacute;nica.<\/div>\n        <\/div>\n        <div><span class=\"roadmap-badge badge-dev\">En desarrollo<\/span><\/div>\n      <\/div>\n\n      <div class=\"roadmap-card\">\n        <span class=\"roadmap-num\">EP&Iacute;LOGO<\/span>\n        <div>\n          <div class=\"roadmap-title\">Utility AI y el Futuro Probabil&iacute;stico &middot; Alien: Isolation y MIR5 (2026 - 2027)<\/div>\n          <div class=\"roadmap-desc\">Curvas de respuesta matem&aacute;tica, sopesado de opciones simult&aacute;neas y redes de decisi&oacute;n. Casos de estudio: <strong>Alien: Isolation<\/strong> (arquitectura de doble cerebro y director de IA) y <strong>MIR5<\/strong> (modelos probabil&iacute;sticos y sistemas neuronales en mundos compartidos).<\/div>\n        <\/div>\n        <div><span class=\"roadmap-badge badge-inv\">Investigaci&oacute;n<\/span><\/div>\n      <\/div>\n    <\/section>\n\n    <!-- MODULE 1: PAC-MAN TARGETING -->\n    <section class=\"lab-module\">\n      <div class=\"lab-module-header\">\n        <h2 class=\"lab-module-title\">M&oacute;dulo 1: La Ilusi&oacute;n de Inteligencia: Vectores y Casillas Objetivo &middot; Pac-Man (1980)<\/h2>\n        <span class=\"lab-module-badge\">Avance Cap. 1 &middot; Navegaci&oacute;n en Cuadr&iacute;cula<\/span>\n      <\/div>\n\n      <p style=\"margin-bottom: 18px; color: #444;\">\n        En lugar de apuntar con l&iacute;neas rectas que atraviesan paredes, <strong>los fantasmas navegan paso a paso por los pasillos del laberinto<\/strong>. La ruta trazada con puntos resalta el camino calculado hacia su casilla objetivo (<span class=\"mono\">Target Tile<\/span>). Si el objetivo matem&aacute;tico cae dentro de un muro, el algoritmo selecciona autom&aacute;ticamente la casilla transitable m&aacute;s pr&oacute;xima.\n      <\/p>\n\n      <div class=\"lab-grid-container\">\n        <!-- Interactive Canvas Container -->\n        <div>\n          <div class=\"canvas-focus-wrapper\" id=\"game-focus-box\" tabindex=\"0\">\n            <canvas id=\"lab-canvas\" width=\"448\" height=\"496\"><\/canvas>\n          <\/div>\n\n          <!-- Toolbars: Obstacles (Clean, no emojis, Anadir obstaculos next to Mapa) -->\n          <div style=\"margin-top:14px;\">\n            <div style=\"font-size:12px; font-weight:700; color:#666; margin-bottom:6px; text-transform:uppercase; letter-spacing:0.05em;\">Gesti&oacute;n de Obst&aacute;culos:<\/div>\n            <div class=\"lab-toolbar\" style=\"margin-top:0;\">\n              <button class=\"tool-btn active\" id=\"btn-orig-walls\">Mapa original de Pac-Man<\/button>\n              <button class=\"tool-btn\" id=\"btn-draw-walls\">A&ntilde;adir obst&aacute;culos<\/button>\n              <button class=\"tool-btn\" id=\"btn-clear-walls\">Limpiar todo<\/button>\n            <\/div>\n          <\/div>\n\n          <!-- Controls Guide (clean, no emojis) -->\n          <div style=\"margin-top:14px; border-top:1px dashed #d6cfc2; padding-top:10px;\">\n            <div style=\"font-size:12px; font-weight:700; color:#666; margin-bottom:6px; text-transform:uppercase; letter-spacing:0.05em;\">Controles del simulador:<\/div>\n            <ul style=\"list-style: none; padding-left: 0; font-size: 12.5px; color: #555; line-height: 1.55;\">\n              <li style=\"margin-bottom: 4px;\">&bull; <strong>Arrastrar:<\/strong> Haz clic y arrastra a <span class=\"tag tag-pacman\">Pac-Man<\/span> o a cualquiera de los 4 fantasmas. La boca rota hacia la direcci&oacute;n de movimiento.<\/li>\n              <li style=\"margin-bottom: 4px;\">&bull; <strong>Modo obst&aacute;culos:<\/strong> Al seleccionar a un personaje con el trazado activo, se arrastra el personaje sin pintar muros.<\/li>\n              <li>&bull; <strong>Teclado:<\/strong> Haz clic sobre el mapa para navegar con las flechas del teclado. Haz clic fuera para desactivar el foco.<\/li>\n            <\/ul>\n          <\/div>\n        <\/div>\n\n        <!-- Formula and live telemetry box -->\n        <div>\n          <div class=\"lab-formula-box\">\n            <div style=\"display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; padding-bottom:8px; border-bottom:1.5px solid #eae5d8;\">\n              <span style=\"font-size:11px; font-weight:800; text-transform:uppercase; letter-spacing:0.06em; color:#78716c;\">Telemetr&iacute;a en Tiempo Real<\/span>\n              <span style=\"font-size:11px; color:#8c7355; font-weight:600;\">(Haz clic en cada fantasma para ver su f&oacute;rmula)<\/span>\n            <\/div>\n\n            <div class=\"lab-formula-row\" data-ghost=\"blinky\">\n              <div class=\"formula-title\">\n                <span class=\"tag tag-blinky\">&bull; Blinky (Perseguidor)<\/span>\n                <span id=\"f-blinky-coord\" class=\"mono\">[13, 23]<\/span>\n              <\/div>\n              <div class=\"formula-math\">Objetivo: Casilla exacta de Pac-Man. Persecuci&oacute;n directa.<\/div>\n            <\/div>\n\n            <div class=\"lab-formula-row\" data-ghost=\"pinky\">\n              <div class=\"formula-title\">\n                <span class=\"tag tag-pinky\">&bull; Pinky (Emboscadora)<\/span>\n                <span id=\"f-pinky-coord\" class=\"mono\">[17, 23]<\/span>\n              <\/div>\n              <div class=\"formula-math\">Objetivo: +4 casillas por delante de la mirada de Pac-Man.<\/div>\n            <\/div>\n\n            <div class=\"lab-formula-row\" data-ghost=\"inky\">\n              <div class=\"formula-title\">\n                <span class=\"tag tag-inky\">&bull; Inky (T&iacute;mido)<\/span>\n                <span id=\"f-inky-coord\" class=\"mono\">[21, 23]<\/span>\n              <\/div>\n              <div class=\"formula-math\">Objetivo: Vector sim&eacute;trico duplicado respecto a Blinky.<\/div>\n            <\/div>\n\n            <div class=\"lab-formula-row\" data-ghost=\"clyde\">\n              <div class=\"formula-title\">\n                <span class=\"tag tag-clyde\">&bull; Clyde (Indeciso)<\/span>\n                <span id=\"f-clyde-coord\" class=\"mono\">[13, 23]<\/span>\n              <\/div>\n              <div class=\"formula-math\" id=\"f-clyde-desc\">Distancia &ge; 8: persigue. &lt; 8: huye a esquina inf-izq.<\/div>\n            <\/div>\n\n            <button class=\"tool-btn\" id=\"btn-toggle-ghost-doc\" style=\"width: 100%; margin-top: 14px; text-align: center; font-size: 12px; display: block;\">\n              Ver an&aacute;lisis matem&aacute;tico y pedag&oacute;gico &darr;\n            <\/button>\n          <\/div>\n\n          <!-- Detailed Technical Documentation Panel (Expandable on click) -->\n          <div class=\"ghost-deep-dive\" id=\"ghost-deep-dive\">\n            <div style=\"display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1.5px solid #e2ddd3;\">\n              <span style=\"font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; color: #171615;\">Ficha T&eacute;cnica de los 4 Algoritmos:<\/span>\n              <button id=\"btn-close-ghost-doc\" style=\"background: none; border: none; font-size: 11.5px; font-weight: 700; color: #78716c; cursor: pointer; text-decoration: underline;\">Cerrar &times;<\/button>\n            <\/div>\n\n            <!-- BLINKY -->\n            <div class=\"ghost-card\" id=\"card-blinky\">\n              <div class=\"ghost-card-header\">\n                <span class=\"tag tag-blinky\">&bull; Blinky (Perseguidor)<\/span>\n              <\/div>\n              <div class=\"ghost-card-formula\">F&oacute;rmula: Target = (X<sub>PacMan<\/sub>, Y<sub>PacMan<\/sub>)<\/div>\n              <div class=\"ghost-card-desc\">\n                Persigue siempre la casilla exacta donde est&aacute; el jugador. Es la amenaza directa m&aacute;s previsible del juego.\n              <\/div>\n            <\/div>\n\n            <!-- PINKY -->\n            <div class=\"ghost-card\" id=\"card-pinky\">\n              <div class=\"ghost-card-header\">\n                <span class=\"tag tag-pinky\">&bull; Pinky (Emboscadora)<\/span>\n              <\/div>\n              <div class=\"ghost-card-formula\">F&oacute;rmula: Target = Pos<sub>PacMan<\/sub> + 4 &middot; V<sub>mirada<\/sub><\/div>\n              <div class=\"ghost-card-desc\">\n                Proyecta su diana 4 casillas por delante de hacia d&oacute;nde mira Pac-Man para cortarle el paso y tender una emboscada.\n              <\/div>\n            <\/div>\n\n            <!-- INKY -->\n            <div class=\"ghost-card\" id=\"card-inky\">\n              <div class=\"ghost-card-header\">\n                <span class=\"tag tag-inky\">&bull; Inky (T&iacute;mido)<\/span>\n              <\/div>\n              <div class=\"ghost-card-formula\">F&oacute;rmula: T<sub>Inky<\/sub> = B + 2 &middot; (P<sub>pivote<\/sub> &minus; B)<\/div>\n              <div class=\"ghost-card-desc\">\n                Calcula un pivote a 2 casillas por delante de Pac-Man, traza el vector desde Blinky a ese pivote y lo duplica.\n              <\/div>\n              <div class=\"ghost-card-pedagogy\">\n                <strong>Valor pedag&oacute;gico:<\/strong> Demuestra <em>comportamiento emergente<\/em>. Inky no puede tomar decisiones sin la posici&oacute;n de Blinky; juntos acorralan al jugador en pinza sin necesidad de comunicarse directamente.\n              <\/div>\n            <\/div>\n\n            <!-- CLYDE -->\n            <div class=\"ghost-card\" id=\"card-clyde\">\n              <div class=\"ghost-card-header\">\n                <span class=\"tag tag-clyde\">&bull; Clyde (Indeciso)<\/span>\n              <\/div>\n              <div class=\"ghost-card-formula\">F&oacute;rmula: D = &radic;(&Delta;x&sup2; + &Delta;y&sup2;)<\/div>\n              <div class=\"ghost-card-desc\">\n                Calcula la distancia euclidiana directa hasta Pac-Man:<br>\n                &bull; <strong>Si D &ge; 8 casillas:<\/strong> Se comporta exactamente igual que Blinky (persigue).<br>\n                &bull; <strong>Si D &lt; 8 casillas:<\/strong> Sufre &ldquo;miedo&rdquo; y huye hacia su esquina inferior izquierda [1, 29].\n              <\/div>\n            <\/div>\n          <\/div>\n\n          <div style=\"margin-top:16px; background:#fff; border:1.5px solid #2b2927; border-radius:8px; padding:14px; box-shadow:2px 2px 0px #2b2927;\">\n            <strong style=\"font-size:12px; text-transform:uppercase; letter-spacing:0.06em; color:#8c7355; display:block; margin-bottom:6px;\">Coordenadas en Vivo:<\/strong>\n            <div class=\"mono\" style=\"font-size:12px; line-height:1.55;\" id=\"pac-telemetry\">\n              Pac-Man: [13, 23] | Mirando: Derecha (&rarr;)\n            <\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n    <\/section>\n\n    <!-- MODULE 2: METAL GEAR SOLID FSM LAB -->\n    <section class=\"lab-module\">\n      <div class=\"lab-module-header\">\n        <h2 class=\"lab-module-title\">M&oacute;dulo 2: M&aacute;quina de Estados Finita (FSM) &middot; Metal Gear Solid (1998)<\/h2>\n        <span class=\"lab-module-badge\">Avance Cap. 2 &middot; IA de Sigilo Sensorial<\/span>\n      <\/div>\n\n      <p style=\"margin-bottom: 14px; color: #444; line-height: 1.55;\">\n        En 1998, <em>Metal Gear Solid<\/em> revolucion&oacute; la inteligencia artificial en consolas sustituyendo la persecuci&oacute;n permanente por un <strong>aut&oacute;mata de estados finitos (FSM)<\/strong> guiado por percepci&oacute;n sensorial (cono de visi&oacute;n con oclusi&oacute;n por coberturas y propagaci&oacute;n de ondas ac&uacute;sticas). Mueve a Snake, oc&uacute;ltate tras los 3 bloques de cobertura y observa a la derecha c&oacute;mo conmuta el aut&oacute;mata en tiempo real:\n      <\/p>\n\n      <div class=\"mgs-sim-container\">\n        <!-- Columna Izquierda: Lienzo Tactico 2D -->\n        <div class=\"mgs-viewport-col\">\n          <div class=\"mgs-canvas-wrapper\">\n            <canvas id=\"mgs-canvas\" width=\"480\" height=\"340\"><\/canvas>\n            <div class=\"mgs-status-bar\">\n              <span class=\"mgs-hud-badge\" id=\"mgs-hud-state\">ESTADO: PATRULLA<\/span>\n              <span class=\"mgs-hud-badge\" id=\"mgs-hud-hp\" style=\"color:#4ade80; border-color:#22c55e;\">VIDA: 100%<\/span>\n              <span class=\"mgs-hud-badge\" id=\"mgs-hud-los\">COBERTURA: LIBRE<\/span>\n            <\/div>\n          <\/div>\n\n          <div class=\"mgs-actions\">\n            <button class=\"mgs-btn\" id=\"mgs-btn-knock\">Golpear pared (Ruido)<\/button>\n            <button class=\"mgs-btn\" id=\"mgs-btn-box\">Equipar Caja de Cart&oacute;n<\/button>\n            <button class=\"mgs-btn\" id=\"mgs-btn-speed\">Velocidad: 1.0x<\/button>\n            <button class=\"mgs-btn\" id=\"mgs-btn-pause\">Pausar<\/button>\n            <button class=\"mgs-btn\" id=\"mgs-btn-reset\">Reiniciar<\/button>\n          <\/div>\n\n          <div class=\"mgs-hint\">\n            <strong>Controles del infiltrado:<\/strong> Usa <code>WASD<\/code>, las <code>flechas del teclado<\/code> o <code>haz clic en el mapa<\/code> para mover a Snake. Los 3 bloques bloquean f&iacute;sicamente el paso y la visi&oacute;n del guardia.\n          <\/div>\n        <\/div>\n\n        <!-- Columna Derecha: Grafo FSM + Parametros + Codec -->\n        <div class=\"mgs-fsm-col\">\n          <div class=\"mgs-fsm-card\">\n            <div class=\"mgs-fsm-card-title\">\n              <span>M&aacute;quina de Estados del Guardia<\/span>\n              <span style=\"font-size:10px; color:#64748b; font-family:'JetBrains Mono',monospace;\">FSM EN VIVO<\/span>\n            <\/div>\n\n            <div class=\"fsm-nodes-grid\">\n              <div class=\"fsm-state-card state-patrol active-state\" id=\"card-patrol\">\n                <span class=\"fsm-card-chip\">ESTADO 01<\/span>\n                <span class=\"fsm-card-name\">Patrulla<\/span>\n                <span class=\"fsm-card-sub\">Ruta de waypoints fija<\/span>\n              <\/div>\n\n              <div class=\"fsm-state-card state-suspicious\" id=\"card-suspicious\">\n                <span class=\"fsm-card-chip\">ESTADO 02<\/span>\n                <span class=\"fsm-card-name\">Sospecha (?)<\/span>\n                <span class=\"fsm-card-sub\">Investiga onda de ruido<\/span>\n              <\/div>\n\n              <div class=\"fsm-state-card state-alert\" id=\"card-alert\">\n                <span class=\"fsm-card-chip\">ESTADO 03<\/span>\n                <span class=\"fsm-card-name\">Alerta (!)<\/span>\n                <span class=\"fsm-card-sub\">Distancia t&aacute;ctica &middot; Fuego<\/span>\n              <\/div>\n\n              <div class=\"fsm-state-card state-busqueda\" id=\"card-busqueda\">\n                <span class=\"fsm-card-chip\">ESTADO 04<\/span>\n                <span class=\"fsm-card-name\">B&uacute;squeda<\/span>\n                <span class=\"fsm-card-sub\">Rastreo activo &middot; Timer<\/span>\n              <\/div>\n            <\/div>\n\n            <!-- Sliders pedagogicos -->\n            <div class=\"fsm-slider-group\">\n              <div class=\"fsm-slider-row\">\n                <span>&Aacute;ngulo de visi&oacute;n:<\/span>\n                <input type=\"range\" id=\"mgs-slider-fov\" min=\"30\" max=\"110\" value=\"110\">\n                <span class=\"fsm-slider-val\" id=\"val-fov\">110&deg;<\/span>\n              <\/div>\n              <div class=\"fsm-slider-row\">\n                <span>Alcance del cono:<\/span>\n                <input type=\"range\" id=\"mgs-slider-dist\" min=\"90\" max=\"230\" value=\"230\">\n                <span class=\"fsm-slider-val\" id=\"val-dist\">230px<\/span>\n              <\/div>\n              <div class=\"fsm-slider-row\">\n                <span>Memoria de b&uacute;squeda:<\/span>\n                <input type=\"range\" id=\"mgs-slider-timer\" min=\"3\" max=\"10\" value=\"6\">\n                <span class=\"fsm-slider-val\" id=\"val-timer\">6s<\/span>\n              <\/div>\n            <\/div>\n          <\/div>\n\n          <!-- Codec \/ Terminal de Registro -->\n          <div class=\"mgs-codec-log\" id=\"mgs-codec-log\">\n            [00:00] CODEC ONLINE &middot; Guardia en ruta de patrulla sin anomal&iacute;as detectadas.\n          <\/div>\n        <\/div>\n      <\/div>\n    <\/section>\n\n    <!-- ========================================================\n         MODULO 3: ARBOLES DE CONDUCTA (BEHAVIOR TREES)\n         ======================================================== -->\n    <section class=\"lab-module\" id=\"modulo-3\">\n      <div class=\"lab-module-header\">\n        <h2 class=\"lab-module-title\">M&oacute;dulo 3: &Aacute;rboles de Conducta (Behavior Trees) &middot; Halo 2 (2004)<\/h2>\n        <span class=\"lab-module-badge\">Avance Cap. 3 &middot; Toma de Decisiones Jer&aacute;rquica<\/span>\n      <\/div>\n\n      <p style=\"margin-bottom: 14px; color: #444; line-height: 1.55;\">\n        En 2004, Damian Isla y el equipo de Bungie revolucionaron la inteligencia artificial en <em>Halo 2<\/em> con la introducci&oacute;n de los <strong>Behavior Trees<\/strong> (&Aacute;rboles de Conducta). A diferencia de las m&aacute;quinas de estados finitas (FSM), donde a&ntilde;adir una nueva acci&oacute;n genera una explosi&oacute;n combinatoria de transiciones ingobernables, un &Aacute;rbol de Conducta organiza las prioridades como ramas jer&aacute;rquicas y reutilizables de <strong>Selectores<\/strong>, <strong>Secuencias<\/strong>, <strong>Condiciones<\/strong> y <strong>Decoradores<\/strong>.\n      <\/p>\n\n      <div class=\"bt-workbench\" id=\"bt-workbench\">\n        <div class=\"bt-teaser-header\">\n          <div>\n            <span class=\"bt-tag\">BANCO DID&Aacute;CTICO EN VIVO &middot; ARQUITECTURA BUNGIE (2004)<\/span>\n            <h3 class=\"bt-title\">Simulador de Toma de Decisiones en &Aacute;rbol<\/h3>\n          <\/div>\n          <div class=\"bt-controls\">\n            <button class=\"bt-action-btn active-sim\" id=\"btn-sim-threat\" type=\"button\">Intruso Detectado<\/button>\n            <button class=\"bt-action-btn\" id=\"btn-sim-clear\" type=\"button\">Zona Despejada<\/button>\n            <button class=\"bt-action-btn pulse-btn\" id=\"btn-bt-tick\" type=\"button\">&#9654; Evaluar Pulso (Tick)<\/button>\n          <\/div>\n        <\/div>\n\n        <div class=\"bt-tree-layout\">\n          <!-- Nivel 0: Raiz Repeater -->\n          <div class=\"bt-level\">\n            <div class=\"bt-node\" id=\"bt-node-root\">\n              <span class=\"bt-node-chip\">DECORADOR RA&Iacute;Z<\/span>\n              <div class=\"bt-node-title\">Repeater (Tick Continuo)<\/div>\n              <div class=\"bt-node-sub\">Mantiene el ciclo de decisi&oacute;n en cada frame<\/div>\n            <\/div>\n          <\/div>\n          <div class=\"bt-connector-v\"><\/div>\n\n          <!-- Nivel 1: Selector de Prioridad -->\n          <div class=\"bt-level\">\n            <div class=\"bt-node\" id=\"bt-node-selector\">\n              <span class=\"bt-node-chip\">COMPUESTO SELECTOR<\/span>\n              <div class=\"bt-node-title\">Prioridad (Combate vs Patrulla)<\/div>\n              <div class=\"bt-node-sub\">Prueba rama 1; si falla, ejecuta rama 2<\/div>\n            <\/div>\n          <\/div>\n          <div class=\"bt-connector-v\"><\/div>\n\n          <!-- Nivel 2: Ramas Paralelas -->\n          <div class=\"bt-branches-grid\">\n            <!-- Rama Izquierda: Secuencia de Ataque -->\n            <div class=\"bt-branch-col\" id=\"col-attack\">\n              <div class=\"bt-node\" id=\"bt-node-seq-attack\" style=\"width:100%;\">\n                <span class=\"bt-node-chip\">SECUENCIA 1 (ALTA PRIORIDAD)<\/span>\n                <div class=\"bt-node-title\">Ataque T&aacute;ctico<\/div>\n              <\/div>\n              <div class=\"bt-connector-v\"><\/div>\n              <div class=\"bt-leafs-stack\">\n                <div class=\"bt-node bt-leaf\" id=\"bt-leaf-detect\">\n                  <div>\n                    <span class=\"bt-node-chip\">CONDICI&Oacute;N<\/span>\n                    <div class=\"bt-node-title\">1. Detectar Intruso<\/div>\n                  <\/div>\n                  <span class=\"bt-leaf-res\" id=\"bt-res-detect\">?<\/span>\n                <\/div>\n                <div class=\"bt-node bt-leaf\" id=\"bt-leaf-approach\">\n                  <div>\n                    <span class=\"bt-node-chip\">ACCI&Oacute;N<\/span>\n                    <div class=\"bt-node-title\">2. Aproximarse<\/div>\n                  <\/div>\n                  <span class=\"bt-leaf-res\" id=\"bt-res-approach\">?<\/span>\n                <\/div>\n                <div class=\"bt-node bt-leaf\" id=\"bt-leaf-shoot\">\n                  <div>\n                    <span class=\"bt-node-chip\">ACCI&Oacute;N<\/span>\n                    <div class=\"bt-node-title\">3. Abrir Fuego<\/div>\n                  <\/div>\n                  <span class=\"bt-leaf-res\" id=\"bt-res-shoot\">?<\/span>\n                <\/div>\n              <\/div>\n            <\/div>\n\n            <!-- Rama Derecha: Secuencia de Patrulla -->\n            <div class=\"bt-branch-col\" id=\"col-patrol\">\n              <div class=\"bt-node\" id=\"bt-node-seq-patrol\" style=\"width:100%;\">\n                <span class=\"bt-node-chip\">SECUENCIA 2 (RUTINA)<\/span>\n                <div class=\"bt-node-title\">Patrulla Preventiva<\/div>\n              <\/div>\n              <div class=\"bt-connector-v\"><\/div>\n              <div class=\"bt-leafs-stack\">\n                <div class=\"bt-node bt-leaf\" id=\"bt-leaf-negator\">\n                  <div>\n                    <span class=\"bt-node-chip\">DECORADOR NOT<\/span>\n                    <div class=\"bt-node-title\">1. &iquest;Sin amenaza?<\/div>\n                  <\/div>\n                  <span class=\"bt-leaf-res\" id=\"bt-res-negator\">?<\/span>\n                <\/div>\n                <div class=\"bt-node bt-leaf\" id=\"bt-leaf-patrol\">\n                  <div>\n                    <span class=\"bt-node-chip\">ACCI&Oacute;N<\/span>\n                    <div class=\"bt-node-title\">2. Seguir Ruta<\/div>\n                  <\/div>\n                  <span class=\"bt-leaf-res\" id=\"bt-res-patrol\">?<\/span>\n                <\/div>\n              <\/div>\n            <\/div>\n          <\/div>\n\n          <!-- Telemetria Didactica en Vivo -->\n          <div class=\"bt-telemetry-box\" id=\"bt-telemetry-box\">\n            <strong>Telemetr&iacute;a did&aacute;ctica:<\/strong> Pulsa &laquo;Evaluar Pulso (Tick)&raquo; o cambia la condici&oacute;n del entorno para observar c&oacute;mo se propaga el flujo l&oacute;gico por las ramas jer&aacute;rquicas.\n          <\/div>\n        <\/div>\n      <\/div>\n    <\/section>\n\n    <footer class=\"lab-footer\">\n      <div class=\"lab-counter-container\">\n        <div class=\"lab-counter-pill\">\n          <span class=\"counter-live-dot\" title=\"Registro de tr&aacute;fico activo\"><\/span>\n          <span class=\"counter-label\">Visualizaciones:<\/span>\n          <img decoding=\"async\" src=\"https:\/\/api.visitorbadge.io\/api\/visitors?path=https%3A%2F%2Fericguerrero.es%2Fpage39&label=VISITAS&labelColor=%232b2927&countColor=%2316a34a&style=flat-square\"\n               alt=\"Contador de visualizaciones de la web\"\n               class=\"counter-badge-img\"\n               loading=\"lazy\"\n               onerror=\"this.style.display='none'; document.getElementById('counter-fallback-val').style.display='inline-block';\" \/>\n          <span id=\"counter-fallback-val\" class=\"counter-fallback\" style=\"display:none;\">ONLINE<\/span>\n        <\/div>\n      <\/div>\n      <p>\n        <strong>Obra Final APGI 2026&ndash;2027<\/strong> &middot; Eric Guerrero &middot; Institut del Disseny i Arts Gr&agrave;fiques de Barcelona IDAG\n      <\/p>\n    <\/footer>\n\n  <\/main>\n<\/div>\n\n<script>\n(function() {\n  const canvas = document.getElementById('lab-canvas');\n  const focusBox = document.getElementById('game-focus-box');\n  const pacTelemetry = document.getElementById('pac-telemetry');\n  if (!canvas || !focusBox) return;\n  const ctx = canvas.getContext('2d');\n\n  \/\/ Disable context menu on canvas only\n  canvas.addEventListener('contextmenu', function(e) {\n    e.preventDefault();\n    return false;\n  });\n\n  const COLS = 28;\n  const ROWS = 31;\n  const cellW = canvas.width \/ COLS; \/\/ 448 \/ 28 = 16px\n  const cellH = canvas.height \/ ROWS; \/\/ 496 \/ 31 = 16px\n\n  \/\/ EXACT ARCADE PAC-MAN 28x31 TILE MATRIX\n  \/\/ Ghost house interior is hollow at rows 13-15, cols 11-16 with pink gate at row 12 cols 13-14\n  const ORIGINAL_MAZE = [\n    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],\n    [1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1],\n    [1,0,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,0,1],\n    [1,0,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,0,1],\n    [1,0,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,0,1],\n    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],\n    [1,0,1,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,1,0,1],\n    [1,0,1,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,1,0,1],\n    [1,0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,0,0,1],\n    [1,1,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,1,1],\n    [1,1,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,1,1],\n    [1,1,1,1,1,1,0,1,1,0,0,0,0,0,0,0,0,0,0,1,1,0,1,1,1,1,1,1],\n    [1,1,1,1,1,1,0,1,1,0,1,1,1,0,0,1,1,1,0,1,1,0,1,1,1,1,1,1],\n    [1,1,1,1,1,1,0,1,1,0,1,0,0,0,0,0,0,1,0,1,1,0,1,1,1,1,1,1],\n    [0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0],\n    [1,1,1,1,1,1,0,1,1,0,1,0,0,0,0,0,0,1,0,1,1,0,1,1,1,1,1,1],\n    [1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1],\n    [1,1,1,1,1,1,0,1,1,0,0,0,0,0,0,0,0,0,0,1,1,0,1,1,1,1,1,1],\n    [1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1],\n    [1,1,1,1,1,1,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,1,1,1,1,1,1],\n    [1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1],\n    [1,0,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,0,1],\n    [1,0,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,1,1,1,1,0,1],\n    [1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1],\n    [1,1,1,0,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,1,0,1,1,0,1,1,1],\n    [1,1,1,0,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,1,0,1,1,0,1,1,1],\n    [1,0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,0,0,1],\n    [1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1],\n    [1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1],\n    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],\n    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]\n  ];\n\n  \/\/ Active walls array & state\n  let walls = ORIGINAL_MAZE.map(row => [...row]);\n  let isOriginalMap = true;\n  let isDrawMode = false;\n  let hasKeyboardFocus = false;\n\n  \/\/ Characters starting positions outside the ghost house in 4 distinct quadrants\n  let pac = { x: 13, y: 23, dirX: 1, dirY: 0, angle: 0 };\n\n  let ghosts = {\n    blinky: { x: 21, y: 5,  color: '#ef4444', name: 'Blinky', cornerX: 25, cornerY: 1 },\n    pinky:  { x: 6,  y: 5,  color: '#ec4899', name: 'Pinky',  cornerX: 2,  cornerY: 1 },\n    inky:   { x: 21, y: 20, color: '#0ea5e9', name: 'Inky',   cornerX: 25, cornerY: 29 },\n    clyde:  { x: 6,  y: 20, color: '#f59e0b', name: 'Clyde',  cornerX: 1,  cornerY: 29 }\n  };\n\n  let draggedEntity = null;\n\n  \/\/ Clamps target to the nearest walkable corridor cell (like in Pac-Man)\n  function getValidWalkableTarget(tx, ty) {\n    tx = Math.max(0, Math.min(COLS - 1, Math.round(tx)));\n    ty = Math.max(0, Math.min(ROWS - 1, Math.round(ty)));\n\n    if (walls[ty][tx] === 0) return { x: tx, y: ty };\n\n    let best = null;\n    let bestDist = Infinity;\n    for (let r = 0; r < ROWS; r++) {\n      for (let c = 0; c < COLS; c++) {\n        if (walls[r][c] === 0) {\n          let dist = Math.hypot(c - tx, r - ty);\n          if (dist < bestDist) {\n            bestDist = dist;\n            best = { x: c, y: r };\n          }\n        }\n      }\n    }\n    return best || { x: tx, y: ty };\n  }\n\n  \/\/ BFS Pathfinding through walkable corridor tiles\n  function findPath(startX, startY, targetX, targetY) {\n    if (startX === targetX && startY === targetY) return [[startX, startY]];\n\n    \/\/ If start cell is on a wall, safely begin from nearest walkable cell\n    if (walls[startY] && walls[startY][startX] === 1) {\n      let nearest = getValidWalkableTarget(startX, startY);\n      startX = nearest.x;\n      startY = nearest.y;\n    }\n\n    let queue = [[startX, startY]];\n    let cameFrom = {};\n    cameFrom[`${startX},${startY}`] = null;\n    let found = false;\n\n    while (queue.length > 0) {\n      let [cx, cy] = queue.shift();\n      if (cx === targetX && cy === targetY) {\n        found = true;\n        break;\n      }\n\n      const dirs = [[0, -1], [-1, 0], [0, 1], [1, 0]];\n      for (let [dx, dy] of dirs) {\n        let nx = cx + dx;\n        let ny = cy + dy;\n\n        \/\/ Tunnel horizontal wrap at row 14\n        if (ny === 14) {\n          if (nx < 0) nx = COLS - 1;\n          else if (nx >= COLS) nx = 0;\n        }\n\n        if (nx >= 0 && nx < COLS && ny >= 0 && ny < ROWS) {\n          if (walls[ny][nx] === 0) {\n            let key = `${nx},${ny}`;\n            if (!(key in cameFrom)) {\n              cameFrom[key] = [cx, cy];\n              queue.push([nx, ny]);\n            }\n          }\n        }\n      }\n    }\n\n    if (!found) return [];\n\n    let path = [];\n    let curr = [targetX, targetY];\n    while (curr) {\n      path.unshift(curr);\n      let key = `${curr[0]},${curr[1]}`;\n      curr = cameFrom[key];\n    }\n    return path;\n  }\n\n  function computeTargets() {\n    \/\/ 1. Blinky: directly targets Pac-Man's tile\n    let b = getValidWalkableTarget(pac.x, pac.y);\n\n    \/\/ 2. Pinky: 4 tiles ahead of Pac-Man facing direction\n    let p = getValidWalkableTarget(pac.x + pac.dirX * 4, pac.y + pac.dirY * 4);\n\n    \/\/ 3. Inky: double vector from Blinky to pivot (2 ahead of Pac-Man)\n    let pivotX = pac.x + pac.dirX * 2;\n    let pivotY = pac.y + pac.dirY * 2;\n    let inkyX = ghosts.blinky.x + (pivotX - ghosts.blinky.x) * 2;\n    let inkyY = ghosts.blinky.y + (pivotY - ghosts.blinky.y) * 2;\n    let i = getValidWalkableTarget(inkyX, inkyY);\n\n    \/\/ 4. Clyde: distance check (>= 8 chases Pac-Man, < 8 retreats to bottom-left corner)\n    let dist = Math.hypot(ghosts.clyde.x - pac.x, ghosts.clyde.y - pac.y);\n    let c = dist >= 8 ? getValidWalkableTarget(pac.x, pac.y) : getValidWalkableTarget(ghosts.clyde.cornerX, ghosts.clyde.cornerY);\n\n    return { blinky: b, pinky: p, inky: i, clyde: c, clydeDist: dist };\n  }\n\n  function draw() {\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n    \/\/ 1. DRAW TILE BACKGROUNDS AND PELLETS\n    for (let r = 0; r < ROWS; r++) {\n      for (let c = 0; c < COLS; c++) {\n        if (walls[r][c] === 1) {\n          \/\/ Solid Wall Fill (clean, dark arcade fill without grid lines)\n          ctx.fillStyle = '#101422';\n          ctx.fillRect(c * cellW, r * cellH, cellW, cellH);\n        } else {\n          \/\/ Walkable Path Tile\n          ctx.fillStyle = '#080a10';\n          ctx.fillRect(c * cellW, r * cellH, cellW, cellH);\n\n          \/\/ Pellets: ONLY drawn in original arcade map mode (never in \"Limpiar todo\")\n          if (isOriginalMap) {\n            const isHouse = (r >= 11 && r <= 16 && c >= 9 && c <= 18);\n            const isTunnel = (r === 14 && (c < 6 || c > 21));\n            if (!isHouse && !isTunnel) {\n              ctx.fillStyle = '#e2e8f0';\n              ctx.fillRect(c * cellW + 7, r * cellH + 7, 2, 2);\n            }\n          }\n        }\n      }\n    }\n\n    \/\/ 2. DRAW WALL OUTLINES ONLY ON BORDERS TOUCHING AIR (NO INTERNAL BLOCK LINES)\n    ctx.strokeStyle = '#2563eb';\n    ctx.lineWidth = 2;\n    ctx.beginPath();\n    for (let r = 0; r < ROWS; r++) {\n      for (let c = 0; c < COLS; c++) {\n        if (walls[r][c] === 1) {\n          const x0 = c * cellW;\n          const x1 = (c + 1) * cellW;\n          const y0 = r * cellH;\n          const y1 = (r + 1) * cellH;\n\n          \/\/ Top edge: only if air above or canvas boundary\n          if (r === 0 || walls[r - 1][c] === 0) {\n            const y = (r === 0) ? 1 : y0;\n            ctx.moveTo(x0, y);\n            ctx.lineTo(x1, y);\n          }\n          \/\/ Bottom edge: only if air below or canvas boundary\n          if (r === ROWS - 1 || walls[r + 1][c] === 0) {\n            const y = (r === ROWS - 1) ? canvas.height - 1 : y1;\n            ctx.moveTo(x0, y);\n            ctx.lineTo(x1, y);\n          }\n          \/\/ Left edge: only if air to left or canvas boundary\n          if (c === 0 || walls[r][c - 1] === 0) {\n            const x = (c === 0) ? 1 : x0;\n            ctx.moveTo(x, y0);\n            ctx.lineTo(x, y1);\n          }\n          \/\/ Right edge: only if air to right or canvas boundary\n          if (c === COLS - 1 || walls[r][c + 1] === 0) {\n            const x = (c === COLS - 1) ? canvas.width - 1 : x1;\n            ctx.moveTo(x, y0);\n            ctx.lineTo(x, y1);\n          }\n        }\n      }\n    }\n    ctx.stroke();\n\n    \/\/ 3. GHOST HOUSE PINK GATE (row 12, cols 13-14) - ONLY in original map mode (never in \"Limpiar todo\")\n    if (isOriginalMap && walls[12][13] === 0 && walls[12][14] === 0) {\n      ctx.fillStyle = '#f472b6';\n      ctx.fillRect(13 * cellW, 12 * cellH + 6, cellW * 2, 4);\n    }\n\n    const targets = computeTargets();\n\n    \/\/ 4. DRAW PATHS \/ TRIGGER TILES (Corridor-based route per ghost)\n    const ghostKeys = ['blinky', 'pinky', 'inky', 'clyde'];\n    ghostKeys.forEach(k => {\n      const g = ghosts[k];\n      const t = targets[k];\n      const path = findPath(g.x, g.y, t.x, t.y);\n\n      if (path.length > 1) {\n        \/\/ Highlight corridor path cells with soft tint\n        ctx.fillStyle = g.color + '1a';\n        for (let idx = 0; idx < path.length; idx++) {\n          let [px, py] = path[idx];\n          ctx.fillRect(px * cellW, py * cellH, cellW, cellH);\n        }\n\n        \/\/ Draw dashed line along corridor centers\n        ctx.beginPath();\n        ctx.strokeStyle = g.color + 'bb';\n        ctx.lineWidth = 2.5;\n        ctx.setLineDash([3, 3]);\n\n        for (let idx = 0; idx < path.length; idx++) {\n          let [px, py] = path[idx];\n          let cx = (px + 0.5) * cellW;\n          let cy = (py + 0.5) * cellH;\n\n          \/\/ Skip drawing line across wrap-around tunnel\n          if (idx > 0) {\n            let [prevX] = path[idx - 1];\n            if (Math.abs(px - prevX) > 1) {\n              ctx.stroke();\n              ctx.beginPath();\n              ctx.moveTo(cx, cy);\n              continue;\n            }\n          }\n\n          if (idx === 0) ctx.moveTo(cx, cy);\n          else ctx.lineTo(cx, cy);\n        }\n        ctx.stroke();\n        ctx.setLineDash([]);\n      }\n\n      \/\/ Draw Target Tile Box\n      ctx.fillStyle = g.color + '33';\n      ctx.fillRect(t.x * cellW, t.y * cellH, cellW, cellH);\n      ctx.strokeStyle = g.color;\n      ctx.lineWidth = 2;\n      ctx.strokeRect(t.x * cellW + 1, t.y * cellH + 1, cellW - 2, cellH - 2);\n\n      \/\/ Target Crosshair \/ Center Mark\n      ctx.beginPath();\n      ctx.arc((t.x + 0.5) * cellW, (t.y + 0.5) * cellH, 3, 0, Math.PI * 2);\n      ctx.fillStyle = g.color;\n      ctx.fill();\n\n      \/\/ Ghost Token\n      ctx.beginPath();\n      ctx.arc((g.x + 0.5) * cellW, (g.y + 0.5) * cellH, cellW * 0.44, 0, Math.PI * 2);\n      ctx.fillStyle = g.color;\n      ctx.fill();\n      ctx.strokeStyle = '#ffffff';\n      ctx.lineWidth = 1;\n      ctx.stroke();\n\n      \/\/ Ghost Eyes looking along computed corridor direction\n      let lookAngle = 0;\n      if (path.length > 1) {\n        lookAngle = Math.atan2(path[1][1] - g.y, path[1][0] - g.x);\n      }\n      let e1x = (g.x + 0.38) * cellW + Math.cos(lookAngle) * 2;\n      let e1y = (g.y + 0.45) * cellH + Math.sin(lookAngle) * 2;\n      let e2x = (g.x + 0.62) * cellW + Math.cos(lookAngle) * 2;\n      let e2y = (g.y + 0.45) * cellH + Math.sin(lookAngle) * 2;\n      ctx.fillStyle = '#ffffff';\n      ctx.beginPath();\n      ctx.arc(e1x, e1y, 2, 0, Math.PI * 2);\n      ctx.arc(e2x, e2y, 2, 0, Math.PI * 2);\n      ctx.fill();\n\n      \/\/ Ghost name label if dragging\n      if (draggedEntity === k) {\n        ctx.fillStyle = '#ffffff';\n        ctx.font = 'bold 9px JetBrains Mono, monospace';\n        ctx.textAlign = 'center';\n        ctx.fillText(g.name, (g.x + 0.5) * cellW, (g.y - 0.2) * cellH);\n      }\n    });\n\n    \/\/ 5. DRAW PAC-MAN WITH ROTATING MOUTH\n    let pCenterX = (pac.x + 0.5) * cellW;\n    let pCenterY = (pac.y + 0.5) * cellH;\n    let radius = cellW * 0.46;\n\n    ctx.save();\n    ctx.translate(pCenterX, pCenterY);\n    ctx.rotate(pac.angle);\n\n    let mouthAngle = 0.25 * Math.PI;\n    ctx.beginPath();\n    ctx.arc(0, 0, radius, mouthAngle, 2 * Math.PI - mouthAngle);\n    ctx.lineTo(0, 0);\n    ctx.closePath();\n    ctx.fillStyle = '#facc15';\n    ctx.fill();\n    ctx.strokeStyle = '#ca8a04';\n    ctx.lineWidth = 1.2;\n    ctx.stroke();\n    ctx.restore();\n\n    \/\/ Direction arrow\n    ctx.beginPath();\n    ctx.moveTo(pCenterX, pCenterY);\n    ctx.lineTo(pCenterX + pac.dirX * cellW * 0.8, pCenterY + pac.dirY * cellH * 0.8);\n    ctx.strokeStyle = '#fef08a';\n    ctx.lineWidth = 2;\n    ctx.stroke();\n\n    \/\/ 6. UPDATE TELEMETRY PANEL\n    document.getElementById('f-blinky-coord').innerText = `[${targets.blinky.x}, ${targets.blinky.y}]`;\n    document.getElementById('f-pinky-coord').innerText  = `[${targets.pinky.x}, ${targets.pinky.y}]`;\n    document.getElementById('f-inky-coord').innerText   = `[${targets.inky.x}, ${targets.inky.y}]`;\n    document.getElementById('f-clyde-coord').innerText  = `[${targets.clyde.x}, ${targets.clyde.y}]`;\n    if (targets.clydeDist !== undefined) {\n      document.getElementById('f-clyde-desc').innerText = \n        `Distancia: ${targets.clydeDist.toFixed(1)} casillas (${targets.clydeDist < 8 ? 'Huyendo a esquina' : 'Persiguiendo'}).`;\n    }\n\n    let dirName = 'Derecha (&rarr;)';\n    if (pac.dirX === -1) dirName = 'Izquierda (&larr;)';\n    if (pac.dirY === -1) dirName = 'Arriba (&uarr;)';\n    if (pac.dirY === 1)  dirName = 'Abajo (&darr;)';\n    pacTelemetry.innerHTML = \n      `Pac-Man: [${pac.x}, ${pac.y}] | Mirando: ${dirName}<br>` +\n      `<span style=\"color:#666;\">Blinky:[${ghosts.blinky.x},${ghosts.blinky.y}] Pinky:[${ghosts.pinky.x},${ghosts.pinky.y}] Inky:[${ghosts.inky.x},${ghosts.inky.y}] Clyde:[${ghosts.clyde.x},${ghosts.clyde.y}]<\/span>`;\n  }\n\n  function movePacMan(newX, newY) {\n    \/\/ Tunnel wrap-around at row 14\n    if (newY === 14) {\n      if (newX < 0) newX = COLS - 1;\n      else if (newX >= COLS) newX = 0;\n    }\n\n    if (newX < 0 || newX >= COLS || newY < 0 || newY >= ROWS) return;\n    if (walls[newY][newX] === 1 && draggedEntity !== 'pacman') return;\n\n    let dx = newX - pac.x;\n    let dy = newY - pac.y;\n\n    if (dx !== 0 || dy !== 0) {\n      if (typeof playTileStepSound === 'function') playTileStepSound();\n      if (Math.abs(dx) >= Math.abs(dy) && dx !== 0) {\n        pac.dirX = dx > 0 ? 1 : -1;\n        pac.dirY = 0;\n        pac.angle = pac.dirX === 1 ? 0 : Math.PI;\n      } else if (dy !== 0) {\n        pac.dirX = 0;\n        pac.dirY = dy > 0 ? 1 : -1;\n        pac.angle = pac.dirY === 1 ? 0.5 * Math.PI : 1.5 * Math.PI;\n      }\n    }\n\n    pac.x = newX;\n    pac.y = newY;\n    draw();\n  }\n\n  \/\/ FOCUS MANAGEMENT\n  function setFocus(active) {\n    hasKeyboardFocus = active;\n    if (active) {\n      focusBox.classList.add('active-focus');\n    } else {\n      focusBox.classList.remove('active-focus');\n    }\n  }\n\n  focusBox.addEventListener('focus', () => setFocus(true));\n  focusBox.addEventListener('blur', () => setFocus(false));\n  document.addEventListener('click', function(e) {\n    if (!focusBox.contains(e.target)) {\n      setFocus(false);\n    }\n  });\n\n  \/\/ KEYBOARD ARROW CONTROLS (ONLY WHEN FOCUSED)\n  window.addEventListener('keydown', function(e) {\n    if (!hasKeyboardFocus) return;\n\n    if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {\n      e.preventDefault();\n      if (e.key === 'ArrowUp')    movePacMan(pac.x, pac.y - 1);\n      if (e.key === 'ArrowDown')  movePacMan(pac.x, pac.y + 1);\n      if (e.key === 'ArrowLeft')  movePacMan(pac.x - 1, pac.y);\n      if (e.key === 'ArrowRight') movePacMan(pac.x + 1, pac.y);\n    }\n    if (e.key === 'Escape') {\n      setFocus(false);\n      focusBox.blur();\n    }\n  });\n\n  \/\/ CELL HIT DETECTION\n  function getCanvasCell(e) {\n    const rect = canvas.getBoundingClientRect();\n    const clientX = e.clientX || (e.touches && e.touches[0].clientX);\n    const clientY = e.clientY || (e.touches && e.touches[0].clientY);\n    if (clientX === undefined) return null;\n    const c = Math.floor(((clientX - rect.left) \/ rect.width) * COLS);\n    const r = Math.floor(((clientY - rect.top) \/ rect.height) * ROWS);\n    return {\n      x: Math.max(0, Math.min(COLS - 1, c)),\n      y: Math.max(0, Math.min(ROWS - 1, r))\n    };\n  }\n\n  function findEntityAt(cell) {\n    if (Math.abs(cell.x - pac.x) <= 0.9 && Math.abs(cell.y - pac.y) <= 0.9) {\n      return 'pacman';\n    }\n    for (let k of ['blinky', 'pinky', 'inky', 'clyde']) {\n      if (Math.abs(cell.x - ghosts[k].x) <= 0.9 && Math.abs(cell.y - ghosts[k].y) <= 0.9) {\n        return k;\n      }\n    }\n    return null;\n  }\n\n  canvas.addEventListener('pointerdown', function(e) {\n    setFocus(true);\n    focusBox.focus();\n    canvas.setPointerCapture(e.pointerId);\n\n    const cell = getCanvasCell(e);\n    if (!cell) return;\n\n    const hit = findEntityAt(cell);\n    if (hit) {\n      draggedEntity = hit;\n      if (hit === 'pacman') {\n        movePacMan(cell.x, cell.y);\n      } else {\n        if (typeof playGhostGrabSound === 'function') {\n          playGhostGrabSound(hit);\n        }\n      }\n      draw();\n      return;\n    }\n\n    if (isDrawMode) {\n      walls[cell.y][cell.x] = walls[cell.y][cell.x] === 1 ? 0 : 1;\n      if (typeof playButtonClickSound === 'function') playButtonClickSound();\n      draw();\n      return;\n    }\n\n    movePacMan(cell.x, cell.y);\n  });\n\n  canvas.addEventListener('pointermove', function(e) {\n    const cell = getCanvasCell(e);\n    if (!cell) return;\n\n    if (draggedEntity) {\n      if (draggedEntity === 'pacman') {\n        movePacMan(cell.x, cell.y);\n      } else {\n        const ghost = ghosts[draggedEntity];\n        if (ghost && (ghost.x !== cell.x || ghost.y !== cell.y)) {\n          ghost.x = cell.x;\n          ghost.y = cell.y;\n          if (typeof playGhostStepSound === 'function') {\n            playGhostStepSound(draggedEntity);\n          }\n          draw();\n        }\n      }\n      return;\n    }\n\n    if (isDrawMode && e.buttons === 1) {\n      if (!findEntityAt(cell)) {\n        if (walls[cell.y][cell.x] !== 1) {\n          walls[cell.y][cell.x] = 1;\n          if (typeof playSliderTickSound === 'function') playSliderTickSound(cell.x + cell.y);\n          draw();\n        }\n      }\n    }\n  });\n\n  canvas.addEventListener('pointerup', function() {\n    if (draggedEntity && draggedEntity !== 'pacman') {\n      if (typeof playGhostDropSound === 'function') {\n        playGhostDropSound(draggedEntity);\n      }\n    }\n    draggedEntity = null;\n    draw();\n  });\n\n  \/\/ OBSTACLES TOOLBAR BUTTONS (Clean, no emojis)\n  const btnClear = document.getElementById('btn-clear-walls');\n  const btnOrig = document.getElementById('btn-orig-walls');\n  const btnDraw = document.getElementById('btn-draw-walls');\n\n  btnOrig.addEventListener('click', function() {\n    walls = ORIGINAL_MAZE.map(row => [...row]);\n    isOriginalMap = true;\n    isDrawMode = false;\n    btnDraw.classList.remove('active');\n    btnClear.classList.remove('active');\n    this.classList.add('active');\n    \/\/ Reset positions to arcade defaults outside the cell\n    pac = { x: 13, y: 23, dirX: 1, dirY: 0, angle: 0 };\n    ghosts.blinky.x = 21; ghosts.blinky.y = 5;\n    ghosts.pinky.x  = 6;  ghosts.pinky.y  = 5;\n    ghosts.inky.x   = 21; ghosts.inky.y   = 20;\n    ghosts.clyde.x  = 6;  ghosts.clyde.y  = 20;\n    draw();\n  });\n\n  btnDraw.addEventListener('click', function() {\n    isDrawMode = !isDrawMode;\n    this.classList.toggle('active', isDrawMode);\n    if (isDrawMode) {\n      btnOrig.classList.remove('active');\n      btnClear.classList.remove('active');\n    }\n    draw();\n  });\n\n  btnClear.addEventListener('click', function() {\n    walls = Array(ROWS).fill(0).map(() => Array(COLS).fill(0));\n    isOriginalMap = false;\n    isDrawMode = false;\n    btnDraw.classList.remove('active');\n    btnOrig.classList.remove('active');\n    this.classList.add('active');\n    draw();\n  });\n\n  \/\/ EXPANDABLE GHOST DOCUMENTATION PANEL\n  const btnToggleDoc = document.getElementById('btn-toggle-ghost-doc');\n  const btnCloseDoc = document.getElementById('btn-close-ghost-doc');\n  const docPanel = document.getElementById('ghost-deep-dive');\n\n  function toggleGhostDoc(open, highlightGhost) {\n    const shouldOpen = open !== undefined ? open : !docPanel.classList.contains('active');\n    docPanel.classList.toggle('active', shouldOpen);\n    if (btnToggleDoc) {\n      btnToggleDoc.innerHTML = shouldOpen \n        ? 'Ocultar an&aacute;lisis matem&aacute;tico &uarr;' \n        : 'Ver an&aacute;lisis matem&aacute;tico y pedag&oacute;gico &darr;';\n    }\n    if (shouldOpen && highlightGhost) {\n      document.querySelectorAll('.ghost-card').forEach(c => c.classList.remove('highlighted'));\n      const card = document.getElementById(`card-${highlightGhost}`);\n      if (card) {\n        card.classList.add('highlighted');\n        card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n      }\n    }\n  }\n\n  if (btnToggleDoc) {\n    btnToggleDoc.addEventListener('click', () => toggleGhostDoc());\n  }\n  if (btnCloseDoc) {\n    btnCloseDoc.addEventListener('click', () => toggleGhostDoc(false));\n  }\n\n  document.querySelectorAll('.lab-formula-row').forEach(row => {\n    row.style.cursor = 'pointer';\n    row.addEventListener('click', function() {\n      const g = this.getAttribute('data-ghost');\n      if (g) {\n        toggleGhostDoc(true, g);\n        if (typeof playGhostGrabSound === 'function') playGhostGrabSound(g);\n      }\n    });\n  });\n\n  draw();\n\n})();\n\n  \/* ========================================================\n     MODULE 2: METAL GEAR SOLID (1998) STEALTH FSM SIMULATOR (V3 COMBAT & KILL)\n     ======================================================== *\/\n  (function initMgsSimulation() {\n    const canvas = document.getElementById('mgs-canvas');\n    if (!canvas) return;\n    const ctx = canvas.getContext('2d');\n\n    \/\/ Disable context menu on canvas (prevents copy image \/ save image)\n    canvas.addEventListener('contextmenu', function(e) {\n      e.preventDefault();\n      return false;\n    });\n\n    \/\/ Prevent dragging canvas as an image\n    canvas.addEventListener('dragstart', function(e) {\n      e.preventDefault();\n      return false;\n    });\n\n    \/\/ UI Elements\n    const hudState = document.getElementById('mgs-hud-state');\n    const hudHp = document.getElementById('mgs-hud-hp');\n    const hudLos = document.getElementById('mgs-hud-los');\n    const codecLog = document.getElementById('mgs-codec-log');\n    const btnKnock = document.getElementById('mgs-btn-knock');\n    const btnBox = document.getElementById('mgs-btn-box');\n    const btnSpeed = document.getElementById('mgs-btn-speed');\n    const btnPause = document.getElementById('mgs-btn-pause');\n    const btnReset = document.getElementById('mgs-btn-reset');\n    const sliderFov = document.getElementById('mgs-slider-fov');\n    const sliderDist = document.getElementById('mgs-slider-dist');\n    const sliderTimer = document.getElementById('mgs-slider-timer');\n    const valFov = document.getElementById('val-fov');\n    const valDist = document.getElementById('val-dist');\n    const valTimer = document.getElementById('val-timer');\n\n    const cards = {\n      patrol: document.getElementById('card-patrol'),\n      suspicious: document.getElementById('card-suspicious'),\n      alert: document.getElementById('card-alert'),\n      busqueda: document.getElementById('card-busqueda') || document.getElementById('card-evasion')\n    };\n\n    \/\/ UX Simulation Speed and Pause controls\n    let simSpeedScale = 1.0;\n    let isPaused = false;\n\n    try {\n      const savedSpeed = localStorage.getItem('cpue_mgs_speed');\n      if (savedSpeed !== null && btnSpeed) {\n        simSpeedScale = parseFloat(savedSpeed) || 1.0;\n        btnSpeed.innerText = simSpeedScale === 1.0 ? 'Velocidad: 1.0x' : 'Velocidad: 0.5x (Lenta)';\n        btnSpeed.classList.toggle('active-toggle', simSpeedScale === 0.5);\n      }\n    } catch (e) {}\n\n    if (btnSpeed) {\n      btnSpeed.addEventListener('click', () => {\n        simSpeedScale = simSpeedScale === 1.0 ? 0.5 : 1.0;\n        btnSpeed.innerText = simSpeedScale === 1.0 ? 'Velocidad: 1.0x' : 'Velocidad: 0.5x (Lenta)';\n        btnSpeed.classList.toggle('active-toggle', simSpeedScale === 0.5);\n        logEvent(`CODEC: Velocidad ajustada a ${simSpeedScale}x.`);\n        try {\n          localStorage.setItem('cpue_mgs_speed', simSpeedScale.toString());\n        } catch (e) {}\n      });\n    }\n\n    if (btnPause) {\n      btnPause.addEventListener('click', () => {\n        isPaused = !isPaused;\n        btnPause.innerText = isPaused ? 'Reanudar' : 'Pausar';\n        btnPause.classList.toggle('active-toggle', isPaused);\n        logEvent(isPaused ? 'CODEC: Pausa tactica activada.' : 'CODEC: Simulacion reanudada.');\n      });\n    }\n\n    \/\/ Parameters (Live adjustable with localStorage persistence)\n    try {\n      const savedFov = localStorage.getItem('cpue_guard_fov');\n      if (savedFov !== null && sliderFov) {\n        sliderFov.value = savedFov;\n        if (valFov) valFov.innerHTML = savedFov + '&deg;';\n      }\n      const savedDist = localStorage.getItem('cpue_guard_dist');\n      if (savedDist !== null && sliderDist) {\n        sliderDist.value = savedDist;\n        if (valDist) valDist.innerText = savedDist + 'px';\n      }\n      const savedTimer = localStorage.getItem('cpue_guard_timer');\n      if (savedTimer !== null && sliderTimer) {\n        sliderTimer.value = savedTimer;\n        if (valTimer) valTimer.innerText = savedTimer + 's';\n      }\n    } catch (e) {}\n\n    let fovDegrees = parseInt(sliderFov.value, 10) || 110;\n    let fovDistance = parseInt(sliderDist.value, 10) || 230;\n    let busquedaDuration = parseInt(sliderTimer.value, 10) || 6;\n\n    sliderFov.addEventListener('input', e => {\n      fovDegrees = parseInt(e.target.value, 10);\n      valFov.innerHTML = fovDegrees + '&deg;';\n      try {\n        localStorage.setItem('cpue_guard_fov', fovDegrees.toString());\n      } catch (err) {}\n    });\n    sliderDist.addEventListener('input', e => {\n      fovDistance = parseInt(e.target.value, 10);\n      valDist.innerText = fovDistance + 'px';\n      try {\n        localStorage.setItem('cpue_guard_dist', fovDistance.toString());\n      } catch (err) {}\n    });\n    sliderTimer.addEventListener('input', e => {\n      busquedaDuration = parseInt(e.target.value, 10);\n      valTimer.innerText = busquedaDuration + 's';\n      try {\n        localStorage.setItem('cpue_guard_timer', busquedaDuration.toString());\n      } catch (err) {}\n    });\n\n    \/\/ 3 SOLID COLLISION BLOCKS (Arena: 480 x 340) - Clean, NO text printed on them\n    const obstacles = [\n      { x: 85,  y: 75,  w: 70, h: 95 },\n      { x: 205, y: 160, w: 75, h: 80 },\n      { x: 325, y: 75,  w: 70, h: 95 }\n    ];\n\n    \/\/ Perimeter waypoints for smooth patrol\n    const waypoints = [\n      { x: 45,  y: 40 },\n      { x: 435, y: 40 },\n      { x: 435, y: 295 },\n      { x: 45,  y: 295 }\n    ];\n\n    \/\/ Exact Euclidean Distance from point (px, py) to AABB Box\n    function pointDistanceToBox(px, py, obs) {\n      const nearestX = Math.max(obs.x, Math.min(px, obs.x + obs.w));\n      const nearestY = Math.max(obs.y, Math.min(py, obs.y + obs.h));\n      const dx = px - nearestX;\n      const dy = py - nearestY;\n      return Math.hypot(dx, dy);\n    }\n\n    \/\/ High-Precision Ray-To-Box Clearance Test (No false positives on near-obstacle goals)\n    function isSegmentClear(x1, y1, x2, y2, radius = 11) {\n      const dx = x2 - x1;\n      const dy = y2 - y1;\n      const dist = Math.hypot(dx, dy);\n      const steps = Math.max(6, Math.ceil(dist \/ 3));\n      const threshold = radius - 1.0; \/\/ 10px buffer prevents obstacle clipping\n      for (let i = 1; i < steps; i++) {\n        const px = x1 + (dx * i) \/ steps;\n        const py = y1 + (dy * i) \/ steps;\n        for (let j = 0; j < obstacles.length; j++) {\n          if (pointDistanceToBox(px, py, obstacles[j]) < threshold) {\n            return false;\n          }\n        }\n      }\n      return true;\n    }\n\n    \/\/ Clamp goal to free space outside obstacles with accurate Euclidean distance\n    function clampGoalToFreeSpace(gx, gy, radius = 11) {\n      const pad = radius + 2.5; \/\/ 13.5px safe margin\n      let cx = Math.max(pad, Math.min(canvas.width - pad, gx));\n      let cy = Math.max(pad, Math.min(canvas.height - pad, gy));\n      for (let i = 0; i < obstacles.length; i++) {\n        const obs = obstacles[i];\n        const nearestX = Math.max(obs.x, Math.min(cx, obs.x + obs.w));\n        const nearestY = Math.max(obs.y, Math.min(cy, obs.y + obs.h));\n        const dx = cx - nearestX;\n        const dy = cy - nearestY;\n        const dist = Math.hypot(dx, dy);\n        if (dist < pad) {\n          if (dist > 0.001) {\n            cx = nearestX + (dx \/ dist) * pad;\n            cy = nearestY + (dy \/ dist) * pad;\n          } else {\n            const dl = Math.abs(cx - obs.x);\n            const dr = Math.abs(cx - (obs.x + obs.w));\n            const dt = Math.abs(cy - obs.y);\n            const db = Math.abs(cy - (obs.y + obs.h));\n            const minD = Math.min(dl, dr, dt, db);\n            if (minD === dl) cx = obs.x - pad;\n            else if (minD === dr) cx = obs.x + obs.w + pad;\n            else if (minD === dt) cy = obs.y - pad;\n            else cy = obs.y + obs.h + pad;\n          }\n        }\n      }\n      return {\n        x: Math.max(pad, Math.min(canvas.width - pad, cx)),\n        y: Math.max(pad, Math.min(canvas.height - pad, cy))\n      };\n    }\n\n    \/\/ Precomputed clearance corners for Visibility Graph Pathfinder (18px margin)\n    function generateClearanceCorners(clearance = 18) {\n      const list = [];\n      for (let i = 0; i < obstacles.length; i++) {\n        const obs = obstacles[i];\n        const candidates = [\n          { x: obs.x - clearance, y: obs.y - clearance },\n          { x: obs.x + obs.w + clearance, y: obs.y - clearance },\n          { x: obs.x - clearance, y: obs.y + obs.h + clearance },\n          { x: obs.x + obs.w + clearance, y: obs.y + obs.h + clearance }\n        ];\n        for (let c = 0; c < candidates.length; c++) {\n          const pt = candidates[c];\n          const cx = Math.max(clearance, Math.min(canvas.width - clearance, pt.x));\n          const cy = Math.max(clearance, Math.min(canvas.height - clearance, pt.y));\n          let inside = false;\n          for (let j = 0; j < obstacles.length; j++) {\n            if (pointDistanceToBox(cx, cy, obstacles[j]) < 10) {\n              inside = true;\n              break;\n            }\n          }\n          if (!inside) list.push({ x: cx, y: cy });\n        }\n      }\n      return list;\n    }\n\n    const staticCorners = generateClearanceCorners(18);\n\n    \/\/ Exact Visibility Graph Shortest-Path Finder (Dijkstra)\n    function findPath(startX, startY, goalX, goalY, radius = 11) {\n      if (isSegmentClear(startX, startY, goalX, goalY, radius)) {\n        return [{ x: goalX, y: goalY }];\n      }\n\n      const nodes = [{ x: startX, y: startY }];\n      for (let i = 0; i < staticCorners.length; i++) {\n        nodes.push(staticCorners[i]);\n      }\n      const goalIndex = nodes.length;\n      nodes.push({ x: goalX, y: goalY });\n\n      const n = nodes.length;\n      const dist = new Array(n).fill(Infinity);\n      const prev = new Array(n).fill(-1);\n      const visited = new Array(n).fill(false);\n\n      dist[0] = 0;\n\n      for (let step = 0; step < n; step++) {\n        let u = -1;\n        let minDist = Infinity;\n        for (let i = 0; i < n; i++) {\n          if (!visited[i] && dist[i] < minDist) {\n            minDist = dist[i];\n            u = i;\n          }\n        }\n        if (u === -1 || u === goalIndex) break;\n        visited[u] = true;\n\n        for (let v = 0; v < n; v++) {\n          if (visited[v] || u === v) continue;\n          if (isSegmentClear(nodes[u].x, nodes[u].y, nodes[v].x, nodes[v].y, radius)) {\n            const edgeDist = Math.hypot(nodes[v].x - nodes[u].x, nodes[v].y - nodes[u].y);\n            const alt = dist[u] + edgeDist;\n            if (alt < dist[v]) {\n              dist[v] = alt;\n              prev[v] = u;\n            }\n          }\n        }\n      }\n\n      if (dist[goalIndex] === Infinity) {\n        return [{ x: goalX, y: goalY }];\n      }\n\n      const path = [];\n      let curr = goalIndex;\n      while (curr !== -1 && curr !== 0) {\n        path.unshift({ x: nodes[curr].x, y: nodes[curr].y });\n        curr = prev[curr];\n      }\n\n      return path;\n    }\n\n    \/\/ Helper: Find a secondary corner to inspect during Busqueda\n    function findSecondarySearchPoint(currentX, currentY) {\n      let bestPoint = null;\n      let bestDist = Infinity;\n      for (let i = 0; i < staticCorners.length; i++) {\n        const c = staticCorners[i];\n        const d = Math.hypot(c.x - currentX, c.y - currentY);\n        if (d > 45 && d < 155) {\n          if (d < bestDist) {\n            bestDist = d;\n            bestPoint = c;\n          }\n        }\n      }\n      return bestPoint || { x: currentX + (currentX < 240 ? 55 : -55), y: currentY };\n    }\n\n    \/\/ Snake State\n    const snake = {\n      x: 45,\n      y: 170,\n      radius: 11,\n      speed: 1.55,\n      hp: 100,\n      isDead: false,\n      damageFlash: 0,\n      isBox: false,\n      isMoving: false,\n      targetX: null,\n      targetY: null,\n      path: [],\n      keys: { w: false, a: false, s: false, d: false }\n    };\n\n    \/\/ Guard State\n    const guard = {\n      x: 45,\n      y: 40,\n      angle: 0,\n      wpIndex: 1,\n      path: [],\n      state: 'patrol',\n      stateTime: 0,\n      freezeTimer: 0,\n      shotCooldown: 0,\n      muzzleFlash: 0,\n      investigateTarget: { x: 0, y: 0 },\n      investigatePauseTimer: 0,\n      busquedaTimer: 6,\n      busquedaStage: 'travel',\n      busquedaStageTime: 0,\n      busquedaBaseAngle: 0,\n      lastSeenSnake: { x: 0, y: 0 },\n      alertAnimPulse: 0\n    };\n\n    \/\/ Combat FX\n    let bulletTracer = null;\n    let screenShake = 0;\n    let soundWaves = [];\n\n    function normalizeAngle(a) {\n      while (a > Math.PI) a -= Math.PI * 2;\n      while (a < -Math.PI) a += Math.PI * 2;\n      return a;\n    }\n\n    \/\/ Health HUD Update\n    function updateHpUI() {\n      if (!hudHp) return;\n      hudHp.innerText = `VIDA: ${Math.max(0, snake.hp)}%`;\n      if (snake.hp > 60) {\n        hudHp.style.color = '#4ade80';\n        hudHp.style.borderColor = '#22c55e';\n      } else if (snake.hp > 30) {\n        hudHp.style.color = '#eab308';\n        hudHp.style.borderColor = '#ca8a04';\n      } else {\n        hudHp.style.color = '#ef4444';\n        hudHp.style.borderColor = '#dc2626';\n      }\n    }\n\n    \/\/ Clean Codec Log (Clean, direct tactical telemetry)\n    function logEvent(msg) {\n      if (codecLog) codecLog.innerHTML = msg;\n    }\n\n    \/\/ Damage & Kill logic\n    function damageSnake(amount, reason) {\n      if (snake.isDead) return;\n      snake.hp -= amount;\n      snake.damageFlash = 0.35;\n      screenShake = 0.25;\n      updateHpUI();\n\n      if (snake.hp <= 0) {\n        snake.hp = 0;\n        snake.isDead = true;\n        snake.isMoving = false;\n        snake.targetX = null;\n        snake.targetY = null;\n        snake.path = [];\n        logEvent('<strong style=\"color:#ef4444;\">MISION FALLIDA:<\/strong> Snake ha sido neutralizado. Pulsa R o haz clic para reintentar.');\n      } else {\n        logEvent(`<span style=\"color:#f87171;\"><strong>IMPACTO:<\/strong> Salud de Snake: ${snake.hp}%. (${reason})<\/span>`);\n      }\n    }\n\n    \/\/ Guard fires weapon\n    function guardShoot() {\n      if (snake.isDead) return;\n      guard.muzzleFlash = 0.18;\n      const gunX = guard.x + Math.cos(guard.angle) * 16;\n      const gunY = guard.y + Math.sin(guard.angle) * 16;\n\n      bulletTracer = {\n        x1: gunX,\n        y1: gunY,\n        x2: snake.x,\n        y2: snake.y,\n        life: 0.22\n      };\n\n      damageSnake(35, 'Fuego de fusil');\n    }\n\n    \/\/ Circle vs AABB Collision Resolution (Continuous sliding physics)\n    function resolveEntityCollision(entity, radius) {\n      const pad = radius + 2;\n      if (entity.x < pad) entity.x = pad;\n      if (entity.x > canvas.width - pad) entity.x = canvas.width - pad;\n      if (entity.y < pad) entity.y = pad;\n      if (entity.y > canvas.height - pad) entity.y = canvas.height - pad;\n\n      for (let i = 0; i < obstacles.length; i++) {\n        const obs = obstacles[i];\n        const closestX = Math.max(obs.x, Math.min(entity.x, obs.x + obs.w));\n        const closestY = Math.max(obs.y, Math.min(entity.y, obs.y + obs.h));\n\n        const dx = entity.x - closestX;\n        const dy = entity.y - closestY;\n        const distSq = dx * dx + dy * dy;\n\n        if (distSq < radius * radius) {\n          const dist = Math.sqrt(distSq);\n          if (dist > 0.001) {\n            const overlap = radius - dist;\n            entity.x += (dx \/ dist) * overlap;\n            entity.y += (dy \/ dist) * overlap;\n          } else {\n            const dl = Math.abs(entity.x - obs.x);\n            const dr = Math.abs(entity.x - (obs.x + obs.w));\n            const dt = Math.abs(entity.y - obs.y);\n            const db = Math.abs(entity.y - (obs.y + obs.h));\n            const minD = Math.min(dl, dr, dt, db);\n            if (minD === dl) entity.x = obs.x - radius;\n            else if (minD === dr) entity.x = obs.x + obs.w + radius;\n            else if (minD === dt) entity.y = obs.y - radius;\n            else entity.y = obs.y + obs.h + radius;\n          }\n        }\n      }\n    }\n\n    \/\/ Fast Line-Of-Sight Check\n    function hasLineOfSight(x1, y1, x2, y2) {\n      const dx = x2 - x1;\n      const dy = y2 - y1;\n      const dist = Math.hypot(dx, dy);\n      const steps = Math.max(12, Math.floor(dist \/ 4));\n      for (let i = 1; i < steps; i++) {\n        const px = x1 + (dx * i) \/ steps;\n        const py = y1 + (dy * i) \/ steps;\n        for (let j = 0; j < obstacles.length; j++) {\n          const obs = obstacles[j];\n          if (px >= obs.x && px <= obs.x + obs.w && py >= obs.y && py <= obs.y + obs.h) {\n            return false;\n          }\n        }\n      }\n      return true;\n    }\n\n    \/\/ State transition with clean, professional Codec feedback\n    function setGuardState(newState, reason) {\n      if (guard.state === newState) return;\n      guard.state = newState;\n      guard.stateTime = 0;\n\n      if (newState === 'alert') {\n        guard.freezeTimer = 0.55;\n        guard.alertAnimPulse = 1.0;\n        guard.shotCooldown = 0.8;\n        playAlertSound();\n      } else if (newState === 'suspicious') {\n        guard.freezeTimer = 0.45;\n      } else if (newState === 'busqueda') {\n        guard.busquedaTimer = busquedaDuration;\n        guard.busquedaStage = 'travel';\n        guard.busquedaStageTime = 0;\n        guard.busquedaBaseAngle = guard.angle;\n        guard.path = findPath(guard.x, guard.y, guard.lastSeenSnake.x, guard.lastSeenSnake.y, 11);\n      }\n\n      Object.keys(cards).forEach(k => {\n        if (cards[k]) cards[k].classList.remove('active-state');\n      });\n      if (cards[newState]) cards[newState].classList.add('active-state');\n\n      const labels = {\n        patrol: 'ESTADO: PATRULLA',\n        suspicious: 'ESTADO: SOSPECHA (?)',\n        alert: 'ESTADO: ALERTA (!)',\n        busqueda: 'ESTADO: BUSQUEDA'\n      };\n      hudState.innerText = labels[newState] || newState.toUpperCase();\n\n      if (newState === 'alert') {\n        hudState.style.borderColor = '#ef4444';\n        hudState.style.color = '#ef4444';\n      } else if (newState === 'suspicious') {\n        hudState.style.borderColor = '#eab308';\n        hudState.style.color = '#eab308';\n      } else if (newState === 'busqueda') {\n        hudState.style.borderColor = '#f97316';\n        hudState.style.color = '#f97316';\n      } else {\n        hudState.style.borderColor = '#334155';\n        hudState.style.color = '#94a3b8';\n      }\n\n      const codecMessages = {\n        patrol: 'CODEC: Guardia en patrulla. Perimetro bajo control.',\n        suspicious: 'CODEC: Estimulo acustico detectado. Guardia investigando origen.',\n        alert: '<strong style=\"color:#ef4444;\">CODEC: ALERTA MAXIMA! Intruso avistado. Guardia abriendo fuego.<\/strong>',\n        busqueda: '<strong style=\"color:#f97316;\">CODEC: BUSQUEDA: Contacto visual perdido tras cobertura. Rastreando perimetro.<\/strong>'\n      };\n      logEvent(codecMessages[newState] || `Estado: ${newState.toUpperCase()}`);\n    }\n\n    function triggerNoise(x, y, maxR = 175, label = 'Golpe tactico') {\n      if (snake.isDead) return;\n      soundWaves.push({ x, y, r: 0, maxR, alpha: 1.0, triggered: false });\n      logEvent(`CODEC: Estimulo acustico generado (${label}).`);\n    }\n\n    btnKnock.addEventListener('click', () => {\n      triggerNoise(snake.x, snake.y, 175, 'Golpe en pared');\n      playKnock();\n    });\n\n    function unequipBox(reason) {\n      if (snake.isBox) {\n        snake.isBox = false;\n        btnBox.classList.remove('active-toggle');\n        btnBox.innerText = 'Equipar Caja de Carton';\n        if (reason) {\n          logEvent(`<strong style=\"color:#ef4444;\">CODEC:<\/strong> ${reason}`);\n        }\n      }\n    }\n\n    btnBox.addEventListener('click', () => {\n      if (snake.isDead) return;\n      snake.isBox = !snake.isBox;\n      btnBox.classList.toggle('active-toggle', snake.isBox);\n      btnBox.innerText = snake.isBox ? 'Quitar Caja de Carton' : 'Equipar Caja de Carton';\n      if (typeof playCardboardSound === 'function') playCardboardSound(snake.isBox);\n      logEvent(snake.isBox ?\n        'CODEC: Snake equipado con Caja de Carton. Inmovil = Invisible al cono.' :\n        'CODEC: Snake sale de la Caja de Carton.'\n      );\n    });\n\n    function restartSimulation() {\n      snake.x = 45;\n      snake.y = 170;\n      snake.targetX = null;\n      snake.targetY = null;\n      snake.path = [];\n      snake.hp = 100;\n      snake.isDead = false;\n      snake.damageFlash = 0;\n      snake.isBox = false;\n      btnBox.classList.remove('active-toggle');\n      btnBox.innerText = 'Equipar Caja de Carton';\n      updateHpUI();\n\n      guard.x = waypoints[0].x;\n      guard.y = waypoints[0].y;\n      guard.angle = 0;\n      guard.wpIndex = 1;\n      guard.path = [];\n      guard.freezeTimer = 0;\n      guard.shotCooldown = 0;\n      guard.muzzleFlash = 0;\n      bulletTracer = null;\n      screenShake = 0;\n      soundWaves = [];\n      setGuardState('patrol', 'Reinicio.');\n    }\n\n    btnReset.addEventListener('click', restartSimulation);\n\n    \/\/ Keyboard handlers\n    window.addEventListener('keydown', e => {\n      const k = e.key.toLowerCase();\n      if (snake.isDead) {\n        if (k === ' ' || k === 'r' || k === 'enter') restartSimulation();\n        return;\n      }\n      if (['w', 'arrowup'].includes(k)) snake.keys.w = true;\n      if (['a', 'arrowleft'].includes(k)) snake.keys.a = true;\n      if (['s', 'arrowdown'].includes(k)) snake.keys.s = true;\n      if (['d', 'arrowright'].includes(k)) snake.keys.d = true;\n      if (k === 'b') btnBox.click();\n      if (k === 'r') restartSimulation();\n    });\n\n    window.addEventListener('keyup', e => {\n      const k = e.key.toLowerCase();\n      if (['w', 'arrowup'].includes(k)) snake.keys.w = false;\n      if (['a', 'arrowleft'].includes(k)) snake.keys.a = false;\n      if (['s', 'arrowdown'].includes(k)) snake.keys.s = false;\n      if (['d', 'arrowright'].includes(k)) snake.keys.d = false;\n    });\n\n    \/\/ Pointer down (Click\/Tap to move with robust Visibility Graph Pathfinder)\n    canvas.addEventListener('pointerdown', e => {\n      if (snake.isDead) {\n        restartSimulation();\n        return;\n      }\n      const rect = canvas.getBoundingClientRect();\n      const scaleX = canvas.width \/ rect.width;\n      const scaleY = canvas.height \/ rect.height;\n      const cx = (e.clientX - rect.left) * scaleX;\n      const cy = (e.clientY - rect.top) * scaleY;\n\n      const goal = clampGoalToFreeSpace(cx, cy, snake.radius);\n      snake.targetX = goal.x;\n      snake.targetY = goal.y;\n      snake.path = findPath(snake.x, snake.y, goal.x, goal.y, snake.radius);\n      snake.isMoving = true;\n    });\n\n    \/\/ Simulation Loop\n    let lastTime = performance.now();\n\n    function update(dt) {\n      if (isPaused) return;\n\n      const effectiveDt = dt * simSpeedScale;\n\n      \/\/ Update Screen Shake & FX\n      if (screenShake > 0) screenShake -= effectiveDt;\n      if (snake.damageFlash > 0) snake.damageFlash -= effectiveDt;\n      if (guard.muzzleFlash > 0) guard.muzzleFlash -= effectiveDt;\n      if (bulletTracer) {\n        bulletTracer.life -= effectiveDt;\n        if (bulletTracer.life <= 0) bulletTracer = null;\n      }\n\n      \/\/ 1. Move Snake (Keyboard or Pathfinder Waypoints Queue)\n      if (!snake.isDead) {\n        let mx = 0;\n        let my = 0;\n        if (snake.keys.w) my -= 1;\n        if (snake.keys.s) my += 1;\n        if (snake.keys.a) mx -= 1;\n        if (snake.keys.d) mx += 1;\n\n        const currentSpeed = (snake.isBox ? 0.80 : snake.speed) * simSpeedScale;\n\n        if (mx !== 0 || my !== 0) {\n          snake.targetX = null;\n          snake.targetY = null;\n          snake.path = [];\n          const len = Math.hypot(mx, my);\n          snake.x += (mx \/ len) * currentSpeed;\n          snake.y += (my \/ len) * currentSpeed;\n          resolveEntityCollision(snake, snake.radius);\n          snake.isMoving = true;\n        } else if (snake.path && snake.path.length > 0) {\n          \/\/ Dynamic line-of-sight shortcutting: if unblocked direct line to final goal, skip waypoints!\n          if (snake.path.length > 1) {\n            const finalWp = snake.path[snake.path.length - 1];\n            if (isSegmentClear(snake.x, snake.y, finalWp.x, finalWp.y, snake.radius)) {\n              snake.path = [finalWp];\n            } else {\n              const nextWp = snake.path[1];\n              if (isSegmentClear(snake.x, snake.y, nextWp.x, nextWp.y, snake.radius)) {\n                snake.path.shift();\n              }\n            }\n          }\n\n          const wp = snake.path[0];\n          const dx = wp.x - snake.x;\n          const dy = wp.y - snake.y;\n          const dist = Math.hypot(dx, dy);\n\n          if (dist <= Math.max(3.5, currentSpeed)) {\n            snake.path.shift();\n            if (snake.path.length === 0) {\n              snake.targetX = null;\n              snake.targetY = null;\n              snake.isMoving = false;\n            }\n          } else {\n            const moveDist = Math.min(dist, currentSpeed);\n            snake.x += (dx \/ dist) * moveDist;\n            snake.y += (dy \/ dist) * moveDist;\n            resolveEntityCollision(snake, snake.radius);\n            snake.isMoving = true;\n          }\n        } else {\n          snake.isMoving = false;\n        }\n      }\n\n      \/\/ 2. Update Sound Waves\n      for (let i = soundWaves.length - 1; i >= 0; i--) {\n        const sw = soundWaves[i];\n        sw.r += 1.8 * simSpeedScale;\n        sw.alpha = Math.max(0, 1 - sw.r \/ sw.maxR);\n\n        if (!sw.triggered && sw.alpha > 0.15 && !snake.isDead) {\n          const dGuard = Math.hypot(guard.x - sw.x, guard.y - sw.y);\n          if (Math.abs(dGuard - sw.r) < 18 || dGuard <= sw.r) {\n            sw.triggered = true;\n            if (guard.state === 'patrol' || guard.state === 'busqueda') {\n              guard.investigateTarget = { x: sw.x, y: sw.y };\n              guard.path = findPath(guard.x, guard.y, sw.x, sw.y, 11);\n              guard.investigatePauseTimer = 0;\n              setGuardState('suspicious', 'Ruido detectado.');\n            }\n          }\n        }\n\n        if (sw.r >= sw.maxR) {\n          soundWaves.splice(i, 1);\n        }\n      }\n\n      \/\/ 3. Vision Detection Logic\n      const dxToSnake = snake.x - guard.x;\n      const dyToSnake = snake.y - guard.y;\n      const distToSnake = Math.hypot(dxToSnake, dyToSnake);\n      const angleToSnake = Math.atan2(dyToSnake, dxToSnake);\n      const angleDiff = Math.abs(normalizeAngle(angleToSnake - guard.angle));\n      const fovHalfRad = (fovDegrees * Math.PI \/ 180) \/ 2;\n\n      let canSeeSnake = false;\n      let losReason = 'FUERA DE RANGO';\n      let losColor = '#94a3b8';\n\n      if (!snake.isDead) {\n        if (distToSnake <= fovDistance) {\n          if (angleDiff <= fovHalfRad) {\n            if (hasLineOfSight(guard.x, guard.y, snake.x, snake.y)) {\n              if (guard.state === 'alert') {\n                \/\/ Durante combate en alerta con linea de vision directa, la caja no burla al guardia\n                canSeeSnake = true;\n                if (snake.isBox) {\n                  losReason = 'COMBATE ACTIVO: CAJA IDENTIFICADA';\n                  unequipBox('&iexcl;El centinela identific&oacute; la caja durante el tiroteo!');\n                } else {\n                  losReason = 'CONTACTO VISUAL DIRECTO';\n                }\n                losColor = '#ef4444';\n              } else if (snake.isBox && snake.isMoving) {\n                \/\/ Caja en movimiento dentro del cono de vision\n                canSeeSnake = true;\n                losReason = 'CAJA EN MOVIMIENTO DETECTADA';\n                losColor = '#ef4444';\n                unequipBox('&iexcl;El centinela vio la caja movi&eacute;ndose!');\n              } else if (snake.isBox && distToSnake <= 48) {\n                \/\/ Inspeccion a corta distancia: levanta o patea la caja\n                canSeeSnake = true;\n                losReason = 'INSPECCI&Oacute;N: CAMUFLAJE DESCUBIERTO';\n                losColor = '#ef4444';\n                unequipBox('&iexcl;El centinela levant&oacute; la caja y descubri&oacute; a Snake!');\n              } else if (snake.isBox && !snake.isMoving) {\n                \/\/ Caja inmovil fuera del radio de inspeccion inmediata: camuflaje exitoso\n                canSeeSnake = false;\n                losReason = 'CAJA CAMUFLADA (IGNORADA)';\n                losColor = '#facc15';\n              } else {\n                \/\/ Intruso descubierto a simple vista\n                canSeeSnake = true;\n                losReason = 'CONTACTO VISUAL DIRECTO';\n                losColor = '#ef4444';\n              }\n            } else {\n              losReason = 'COBERTURA: OCULTO TRAS BLOQUE';\n              losColor = '#4ade80';\n            }\n          } else {\n            losReason = 'FUERA DE ANGULO FOV';\n            losColor = '#94a3b8';\n          }\n        }\n      } else {\n        losReason = 'OBJETIVO NEUTRALIZADO';\n        losColor = '#ef4444';\n      }\n\n      hudLos.innerText = `VISION: ${losReason}`;\n      hudLos.style.color = losColor;\n      hudLos.style.borderColor = losColor;\n\n      \/\/ 4. Guard FSM Behavior Update\n      if (guard.freezeTimer > 0) {\n        guard.freezeTimer -= effectiveDt;\n        return;\n      }\n\n      if (snake.isDead) {\n        return;\n      }\n\n      if (guard.state === 'patrol') {\n        if (canSeeSnake) {\n          guard.lastSeenSnake = { x: snake.x, y: snake.y };\n          setGuardState('alert', 'Intruso avistado.');\n        } else {\n          const wp = waypoints[guard.wpIndex];\n          const gdx = wp.x - guard.x;\n          const gdy = wp.y - guard.y;\n          const gdist = Math.hypot(gdx, gdy);\n          const targetAngle = Math.atan2(gdy, gdx);\n          guard.angle += normalizeAngle(targetAngle - guard.angle) * 0.06 * simSpeedScale;\n\n          if (gdist > 8) {\n            guard.x += Math.cos(guard.angle) * 0.65 * simSpeedScale;\n            guard.y += Math.sin(guard.angle) * 0.65 * simSpeedScale;\n            resolveEntityCollision(guard, 11);\n          } else {\n            guard.wpIndex = (guard.wpIndex + 1) % waypoints.length;\n          }\n        }\n      } else if (guard.state === 'suspicious') {\n        if (canSeeSnake) {\n          guard.lastSeenSnake = { x: snake.x, y: snake.y };\n          setGuardState('alert', 'Intruso localizado durante inspeccion.');\n        } else {\n          \/\/ Travel along path to investigate position\n          if (guard.path && guard.path.length > 0) {\n            const wp = guard.path[0];\n            const sdx = wp.x - guard.x;\n            const sdy = wp.y - guard.y;\n            const sdist = Math.hypot(sdx, sdy);\n            const targetAngle = Math.atan2(sdy, sdx);\n            guard.angle += normalizeAngle(targetAngle - guard.angle) * 0.08 * simSpeedScale;\n\n            if (sdist > 6) {\n              guard.x += Math.cos(guard.angle) * 0.85 * simSpeedScale;\n              guard.y += Math.sin(guard.angle) * 0.85 * simSpeedScale;\n              resolveEntityCollision(guard, 11);\n            } else {\n              guard.path.shift();\n            }\n          } else {\n            guard.investigatePauseTimer += effectiveDt;\n            guard.angle += Math.sin(guard.investigatePauseTimer * 2.5) * 0.025 * simSpeedScale;\n            if (guard.investigatePauseTimer > 2.6) {\n              setGuardState('patrol', 'Zona despejada.');\n            }\n          }\n        }\n      } else if (guard.state === 'alert') {\n        if (canSeeSnake) {\n          guard.lastSeenSnake = { x: snake.x, y: snake.y };\n\n          const cdx = snake.x - guard.x;\n          const cdy = snake.y - guard.y;\n          const currentDist = Math.hypot(cdx, cdy);\n          const angleToSnake = Math.atan2(cdy, cdx);\n\n          \/\/ Rotate to face Snake accurately during alert\n          guard.angle += normalizeAngle(angleToSnake - guard.angle) * 0.14 * simSpeedScale;\n\n          \/\/ Tactical Distance Combat (Keep ~80px to 115px distance while firing)\n          const desiredMinDist = 78;\n          const desiredMaxDist = 115;\n\n          if (isSegmentClear(guard.x, guard.y, snake.x, snake.y, 14)) {\n            guard.path = []; \/\/ Direct engagement\n\n            if (currentDist > desiredMaxDist) {\n              \/\/ Advance to maintain firing range\n              const speed = 1.15 * simSpeedScale;\n              guard.x += Math.cos(guard.angle) * speed;\n              guard.y += Math.sin(guard.angle) * speed;\n              resolveEntityCollision(guard, 11);\n            } else if (currentDist < desiredMinDist) {\n              \/\/ Tactical backstep to avoid hugging the player\n              const speed = 0.85 * simSpeedScale;\n              guard.x -= Math.cos(guard.angle) * speed;\n              guard.y -= Math.sin(guard.angle) * speed;\n              resolveEntityCollision(guard, 11);\n            }\n            \/\/ In optimal combat zone (78-115px), holds firing stance!\n          } else {\n            \/\/ Pathfind around obstacle to flank or re-acquire line of sight\n            if (!guard.path || guard.path.length === 0 || Math.random() < 0.06) {\n              guard.path = findPath(guard.x, guard.y, snake.x, snake.y, 11);\n            }\n            if (guard.path && guard.path.length > 0) {\n              const wp = guard.path[0];\n              const wdx = wp.x - guard.x;\n              const wdy = wp.y - guard.y;\n              const dist = Math.hypot(wdx, wdy);\n              const targetAngle = Math.atan2(wdy, wdx);\n              guard.angle += normalizeAngle(targetAngle - guard.angle) * 0.11 * simSpeedScale;\n              if (dist > 5) {\n                guard.x += Math.cos(guard.angle) * 1.25 * simSpeedScale;\n                guard.y += Math.sin(guard.angle) * 1.25 * simSpeedScale;\n                resolveEntityCollision(guard, 11);\n              } else {\n                guard.path.shift();\n              }\n            }\n          }\n\n          \/\/ Tactical Firearm Combat (Rifle firing cadence with line of sight)\n          guard.shotCooldown -= effectiveDt;\n          const angleDiffToSnake = Math.abs(normalizeAngle(angleToSnake - guard.angle));\n\n          if (currentDist <= 165 && hasLineOfSight(guard.x, guard.y, snake.x, snake.y) && angleDiffToSnake < 0.42) {\n            if (guard.shotCooldown <= 0) {\n              guard.shotCooldown = 1.25;\n              guardShoot();\n            }\n          }\n\n          \/\/ Emergency melee if player rushes directly into guard (< 20px)\n          if (currentDist <= 20) {\n            damageSnake(35, 'Cuerpo a cuerpo');\n            \/\/ Slight separation impulse\n            guard.x -= Math.cos(guard.angle) * 3;\n            guard.y -= Math.sin(guard.angle) * 3;\n            resolveEntityCollision(guard, 11);\n          }\n        } else {\n          setGuardState('busqueda', 'Contacto perdido tras bloque.');\n        }\n      } else if (guard.state === 'busqueda') {\n        if (canSeeSnake) {\n          guard.lastSeenSnake = { x: snake.x, y: snake.y };\n          setGuardState('alert', 'Intruso reavistado.');\n          return;\n        }\n\n        guard.busquedaTimer -= effectiveDt;\n\n        \/\/ Realistic Multi-Stage Search Behavior:\n        if (guard.busquedaStage === 'travel') {\n          \/\/ Walk to last known position\n          if (guard.path && guard.path.length > 0) {\n            const wp = guard.path[0];\n            const ldx = wp.x - guard.x;\n            const ldy = wp.y - guard.y;\n            const dist = Math.hypot(ldx, ldy);\n            const targetAngle = Math.atan2(ldy, ldx);\n            guard.angle += normalizeAngle(targetAngle - guard.angle) * 0.08 * simSpeedScale;\n            if (dist > 5) {\n              guard.x += Math.cos(guard.angle) * 0.95 * simSpeedScale;\n              guard.y += Math.sin(guard.angle) * 0.95 * simSpeedScale;\n              resolveEntityCollision(guard, 11);\n            } else {\n              guard.path.shift();\n            }\n          } else {\n            \/\/ Reached last known point: begin deliberate flank scans\n            guard.busquedaStage = 'look_left';\n            guard.busquedaStageTime = 0;\n            guard.busquedaBaseAngle = guard.angle;\n            logEvent('<strong style=\"color:#f97316;\">CODEC: BUSQUEDA:<\/strong> Punto de contacto alcanzado. Inspeccionando flancos...');\n          }\n        } else if (guard.busquedaStage === 'look_left') {\n          \/\/ Look ~60 degrees left\n          guard.busquedaStageTime += effectiveDt;\n          const targetAngle = guard.busquedaBaseAngle - Math.PI \/ 3;\n          guard.angle += normalizeAngle(targetAngle - guard.angle) * 0.08 * simSpeedScale;\n          if (guard.busquedaStageTime >= 0.85) {\n            guard.busquedaStage = 'look_right';\n            guard.busquedaStageTime = 0;\n          }\n        } else if (guard.busquedaStage === 'look_right') {\n          \/\/ Look ~60 degrees right\n          guard.busquedaStageTime += effectiveDt;\n          const targetAngle = guard.busquedaBaseAngle + Math.PI \/ 3;\n          guard.angle += normalizeAngle(targetAngle - guard.angle) * 0.08 * simSpeedScale;\n          if (guard.busquedaStageTime >= 0.85) {\n            guard.busquedaStage = 'look_front';\n            guard.busquedaStageTime = 0;\n          }\n        } else if (guard.busquedaStage === 'look_front') {\n          \/\/ Turn back forward\n          guard.busquedaStageTime += effectiveDt;\n          const targetAngle = guard.busquedaBaseAngle;\n          guard.angle += normalizeAngle(targetAngle - guard.angle) * 0.08 * simSpeedScale;\n          if (guard.busquedaStageTime >= 0.55) {\n            if (guard.busquedaTimer > 1.8) {\n              \/\/ Inspect a secondary adjacent corner\n              const secondary = findSecondarySearchPoint(guard.x, guard.y);\n              guard.path = findPath(guard.x, guard.y, secondary.x, secondary.y, 11);\n              guard.busquedaStage = 'travel_secondary';\n              guard.busquedaStageTime = 0;\n              logEvent('<strong style=\"color:#f97316;\">CODEC: BUSQUEDA:<\/strong> Rastreando esquina adyacente...');\n            } else {\n              guard.busquedaStage = 'look_secondary';\n              guard.busquedaStageTime = 0;\n              guard.busquedaBaseAngle = guard.angle;\n            }\n          }\n        } else if (guard.busquedaStage === 'travel_secondary') {\n          if (guard.path && guard.path.length > 0) {\n            const wp = guard.path[0];\n            const ldx = wp.x - guard.x;\n            const ldy = wp.y - guard.y;\n            const dist = Math.hypot(ldx, ldy);\n            const targetAngle = Math.atan2(ldy, ldx);\n            guard.angle += normalizeAngle(targetAngle - guard.angle) * 0.08 * simSpeedScale;\n            if (dist > 5) {\n              guard.x += Math.cos(guard.angle) * 0.95 * simSpeedScale;\n              guard.y += Math.sin(guard.angle) * 0.95 * simSpeedScale;\n              resolveEntityCollision(guard, 11);\n            } else {\n              guard.path.shift();\n            }\n          } else {\n            guard.busquedaStage = 'look_secondary';\n            guard.busquedaStageTime = 0;\n            guard.busquedaBaseAngle = guard.angle;\n          }\n        } else if (guard.busquedaStage === 'look_secondary') {\n          guard.busquedaStageTime += effectiveDt;\n          guard.angle = guard.busquedaBaseAngle + Math.sin(guard.busquedaStageTime * 2.2) * 0.45;\n        }\n\n        if (guard.busquedaTimer <= 0) {\n          \/\/ Conclude search and return to patrol route smoothly\n          let bestWp = 0;\n          let bestWpDist = Infinity;\n          for (let i = 0; i < waypoints.length; i++) {\n            const d = Math.hypot(waypoints[i].x - guard.x, waypoints[i].y - guard.y);\n            if (d < bestWpDist) {\n              bestWpDist = d;\n              bestWp = i;\n            }\n          }\n          guard.wpIndex = bestWp;\n          setGuardState('patrol', 'Temporizador agotado. Zona despejada.');\n        }\n      }\n    }\n\n    function render() {\n      ctx.save();\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n      \/\/ Screen shake on hit\n      if (screenShake > 0) {\n        const sx = (Math.random() - 0.5) * 6;\n        const sy = (Math.random() - 0.5) * 6;\n        ctx.translate(sx, sy);\n      }\n\n      \/\/ Floor grid\n      ctx.fillStyle = '#0f172a';\n      ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n      ctx.strokeStyle = 'rgba(51, 65, 85, 0.35)';\n      ctx.lineWidth = 1;\n      const gridSize = 25;\n      for (let x = gridSize; x < canvas.width; x += gridSize) {\n        ctx.beginPath();\n        ctx.moveTo(x, 0);\n        ctx.lineTo(x, canvas.height);\n        ctx.stroke();\n      }\n      for (let y = gridSize; y < canvas.height; y += gridSize) {\n        ctx.beginPath();\n        ctx.moveTo(0, y);\n        ctx.lineTo(canvas.width, y);\n        ctx.stroke();\n      }\n\n      \/\/ Draw Waypoint Route\n      ctx.strokeStyle = 'rgba(74, 222, 128, 0.2)';\n      ctx.lineWidth = 1.5;\n      ctx.setLineDash([4, 4]);\n      ctx.beginPath();\n      ctx.moveTo(waypoints[0].x, waypoints[0].y);\n      for (let i = 1; i < waypoints.length; i++) ctx.lineTo(waypoints[i].x, waypoints[i].y);\n      ctx.closePath();\n      ctx.stroke();\n      ctx.setLineDash([]);\n\n      \/\/ Draw 3 Solid Obstacles (CLEAN, NO TEXT LABELS)\n      obstacles.forEach(obs => {\n        ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';\n        ctx.fillRect(obs.x + 4, obs.y + 4, obs.w, obs.h);\n\n        ctx.fillStyle = '#1e293b';\n        ctx.fillRect(obs.x, obs.y, obs.w, obs.h);\n\n        ctx.strokeStyle = '#475569';\n        ctx.lineWidth = 2;\n        ctx.beginPath();\n        ctx.moveTo(obs.x, obs.y + obs.h);\n        ctx.lineTo(obs.x, obs.y);\n        ctx.lineTo(obs.x + obs.w, obs.y);\n        ctx.stroke();\n\n        ctx.strokeStyle = '#0f172a';\n        ctx.beginPath();\n        ctx.moveTo(obs.x + obs.w, obs.y);\n        ctx.lineTo(obs.x + obs.w, obs.y + obs.h);\n        ctx.lineTo(obs.x, obs.y + obs.h);\n        ctx.stroke();\n\n        ctx.strokeStyle = '#0284c7';\n        ctx.lineWidth = 1.5;\n        ctx.strokeRect(obs.x, obs.y, obs.w, obs.h);\n\n        \/\/ Subtle tactical hatch\n        ctx.strokeStyle = 'rgba(56, 189, 248, 0.12)';\n        ctx.beginPath();\n        ctx.moveTo(obs.x, obs.y);\n        ctx.lineTo(obs.x + obs.w, obs.y + obs.h);\n        ctx.moveTo(obs.x + obs.w, obs.y);\n        ctx.lineTo(obs.x, obs.y + obs.h);\n        ctx.stroke();\n      });\n\n      \/\/ Draw Sound Waves\n      soundWaves.forEach(sw => {\n        ctx.strokeStyle = `rgba(56, 189, 248, ${sw.alpha * 0.9})`;\n        ctx.lineWidth = 2;\n        ctx.beginPath();\n        ctx.arc(sw.x, sw.y, sw.r, 0, Math.PI * 2);\n        ctx.stroke();\n\n        ctx.strokeStyle = `rgba(56, 189, 248, ${sw.alpha * 0.3})`;\n        ctx.lineWidth = 1;\n        ctx.beginPath();\n        ctx.arc(sw.x, sw.y, Math.max(0, sw.r - 8), 0, Math.PI * 2);\n        ctx.stroke();\n      });\n\n      \/\/ Draw Bullet Tracer Line\n      if (bulletTracer) {\n        ctx.strokeStyle = `rgba(250, 204, 21, ${Math.min(1.0, bulletTracer.life \/ 0.15)})`;\n        ctx.lineWidth = 2.5;\n        ctx.beginPath();\n        ctx.moveTo(bulletTracer.x1, bulletTracer.y1);\n        ctx.lineTo(bulletTracer.x2, bulletTracer.y2);\n        ctx.stroke();\n      }\n\n      \/\/ Draw Guard Field of View Cone\n      const fovHalfRad = (fovDegrees * Math.PI \/ 180) \/ 2;\n      const rayCount = 50;\n      const fovPoints = [];\n\n      for (let i = 0; i <= rayCount; i++) {\n        const rayAngle = (guard.angle - fovHalfRad) + (fovHalfRad * 2 * (i \/ rayCount));\n        const cosA = Math.cos(rayAngle);\n        const sinA = Math.sin(rayAngle);\n        let rayDist = fovDistance;\n\n        for (let step = 4; step <= fovDistance; step += 4) {\n          const testX = guard.x + cosA * step;\n          const testY = guard.y + sinA * step;\n          if (testX < 0 || testX > canvas.width || testY < 0 || testY > canvas.height) {\n            rayDist = step;\n            break;\n          }\n          let hit = false;\n          for (let j = 0; j < obstacles.length; j++) {\n            const obs = obstacles[j];\n            if (testX >= obs.x && testX <= obs.x + obs.w && testY >= obs.y && testY <= obs.y + obs.h) {\n              hit = true;\n              break;\n            }\n          }\n          if (hit) {\n            rayDist = step;\n            break;\n          }\n        }\n        fovPoints.push({\n          x: guard.x + cosA * rayDist,\n          y: guard.y + sinA * rayDist\n        });\n      }\n\n      if (fovPoints.length > 0) {\n        ctx.beginPath();\n        ctx.moveTo(guard.x, guard.y);\n        fovPoints.forEach(p => ctx.lineTo(p.x, p.y));\n        ctx.closePath();\n\n        let coneColor, coneStroke;\n        if (guard.state === 'alert') {\n          coneColor = 'rgba(239, 68, 68, 0.28)';\n          coneStroke = 'rgba(239, 68, 68, 0.85)';\n        } else if (guard.state === 'suspicious') {\n          coneColor = 'rgba(234, 179, 8, 0.22)';\n          coneStroke = 'rgba(234, 179, 8, 0.8)';\n        } else if (guard.state === 'busqueda') {\n          coneColor = 'rgba(249, 115, 22, 0.22)';\n          coneStroke = 'rgba(249, 115, 22, 0.8)';\n        } else {\n          coneColor = 'rgba(34, 197, 94, 0.18)';\n          coneStroke = 'rgba(34, 197, 94, 0.65)';\n        }\n\n        ctx.fillStyle = coneColor;\n        ctx.fill();\n        ctx.strokeStyle = coneStroke;\n        ctx.lineWidth = 1.5;\n        ctx.stroke();\n      }\n\n      \/\/ Draw Guard Body\n      ctx.save();\n      ctx.translate(guard.x, guard.y);\n      ctx.rotate(guard.angle);\n\n      ctx.fillStyle = '#15803d';\n      ctx.beginPath();\n      ctx.arc(0, 0, 11, 0, Math.PI * 2);\n      ctx.fill();\n      ctx.strokeStyle = '#4ade80';\n      ctx.lineWidth = 2;\n      ctx.stroke();\n\n      ctx.fillStyle = '#cbd5e1';\n      ctx.fillRect(8, -2.5, 9, 5);\n\n      if (guard.muzzleFlash > 0) {\n        ctx.fillStyle = '#fef08a';\n        ctx.beginPath();\n        ctx.arc(20, 0, 7, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.fillStyle = '#ffffff';\n        ctx.beginPath();\n        ctx.arc(20, 0, 4, 0, Math.PI * 2);\n        ctx.fill();\n      }\n\n      ctx.restore();\n\n      \/\/ Guard Head Status Indicator\n      if (guard.state === 'alert') {\n        ctx.fillStyle = '#ef4444';\n        ctx.beginPath();\n        ctx.arc(guard.x, guard.y - 20, 9, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.fillStyle = '#ffffff';\n        ctx.font = 'bold 12px sans-serif';\n        ctx.textAlign = 'center';\n        ctx.textBaseline = 'middle';\n        ctx.fillText('!', guard.x, guard.y - 20);\n\n        if (guard.freezeTimer > 0) {\n          const pulseR = 14 + (1 - guard.freezeTimer \/ 0.55) * 16;\n          ctx.strokeStyle = 'rgba(239, 68, 68, 0.7)';\n          ctx.lineWidth = 2;\n          ctx.beginPath();\n          ctx.arc(guard.x, guard.y, pulseR, 0, Math.PI * 2);\n          ctx.stroke();\n        }\n      } else if (guard.state === 'suspicious') {\n        ctx.fillStyle = '#eab308';\n        ctx.beginPath();\n        ctx.arc(guard.x, guard.y - 20, 9, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.fillStyle = '#000000';\n        ctx.font = 'bold 12px sans-serif';\n        ctx.textAlign = 'center';\n        ctx.textBaseline = 'middle';\n        ctx.fillText('?', guard.x, guard.y - 20);\n      } else if (guard.state === 'busqueda') {\n        ctx.fillStyle = '#f97316';\n        ctx.beginPath();\n        ctx.arc(guard.x, guard.y - 20, 10, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.fillStyle = '#ffffff';\n        ctx.font = 'bold 10px sans-serif';\n        ctx.textAlign = 'center';\n        ctx.textBaseline = 'middle';\n        ctx.fillText(Math.ceil(guard.busquedaTimer) + 's', guard.x, guard.y - 20);\n      }\n\n      \/\/ Draw Snake (Infiltrator, Box, or Downed)\n      if (snake.isDead) {\n        ctx.fillStyle = '#475569';\n        ctx.beginPath();\n        ctx.arc(snake.x, snake.y, snake.radius, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.strokeStyle = '#ef4444';\n        ctx.lineWidth = 2;\n        ctx.stroke();\n\n        ctx.fillStyle = '#ef4444';\n        ctx.font = 'bold 10px sans-serif';\n        ctx.textAlign = 'center';\n        ctx.textBaseline = 'middle';\n        ctx.fillText('X', snake.x, snake.y);\n      } else if (snake.isBox) {\n        ctx.fillStyle = '#b45309';\n        ctx.fillRect(snake.x - 12, snake.y - 12, 24, 24);\n        ctx.strokeStyle = '#fde68a';\n        ctx.lineWidth = 1.5;\n        ctx.strokeRect(snake.x - 12, snake.y - 12, 24, 24);\n\n        ctx.fillStyle = '#fde68a';\n        ctx.font = 'bold 8px monospace';\n        ctx.textAlign = 'center';\n        ctx.fillText('BOX', snake.x, snake.y + 3);\n      } else {\n        ctx.fillStyle = snake.damageFlash > 0 ? '#ef4444' : '#0284c7';\n        ctx.beginPath();\n        ctx.arc(snake.x, snake.y, snake.radius, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.strokeStyle = snake.damageFlash > 0 ? '#ffffff' : '#38bdf8';\n        ctx.lineWidth = 2;\n        ctx.stroke();\n\n        ctx.fillStyle = '#38bdf8';\n        ctx.beginPath();\n        ctx.arc(snake.x, snake.y, 4, 0, Math.PI * 2);\n        ctx.fill();\n      }\n\n      \/\/ Click target indicator & subtle dotted tactical path line\n      if (!snake.isDead && snake.path && snake.path.length > 0) {\n        const goal = snake.path[snake.path.length - 1];\n        ctx.strokeStyle = 'rgba(56, 189, 248, 0.45)';\n        ctx.lineWidth = 1;\n        ctx.beginPath();\n        ctx.arc(goal.x, goal.y, 6, 0, Math.PI * 2);\n        ctx.stroke();\n\n        ctx.strokeStyle = 'rgba(56, 189, 248, 0.22)';\n        ctx.setLineDash([3, 3]);\n        ctx.beginPath();\n        ctx.moveTo(snake.x, snake.y);\n        snake.path.forEach(p => ctx.lineTo(p.x, p.y));\n        ctx.stroke();\n        ctx.setLineDash([]);\n      }\n\n      \/\/ GAME OVER OVERLAY (Clean ASCII text)\n      if (snake.isDead) {\n        ctx.fillStyle = 'rgba(15, 23, 42, 0.82)';\n        ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n        const cardW = 320;\n        const cardH = 130;\n        const cardX = (canvas.width - cardW) \/ 2;\n        const cardY = (canvas.height - cardH) \/ 2;\n\n        ctx.fillStyle = '#0f172a';\n        ctx.fillRect(cardX, cardY, cardW, cardH);\n        ctx.strokeStyle = '#ef4444';\n        ctx.lineWidth = 2;\n        ctx.strokeRect(cardX, cardY, cardW, cardH);\n\n        ctx.fillStyle = '#ef4444';\n        ctx.font = 'bold 18px sans-serif';\n        ctx.textAlign = 'center';\n        ctx.fillText('MISION FALLIDA', canvas.width \/ 2, cardY + 38);\n\n        ctx.fillStyle = '#cbd5e1';\n        ctx.font = '12px sans-serif';\n        ctx.fillText('Snake ha sido neutralizado por el guardia.', canvas.width \/ 2, cardY + 68);\n\n        ctx.fillStyle = '#fde047';\n        ctx.font = 'bold 11px monospace';\n        ctx.fillText('[ HAZ CLIC O PULSA R PARA REINTENTAR ]', canvas.width \/ 2, cardY + 104);\n      }\n\n      ctx.restore();\n    }\n\n    let isVisible = false;\n    let animFrameId = null;\n\n    function loop(now) {\n      if (!isVisible) {\n        animFrameId = null;\n        return;\n      }\n      const dt = Math.min(0.1, (now - lastTime) \/ 1000);\n      lastTime = now;\n      update(dt);\n      render();\n      animFrameId = requestAnimationFrame(loop);\n    }\n\n    \/\/ Dibujar el estado inicial una vez para que el canvas tenga graficos listos\n    render();\n\n    \/\/ Solo ejecutar la simulacion cuando el canvas este dentro del viewport\n    if ('IntersectionObserver' in window) {\n      const observer = new IntersectionObserver((entries) => {\n        entries.forEach(entry => {\n          const wasVisible = isVisible;\n          isVisible = entry.isIntersecting;\n          if (isVisible && !wasVisible) {\n            lastTime = performance.now();\n            if (!animFrameId) {\n              animFrameId = requestAnimationFrame(loop);\n            }\n          } else if (!isVisible && wasVisible) {\n            if (animFrameId) {\n              cancelAnimationFrame(animFrameId);\n              animFrameId = null;\n            }\n          }\n        });\n      }, { threshold: 0.15 });\n\n      observer.observe(canvas);\n    } else {\n      \/\/ Fallback para navegadores antiguos\n      isVisible = true;\n      animFrameId = requestAnimationFrame(loop);\n    }\n  })();\n\n  \/\/ Inicializar contador fallback si es necesario\n  try {\n    const rawViews = localStorage.getItem('cpue_views_count') || '142';\n    const nextViews = parseInt(rawViews, 10) + 1;\n    localStorage.setItem('cpue_views_count', nextViews.toString());\n    const fb = document.getElementById('counter-fallback-val');\n    if (fb) fb.innerText = nextViews + ' visitas';\n  } catch (e) {}\n\n  \/\/ ========================================================\n  \/\/ WEB AUDIO SYNTHESIS ENGINE (0 EXTERNAL DEPENDENCIES)\n  \/\/ ========================================================\n  let audioCtx = null;\n  let soundEnabled = true;\n  let soundPreset = 'subtle'; \/\/ 'subtle' (default), 'tactical', 'arcade'\n\n  try {\n    const savedSound = localStorage.getItem('cpue_sound_enabled');\n    if (savedSound !== null) soundEnabled = (savedSound === 'true');\n    const savedPreset = localStorage.getItem('cpue_sound_preset');\n    if (savedPreset) soundPreset = savedPreset;\n  } catch (e) {}\n\n  function getAudioContext() {\n    if (!audioCtx) {\n      const AudioContextClass = window.AudioContext || window.webkitAudioContext;\n      if (AudioContextClass) {\n        audioCtx = new AudioContextClass();\n      }\n    }\n    if (audioCtx && audioCtx.state === 'suspended') {\n      audioCtx.resume();\n    }\n    return audioCtx;\n  }\n\n  function updateSoundUI() {\n    const btn = document.getElementById('btn-sound-toggle');\n    const icon = document.getElementById('sound-icon');\n    const text = document.getElementById('sound-text');\n    if (!btn || !icon || !text) return;\n    if (soundEnabled) {\n      btn.classList.remove('muted');\n      icon.innerHTML = '&#128266;';\n      text.innerText = 'Audio: Activo';\n    } else {\n      btn.classList.add('muted');\n      icon.innerHTML = '&#128263;';\n      text.innerText = 'Audio: Silenciado';\n    }\n    try {\n      localStorage.setItem('cpue_sound_enabled', soundEnabled.toString());\n    } catch (e) {}\n  }\n\n  const btnSoundToggle = document.getElementById('btn-sound-toggle');\n  if (btnSoundToggle) {\n    btnSoundToggle.addEventListener('click', (e) => {\n      e.stopPropagation();\n      soundEnabled = !soundEnabled;\n      updateSoundUI();\n      if (soundEnabled) {\n        getAudioContext();\n        playButtonClickSound();\n      }\n    });\n  }\n  updateSoundUI();\n\n  const presetSelect = document.getElementById('sound-preset-select');\n  if (presetSelect) {\n    presetSelect.value = soundPreset;\n    presetSelect.addEventListener('change', (e) => {\n      e.stopPropagation();\n      soundPreset = e.target.value;\n      try {\n        localStorage.setItem('cpue_sound_preset', soundPreset);\n      } catch (err) {}\n      if (soundEnabled) {\n        getAudioContext();\n        playButtonClickSound();\n      }\n    });\n  }\n\n  \/\/ 1. Sonido sordo \"toc-toc\" al golpear pared o fondo\n  function playKnock() {\n    if (!soundEnabled) return;\n    const ctx = getAudioContext();\n    if (!ctx) return;\n    const now = ctx.currentTime;\n\n    if (soundPreset === 'arcade') {\n      [0, 0.088].forEach((delay, idx) => {\n        const t = now + delay;\n        const osc = ctx.createOscillator();\n        const gain = ctx.createGain();\n        osc.type = 'square';\n        osc.frequency.setValueAtTime(idx === 0 ? 210 : 160, t);\n        osc.frequency.exponentialRampToValueAtTime(50, t + 0.05);\n\n        gain.gain.setValueAtTime(0.001, t);\n        gain.gain.linearRampToValueAtTime(0.18, t + 0.004);\n        gain.gain.exponentialRampToValueAtTime(0.001, t + 0.055);\n\n        osc.connect(gain);\n        gain.connect(ctx.destination);\n        osc.start(t);\n        osc.stop(t + 0.06);\n      });\n    } else if (soundPreset === 'subtle') {\n      \/\/ Mecanico Sutil: Toque percusivo amortiguado\n      [0, 0.08].forEach((delay, idx) => {\n        const t = now + delay;\n        const osc = ctx.createOscillator();\n        const gain = ctx.createGain();\n        osc.type = 'triangle';\n        osc.frequency.setValueAtTime(idx === 0 ? 220 : 170, t);\n        osc.frequency.exponentialRampToValueAtTime(65, t + 0.045);\n\n        gain.gain.setValueAtTime(0.001, t);\n        gain.gain.linearRampToValueAtTime(0.24, t + 0.003);\n        gain.gain.exponentialRampToValueAtTime(0.001, t + 0.05);\n\n        osc.connect(gain);\n        gain.connect(ctx.destination);\n        osc.start(t);\n        osc.stop(t + 0.055);\n      });\n    } else {\n      \/\/ Tactico MGS: Tabique denso\n      [0, 0.084].forEach((delay, idx) => {\n        const t = now + delay;\n        const osc = ctx.createOscillator();\n        const gain = ctx.createGain();\n        const filter = ctx.createBiquadFilter();\n\n        filter.type = 'lowpass';\n        filter.frequency.setValueAtTime(idx === 0 ? 340 : 290, t);\n        filter.frequency.exponentialRampToValueAtTime(75, t + 0.065);\n\n        osc.type = 'triangle';\n        osc.frequency.setValueAtTime(idx === 0 ? 135 : 115, t);\n        osc.frequency.exponentialRampToValueAtTime(45, t + 0.06);\n\n        gain.gain.setValueAtTime(0.001, t);\n        gain.gain.linearRampToValueAtTime(0.42, t + 0.005);\n        gain.gain.exponentialRampToValueAtTime(0.001, t + 0.065);\n\n        osc.connect(filter);\n        filter.connect(gain);\n        gain.connect(ctx.destination);\n\n        osc.start(t);\n        osc.stop(t + 0.07);\n      });\n    }\n  }\n\n  \/\/ 2. Clic mecanico satisfactorio para botones\n  function playButtonClickSound() {\n    if (!soundEnabled) return;\n    const ctx = getAudioContext();\n    if (!ctx) return;\n    const now = ctx.currentTime;\n    const osc = ctx.createOscillator();\n    const gain = ctx.createGain();\n\n    if (soundPreset === 'arcade') {\n      osc.type = 'square';\n      osc.frequency.setValueAtTime(920, now);\n      osc.frequency.exponentialRampToValueAtTime(440, now + 0.035);\n      gain.gain.setValueAtTime(0.001, now);\n      gain.gain.linearRampToValueAtTime(0.12, now + 0.004);\n      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.035);\n    } else if (soundPreset === 'subtle') {\n      \/\/ Switch mecanico suave y nitido estilo Cherry Brown\n      osc.type = 'sine';\n      osc.frequency.setValueAtTime(680, now);\n      osc.frequency.exponentialRampToValueAtTime(280, now + 0.026);\n      gain.gain.setValueAtTime(0.001, now);\n      gain.gain.linearRampToValueAtTime(0.16, now + 0.003);\n      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.026);\n    } else {\n      \/\/ Tactico mecanico pesado\n      osc.type = 'triangle';\n      osc.frequency.setValueAtTime(620, now);\n      osc.frequency.exponentialRampToValueAtTime(180, now + 0.032);\n      gain.gain.setValueAtTime(0.001, now);\n      gain.gain.linearRampToValueAtTime(0.16, now + 0.004);\n      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.032);\n    }\n\n    osc.connect(gain);\n    gain.connect(ctx.destination);\n    osc.start(now);\n    osc.stop(now + 0.038);\n  }\n\n  \/\/ 3. Micro-ticks ultrarrapidos y satisfactorios al arrastrar marcando texto (ASMR Typewriter Ratchet)\n  function playScrubTickSound(pitchOffset = 0) {\n    if (!soundEnabled) return;\n    const ctx = getAudioContext();\n    if (!ctx) return;\n    const now = ctx.currentTime;\n    const osc = ctx.createOscillator();\n    const gain = ctx.createGain();\n\n    if (soundPreset === 'arcade') {\n      osc.type = 'square';\n      osc.frequency.setValueAtTime(800 + pitchOffset, now);\n      osc.frequency.exponentialRampToValueAtTime(320, now + 0.016);\n      gain.gain.setValueAtTime(0.001, now);\n      gain.gain.linearRampToValueAtTime(0.06, now + 0.002);\n      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.016);\n    } else if (soundPreset === 'tactical') {\n      osc.type = 'triangle';\n      osc.frequency.setValueAtTime(720 + pitchOffset, now);\n      osc.frequency.exponentialRampToValueAtTime(260, now + 0.015);\n      gain.gain.setValueAtTime(0.001, now);\n      gain.gain.linearRampToValueAtTime(0.08, now + 0.002);\n      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.015);\n    } else {\n      \/\/ Mecanico Sutil: Micro-tic cristalino y percusivo\n      osc.type = 'sine';\n      osc.frequency.setValueAtTime(980 + pitchOffset, now);\n      osc.frequency.exponentialRampToValueAtTime(480, now + 0.014);\n      gain.gain.setValueAtTime(0.001, now);\n      gain.gain.linearRampToValueAtTime(0.08, now + 0.002);\n      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.014);\n    }\n\n    osc.connect(gain);\n    gain.connect(ctx.destination);\n    osc.start(now);\n    osc.stop(now + 0.018);\n  }\n\n  \/\/ 4. Tick para sliders de rango (deslizadores de FOV, Distancia, Memoria)\n  function playSliderTickSound(val) {\n    if (!soundEnabled) return;\n    const ctx = getAudioContext();\n    if (!ctx) return;\n    const now = ctx.currentTime;\n    const osc = ctx.createOscillator();\n    const gain = ctx.createGain();\n\n    const baseFreq = 520 + ((val % 10) * 22);\n    osc.type = soundPreset === 'arcade' ? 'square' : 'triangle';\n    osc.frequency.setValueAtTime(baseFreq, now);\n    osc.frequency.exponentialRampToValueAtTime(baseFreq * 0.55, now + 0.018);\n\n    gain.gain.setValueAtTime(0.001, now);\n    gain.gain.linearRampToValueAtTime(0.07, now + 0.002);\n    gain.gain.exponentialRampToValueAtTime(0.001, now + 0.018);\n\n    osc.connect(gain);\n    gain.connect(ctx.destination);\n    osc.start(now);\n    osc.stop(now + 0.02);\n  }\n\n  \/\/ 5. Paso en cuadricula al arrastrar Pac-Man o fantasmas\n  function playTileStepSound() {\n    if (!soundEnabled) return;\n    const ctx = getAudioContext();\n    if (!ctx) return;\n    const now = ctx.currentTime;\n    const osc = ctx.createOscillator();\n    const gain = ctx.createGain();\n\n    osc.type = soundPreset === 'arcade' ? 'square' : 'sine';\n    osc.frequency.setValueAtTime(320, now);\n    osc.frequency.exponentialRampToValueAtTime(160, now + 0.018);\n\n    gain.gain.setValueAtTime(0.001, now);\n    gain.gain.linearRampToValueAtTime(0.09, now + 0.003);\n    gain.gain.exponentialRampToValueAtTime(0.001, now + 0.018);\n\n    osc.connect(gain);\n    gain.connect(ctx.destination);\n    osc.start(now);\n    osc.stop(now + 0.022);\n  }\n\n\n  \/\/ 5b. Tonos distintivos para los 4 fantasmas al desplazarlos por el laberinto\n  const GHOST_FREQS = {\n    blinky: 460, \/\/ Rojo \/ Perseguidor (agudo y directo)\n    pinky:  540, \/\/ Rosa \/ Emboscadora (rapido y brillante)\n    inky:   380, \/\/ Cian \/ Timido (medio resonante)\n    clyde:  280  \/\/ Naranja \/ Indeciso (grave y ondulante)\n  };\n\n  function playGhostStepSound(ghostName) {\n    if (!soundEnabled) return;\n    const ctx = getAudioContext();\n    if (!ctx) return;\n    const now = ctx.currentTime;\n    const osc = ctx.createOscillator();\n    const gain = ctx.createGain();\n\n    const baseFreq = GHOST_FREQS[ghostName] || 400;\n\n    if (soundPreset === 'arcade') {\n      \/\/ Chiptune 8-bit ghost wobble\n      osc.type = 'square';\n      osc.frequency.setValueAtTime(baseFreq, now);\n      osc.frequency.exponentialRampToValueAtTime(baseFreq * 0.72, now + 0.025);\n      gain.gain.setValueAtTime(0.001, now);\n      gain.gain.linearRampToValueAtTime(0.12, now + 0.003);\n      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.025);\n    } else if (soundPreset === 'tactical') {\n      \/\/ Tactico sonar ping\n      osc.type = 'triangle';\n      osc.frequency.setValueAtTime(baseFreq * 0.9, now);\n      osc.frequency.exponentialRampToValueAtTime(baseFreq * 0.5, now + 0.022);\n      gain.gain.setValueAtTime(0.001, now);\n      gain.gain.linearRampToValueAtTime(0.14, now + 0.003);\n      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.022);\n    } else {\n      \/\/ Mecanico Sutil (default): Toque etereo cristalino\n      osc.type = 'sine';\n      osc.frequency.setValueAtTime(baseFreq, now);\n      osc.frequency.exponentialRampToValueAtTime(baseFreq * 0.65, now + 0.022);\n      gain.gain.setValueAtTime(0.001, now);\n      gain.gain.linearRampToValueAtTime(0.18, now + 0.002);\n      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.022);\n    }\n\n    osc.connect(gain);\n    gain.connect(ctx.destination);\n    osc.start(now);\n    osc.stop(now + 0.026);\n  }\n\n  function playGhostGrabSound(ghostName) {\n    if (!soundEnabled) return;\n    const ctx = getAudioContext();\n    if (!ctx) return;\n    const now = ctx.currentTime;\n    const osc = ctx.createOscillator();\n    const gain = ctx.createGain();\n\n    const baseFreq = (GHOST_FREQS[ghostName] || 400) * 0.85;\n    osc.type = soundPreset === 'arcade' ? 'square' : 'triangle';\n    osc.frequency.setValueAtTime(baseFreq, now);\n    osc.frequency.exponentialRampToValueAtTime(baseFreq * 1.35, now + 0.035);\n\n    gain.gain.setValueAtTime(0.001, now);\n    gain.gain.linearRampToValueAtTime(0.14, now + 0.004);\n    gain.gain.exponentialRampToValueAtTime(0.001, now + 0.035);\n\n    osc.connect(gain);\n    gain.connect(ctx.destination);\n    osc.start(now);\n    osc.stop(now + 0.04);\n  }\n\n  function playGhostDropSound(ghostName) {\n    if (!soundEnabled) return;\n    const ctx = getAudioContext();\n    if (!ctx) return;\n    const now = ctx.currentTime;\n    const osc = ctx.createOscillator();\n    const gain = ctx.createGain();\n\n    const baseFreq = GHOST_FREQS[ghostName] || 400;\n    osc.type = 'sine';\n    osc.frequency.setValueAtTime(baseFreq * 1.1, now);\n    osc.frequency.exponentialRampToValueAtTime(baseFreq * 0.7, now + 0.028);\n\n    gain.gain.setValueAtTime(0.001, now);\n    gain.gain.linearRampToValueAtTime(0.12, now + 0.003);\n    gain.gain.exponentialRampToValueAtTime(0.001, now + 0.028);\n\n    osc.connect(gain);\n    gain.connect(ctx.destination);\n    osc.start(now);\n    osc.stop(now + 0.032);\n  }\n\n  window.playGhostStepSound = playGhostStepSound;\n  window.playGhostGrabSound = playGhostGrabSound;\n  window.playGhostDropSound = playGhostDropSound;\n\n  \/\/ 6. Friccion de caja de carton de Snake\n  function playCardboardSound(isEquipping) {\n    if (!soundEnabled) return;\n    const ctx = getAudioContext();\n    if (!ctx) return;\n    const now = ctx.currentTime;\n\n    [0, 0.04].forEach((delay, idx) => {\n      const t = now + delay;\n      const osc = ctx.createOscillator();\n      const gain = ctx.createGain();\n      const filter = ctx.createBiquadFilter();\n\n      filter.type = 'bandpass';\n      filter.frequency.setValueAtTime(idx === 0 ? 460 : 340, t);\n      filter.Q.setValueAtTime(2.2, t);\n\n      osc.type = 'triangle';\n      osc.frequency.setValueAtTime(isEquipping ? (idx === 0 ? 170 : 120) : (idx === 0 ? 130 : 180), t);\n      osc.frequency.exponentialRampToValueAtTime(45, t + 0.038);\n\n      gain.gain.setValueAtTime(0.001, t);\n      gain.gain.linearRampToValueAtTime(0.18, t + 0.004);\n      gain.gain.exponentialRampToValueAtTime(0.001, t + 0.04);\n\n      osc.connect(filter);\n      filter.connect(gain);\n      gain.connect(ctx.destination);\n      osc.start(t);\n      osc.stop(t + 0.045);\n    });\n  }\n\n  \/\/ 7. Alerta del centinela (!)\n  function playAlertSound() {\n    if (!soundEnabled) return;\n    const ctx = getAudioContext();\n    if (!ctx) return;\n    const now = ctx.currentTime;\n    const osc = ctx.createOscillator();\n    const gain = ctx.createGain();\n\n    osc.type = 'sine';\n    osc.frequency.setValueAtTime(880, now);\n    osc.frequency.setValueAtTime(1760, now + 0.08);\n\n    gain.gain.setValueAtTime(0.001, now);\n    gain.gain.linearRampToValueAtTime(0.24, now + 0.015);\n    gain.gain.exponentialRampToValueAtTime(0.001, now + 0.26);\n\n    osc.connect(gain);\n    gain.connect(ctx.destination);\n    osc.start(now);\n    osc.stop(now + 0.27);\n  }\n\n  \/\/ 8. Tono de evaluacion para el Arbol de Conducta\n  function playTickSound(isSuccess) {\n    if (!soundEnabled) return;\n    const ctx = getAudioContext();\n    if (!ctx) return;\n    const now = ctx.currentTime;\n    const osc = ctx.createOscillator();\n    const gain = ctx.createGain();\n\n    osc.type = 'sine';\n    osc.frequency.setValueAtTime(isSuccess ? 620 : 290, now);\n    osc.frequency.exponentialRampToValueAtTime(isSuccess ? 920 : 190, now + 0.055);\n\n    gain.gain.setValueAtTime(0.001, now);\n    gain.gain.linearRampToValueAtTime(0.16, now + 0.007);\n    gain.gain.exponentialRampToValueAtTime(0.001, now + 0.055);\n\n    osc.connect(gain);\n    gain.connect(ctx.destination);\n    osc.start(now);\n    osc.stop(now + 0.06);\n  }\n\n\n  \/\/ 9. Sonido de confirmacion al copiar texto (Ctrl+C \/ Copiar)\n  function playCopySound() {\n    if (!soundEnabled) return;\n    const ctx = getAudioContext();\n    if (!ctx) return;\n    const now = ctx.currentTime;\n\n    if (soundPreset === 'arcade') {\n      \/\/ Chiptune retro item pickup (dos notas rapidas brillantes)\n      [0, 0.06].forEach((delay, idx) => {\n        const t = now + delay;\n        const osc = ctx.createOscillator();\n        const gain = ctx.createGain();\n        osc.type = 'square';\n        osc.frequency.setValueAtTime(idx === 0 ? 660 : 990, t);\n\n        gain.gain.setValueAtTime(0.001, t);\n        gain.gain.linearRampToValueAtTime(0.12, t + 0.004);\n        gain.gain.exponentialRampToValueAtTime(0.001, t + 0.055);\n\n        osc.connect(gain);\n        gain.connect(ctx.destination);\n        osc.start(t);\n        osc.stop(t + 0.06);\n      });\n    } else if (soundPreset === 'tactical') {\n      \/\/ Tactico: bleep de transmision de datos segura (MGS codec \/ data sync)\n      [0, 0.05].forEach((delay, idx) => {\n        const t = now + delay;\n        const osc = ctx.createOscillator();\n        const gain = ctx.createGain();\n        osc.type = 'sine';\n        osc.frequency.setValueAtTime(idx === 0 ? 1046 : 1318, t);\n\n        gain.gain.setValueAtTime(0.001, t);\n        gain.gain.linearRampToValueAtTime(0.15, t + 0.003);\n        gain.gain.exponentialRampToValueAtTime(0.001, t + 0.045);\n\n        osc.connect(gain);\n        gain.connect(ctx.destination);\n        osc.start(t);\n        osc.stop(t + 0.05);\n      });\n    } else {\n      \/\/ Mecanico Sutil (default): Campanilla cristalina de maquina de escribir (ASMR typewriter bell)\n      const osc1 = ctx.createOscillator();\n      const osc2 = ctx.createOscillator();\n      const gain = ctx.createGain();\n\n      osc1.type = 'sine';\n      osc1.frequency.setValueAtTime(1174, now);\n      osc2.type = 'triangle';\n      osc2.frequency.setValueAtTime(2349, now);\n\n      gain.gain.setValueAtTime(0.001, now);\n      gain.gain.linearRampToValueAtTime(0.18, now + 0.004);\n      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.18);\n\n      osc1.connect(gain);\n      osc2.connect(gain);\n      gain.connect(ctx.destination);\n\n      osc1.start(now);\n      osc2.start(now);\n      osc1.stop(now + 0.19);\n      osc2.stop(now + 0.19);\n    }\n  }\n\n  \/\/ Listener para evento de copiado en el documento\n  document.addEventListener('copy', () => {\n    playCopySound();\n  });\n\n  \/\/ ========================================================\n  \/\/ LISTENERS GLOBALES DE INTERACCION\n  \/\/ ========================================================\n  \/\/ A. Clic en fondo de puntos \/ areas neutras -> sonido \"toc-toc\"\n  document.addEventListener('click', (e) => {\n    if (e.target.closest('button, input, select, textarea, a, canvas, label, .ghost-pill, .bt-node, .bt-leaf, .bt-action-btn, .sound-toggle-btn, .sound-preset-select')) {\n      return;\n    }\n    playKnock();\n  });\n\n  \/\/ B. Clic tactil en todos los botones y selectores\n  document.addEventListener('click', (e) => {\n    const btn = e.target.closest('button, .ghost-pill, select');\n    if (btn) {\n      if (btn.id === 'btnKnock' || btn.id === 'btnBox') return; \/\/ Tienen sonidos propios\n      playButtonClickSound();\n    }\n  });\n\n  \/\/ C. Arrastre de controles deslizantes (Sliders)\n  document.addEventListener('input', (e) => {\n    if (e.target.matches('input[type=\"range\"]')) {\n      playSliderTickSound(parseFloat(e.target.value) || 0);\n    }\n  });\n\n  \/\/ D. Arrastrar pulsando clic para marcar\/seleccionar texto (ASMR Ratchet)\n  let isMousePressed = false;\n  let lastSelectionLength = 0;\n  let lastScrubTimestamp = 0;\n\n  document.addEventListener('mousedown', (e) => {\n    \/\/ Si pulsa boton izquierdo\n    if (e.button === 0) {\n      isMousePressed = true;\n      const sel = window.getSelection();\n      lastSelectionLength = sel ? sel.toString().length : 0;\n    }\n  });\n\n  document.addEventListener('mouseup', () => {\n    isMousePressed = false;\n  });\n\n  document.addEventListener('selectionchange', () => {\n    if (!isMousePressed) return;\n    const sel = window.getSelection();\n    if (!sel) return;\n    const currentLength = sel.toString().length;\n    if (currentLength > 0 && currentLength !== lastSelectionLength) {\n      const now = performance.now();\n      \/\/ Limitar a maximo ~35 ticks\/segundo para cadencia fluida y agradable\n      if (now - lastScrubTimestamp > 28) {\n        lastScrubTimestamp = now;\n        const pitchDelta = ((currentLength * 32) % 360);\n        playScrubTickSound(pitchDelta);\n      }\n      lastSelectionLength = currentLength;\n    }\n  });\n\n  \/\/ ========================================================\n  \/\/ CHAPTER 3: BEHAVIOR TREE INTERACTIVE SIMULATOR LOGIC\n  \/\/ ========================================================\n  const btnSimThreat = document.getElementById('btn-sim-threat');\n  const btnSimClear = document.getElementById('btn-sim-clear');\n  const btnBtTick = document.getElementById('btn-bt-tick');\n  const btTelemetry = document.getElementById('bt-telemetry-box');\n\n  let btHasThreat = true;\n\n  try {\n    const savedBtThreat = localStorage.getItem('cpue_bt_threat');\n    if (savedBtThreat !== null) {\n      btHasThreat = (savedBtThreat === 'true');\n      if (btnSimThreat && btnSimClear) {\n        if (btHasThreat) {\n          btnSimThreat.classList.add('active-sim');\n          btnSimClear.classList.remove('active-sim');\n        } else {\n          btnSimClear.classList.add('active-sim');\n          btnSimThreat.classList.remove('active-sim');\n        }\n      }\n    }\n  } catch (e) {}\n\n  if (btnSimThreat && btnSimClear) {\n    btnSimThreat.addEventListener('click', () => {\n      btHasThreat = true;\n      try {\n        localStorage.setItem('cpue_bt_threat', 'true');\n      } catch (e) {}\n      btnSimThreat.classList.add('active-sim');\n      btnSimClear.classList.remove('active-sim');\n      evaluateBehaviorTree(true);\n    });\n    btnSimClear.addEventListener('click', () => {\n      btHasThreat = false;\n      try {\n        localStorage.setItem('cpue_bt_threat', 'false');\n      } catch (e) {}\n      btnSimClear.classList.add('active-sim');\n      btnSimThreat.classList.remove('active-sim');\n      evaluateBehaviorTree(true);\n    });\n  }\n\n  if (btnBtTick) {\n    btnBtTick.addEventListener('click', () => {\n      evaluateBehaviorTree(true);\n    });\n  }\n\n  function resetTreeVisuals() {\n    const nodes = document.querySelectorAll('.bt-node');\n    nodes.forEach(n => {\n      n.classList.remove('eval-success', 'eval-failure', 'eval-running', 'eval-skipped');\n    });\n  }\n\n  function evaluateBehaviorTree(withSound = true) {\n    resetTreeVisuals();\n\n    const nodeRoot = document.getElementById('bt-node-root');\n    const nodeSelector = document.getElementById('bt-node-selector');\n    const nodeSeqAttack = document.getElementById('bt-node-seq-attack');\n    const leafDetect = document.getElementById('bt-leaf-detect');\n    const leafApproach = document.getElementById('bt-leaf-approach');\n    const leafShoot = document.getElementById('bt-leaf-shoot');\n    const resDetect = document.getElementById('bt-res-detect');\n    const resApproach = document.getElementById('bt-res-approach');\n    const resShoot = document.getElementById('bt-res-shoot');\n\n    const nodeSeqPatrol = document.getElementById('bt-node-seq-patrol');\n    const leafNegator = document.getElementById('bt-leaf-negator');\n    const leafPatrol = document.getElementById('bt-leaf-patrol');\n    const resNegator = document.getElementById('bt-res-negator');\n    const resPatrol = document.getElementById('bt-res-patrol');\n\n    if (nodeRoot) nodeRoot.classList.add('eval-running');\n    if (nodeSelector) nodeSelector.classList.add('eval-running');\n\n    if (btHasThreat) {\n      \/\/ Rama 1 (Ataque) TIENE EXITO\n      if (nodeSeqAttack) nodeSeqAttack.classList.add('eval-success');\n      if (leafDetect) leafDetect.classList.add('eval-success');\n      if (leafApproach) leafApproach.classList.add('eval-success');\n      if (leafShoot) leafShoot.classList.add('eval-success');\n      if (resDetect) { resDetect.innerText = '\\u00c9XITO'; resDetect.style.color = '#16a34a'; }\n      if (resApproach) { resApproach.innerText = '\\u00c9XITO'; resApproach.style.color = '#16a34a'; }\n      if (resShoot) { resShoot.innerText = '\\u00c9XITO'; resShoot.style.color = '#16a34a'; }\n\n      \/\/ Rama 2 (Patrulla) es IGNORADA por el Selector\n      if (nodeSeqPatrol) nodeSeqPatrol.classList.add('eval-skipped');\n      if (leafNegator) leafNegator.classList.add('eval-skipped');\n      if (leafPatrol) leafPatrol.classList.add('eval-skipped');\n      if (resNegator) { resNegator.innerText = 'OMITIDO'; resNegator.style.color = '#78716c'; }\n      if (resPatrol) { resPatrol.innerText = 'OMITIDO'; resPatrol.style.color = '#78716c'; }\n\n      if (btTelemetry) {\n        btTelemetry.innerHTML = '<strong>[TICK: INTRUSO DETECTADO]<\/strong> El Selector eval&uacute;a primero la rama izquierda (Secuencia de Ataque). Como <em>&laquo;Detectar Intruso&raquo;<\/em> devuelve <span style=\"color:#16a34a; font-weight:800;\">&Eacute;XITO<\/span>, ejecuta <em>&laquo;Aproximarse&raquo;<\/em> y <em>&laquo;Abrir Fuego&raquo;<\/em>. La rama de patrulla se omite por completo (prioridad resuelta).';\n      }\n      if (withSound) playTickSound(true);\n    } else {\n      \/\/ Rama 1 (Ataque) FALLA en la primera condicion\n      if (nodeSeqAttack) nodeSeqAttack.classList.add('eval-failure');\n      if (leafDetect) leafDetect.classList.add('eval-failure');\n      if (leafApproach) leafApproach.classList.add('eval-skipped');\n      if (leafShoot) leafShoot.classList.add('eval-skipped');\n      if (resDetect) { resDetect.innerText = 'FALLO'; resDetect.style.color = '#dc2626'; }\n      if (resApproach) { resApproach.innerText = 'OMITIDO'; resApproach.style.color = '#78716c'; }\n      if (resShoot) { resShoot.innerText = 'OMITIDO'; resShoot.style.color = '#78716c'; }\n\n      \/\/ Selector conmuta a Rama 2 (Patrulla)\n      if (nodeSeqPatrol) nodeSeqPatrol.classList.add('eval-success');\n      if (leafNegator) leafNegator.classList.add('eval-success');\n      if (leafPatrol) leafPatrol.classList.add('eval-success');\n      if (resNegator) { resNegator.innerText = '\\u00c9XITO (NOT)'; resNegator.style.color = '#16a34a'; }\n      if (resPatrol) { resPatrol.innerText = '\\u00c9XITO'; resPatrol.style.color = '#16a34a'; }\n\n      if (btTelemetry) {\n        btTelemetry.innerHTML = '<strong>[TICK: ZONA DESPEJADA]<\/strong> El Selector eval&uacute;a la Secuencia de Ataque: como <em>&laquo;Detectar Intruso&raquo;<\/em> devuelve <span style=\"color:#dc2626; font-weight:800;\">FALLO<\/span>, la secuencia de combate se aborta de inmediato. El Selector salta a la rama secundaria (Patrulla): el negador confirma ausencia de alerta y ejecuta <em>&laquo;Seguir Ruta&raquo;<\/em> con <span style=\"color:#16a34a; font-weight:800;\">&Eacute;XITO<\/span>.';\n      }\n      if (withSound) playTickSound(false);\n    }\n  }\n\n  \/\/ Inicializar evaluacion inicial del Arbol de Conducta\n  evaluateBehaviorTree(false);\n\n<\/script>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Laboratorio Interactivo &middot; Obra Final APGI C\u00c3\u00b3mo piensa un enemigo Un cuaderno interactivo inspirado en Red Blob Games y Nicky Case para experimentar con las matem\u00c3\u00a1ticas y la arquitectura detr\u00c3\u00a1s de los enemigos de videojuegos. M\u00c3\u00b3dulo 1: La Geometr\u00c3\u00ada de los 4 Fantasmas Pac-Man (1980) En lugar de perseguir directamente, cada fantasma tiene su propio [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"footnotes":""},"class_list":["post-39","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/ericguerrero.es\/index.php?rest_route=\/wp\/v2\/pages\/39","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ericguerrero.es\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/ericguerrero.es\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/ericguerrero.es\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ericguerrero.es\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=39"}],"version-history":[{"count":1,"href":"https:\/\/ericguerrero.es\/index.php?rest_route=\/wp\/v2\/pages\/39\/revisions"}],"predecessor-version":[{"id":41,"href":"https:\/\/ericguerrero.es\/index.php?rest_route=\/wp\/v2\/pages\/39\/revisions\/41"}],"wp:attachment":[{"href":"https:\/\/ericguerrero.es\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=39"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}