Spaces:
Running
Running
GitHub Actions commited on
Commit ·
bf9f71f
0
Parent(s):
Deploy BrainBit desde GitHub dfe1bad1c7cf060cbc34bf6aa22ae12fae8518b1
Browse files- README.md +8 -0
- apps/analyse-programmierung-app.html +792 -0
- apps/konzentration-merkfaehigkeit-app.html +714 -0
- apps/mathematik-app.html +686 -0
- apps/vernetztes-denken-app.html +590 -0
- apps/vorstellungsvermoegen-app.html +582 -0
- apps/zahlenreihen-app.html +476 -0
- assets/index-CwQxyZjZ.css +1 -0
- assets/index-Dpi-wtlo.js +0 -0
- index.html +14 -0
README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: BrainBit
|
| 3 |
+
emoji: 🧠
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: static
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
apps/analyse-programmierung-app.html
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="es">
|
| 3 |
+
<head>
|
| 4 |
+
<script>
|
| 5 |
+
/* BrainBit storage shim: sustituye window.storage (entorno artifacts de Claude.ai)
|
| 6 |
+
por localStorage con la misma interfaz async {value}. Notifica el progreso al parent. */
|
| 7 |
+
window.storage = {
|
| 8 |
+
async get(key){ const v = localStorage.getItem(key); return v === null ? null : { key, value: v }; },
|
| 9 |
+
async set(key, value){
|
| 10 |
+
localStorage.setItem(key, String(value));
|
| 11 |
+
try { if (window.parent !== window) window.parent.postMessage({ source: "brainbit-app", type: "progress", key: key, value: String(value) }, "*"); } catch(e){}
|
| 12 |
+
}
|
| 13 |
+
};
|
| 14 |
+
window.addEventListener("load", () => {
|
| 15 |
+
try {
|
| 16 |
+
if (window.parent === window) return;
|
| 17 |
+
for (let i = 0; i < localStorage.length; i++) {
|
| 18 |
+
const key = localStorage.key(i);
|
| 19 |
+
if (key && key.endsWith("-progress")) {
|
| 20 |
+
window.parent.postMessage({ source: "brainbit-app", type: "progress", key: key, value: localStorage.getItem(key) }, "*");
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
} catch(e){}
|
| 24 |
+
});
|
| 25 |
+
</script>
|
| 26 |
+
|
| 27 |
+
<meta charset="UTF-8">
|
| 28 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 29 |
+
<title>Analyse und Programmierung — ICT-Eignungstest</title>
|
| 30 |
+
<style>
|
| 31 |
+
:root{
|
| 32 |
+
--bg:#F7F6F3; --panel:#FFFFFF; --ink:#1C1C1C; --muted:#6B6864; --line:#D8D5CC;
|
| 33 |
+
--red:#D8232A; --pos:#1F7A4C; --pos-bg:#E6F1EA; --neg:#A63A22; --neg-bg:#F6E9E3;
|
| 34 |
+
--amber:#B8860B; --code-bg:#1C1C1C; --code-cur:#3A2E10;
|
| 35 |
+
--code-text:#E8E6DF; --code-num:#6B6864; --var-changed:#FBF3E4;
|
| 36 |
+
}
|
| 37 |
+
*{box-sizing:border-box;}
|
| 38 |
+
html,body{margin:0;padding:0;background:var(--bg);color:var(--ink);
|
| 39 |
+
font-family:"Helvetica Neue",Arial,sans-serif;-webkit-font-smoothing:antialiased;}
|
| 40 |
+
.mono{font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;}
|
| 41 |
+
|
| 42 |
+
header{background:var(--ink);color:#fff;padding:28px 20px 22px;position:relative;overflow:hidden;}
|
| 43 |
+
header .rail{position:absolute;left:0;right:0;bottom:0;height:4px;
|
| 44 |
+
background:repeating-linear-gradient(90deg,var(--red) 0 18px, transparent 18px 30px);opacity:.9;}
|
| 45 |
+
.eyebrow{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:#C9C6BD;font-weight:700;margin-bottom:8px;}
|
| 46 |
+
h1{font-size:26px;margin:0 0 4px;font-weight:800;letter-spacing:-0.01em;}
|
| 47 |
+
.sub{font-size:14px;color:#C9C6BD;margin:0;max-width:560px;line-height:1.4;}
|
| 48 |
+
|
| 49 |
+
nav.tabs{display:flex;background:var(--panel);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20;}
|
| 50 |
+
nav.tabs button{flex:1;padding:14px 8px;font-size:13px;font-weight:700;background:none;border:none;
|
| 51 |
+
color:var(--muted);cursor:pointer;border-bottom:3px solid transparent;}
|
| 52 |
+
nav.tabs button.active{color:var(--ink);border-bottom-color:var(--red);}
|
| 53 |
+
|
| 54 |
+
main{max-width:760px;margin:0 auto;padding:20px 16px 60px;}
|
| 55 |
+
section{display:none;}
|
| 56 |
+
section.active{display:block;}
|
| 57 |
+
|
| 58 |
+
h2.section-title{font-size:19px;font-weight:800;margin:26px 0 10px;}
|
| 59 |
+
h2.section-title:first-child{margin-top:4px;}
|
| 60 |
+
|
| 61 |
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;margin-bottom:14px;}
|
| 62 |
+
.card p{line-height:1.55;font-size:14.5px;margin:0 0 10px;}
|
| 63 |
+
.card p:last-child{margin-bottom:0;}
|
| 64 |
+
.trap-box{background:var(--neg-bg);border:1px solid #E3C4B5;border-radius:8px;padding:12px 14px;margin:10px 0;}
|
| 65 |
+
.trap-box b{color:var(--neg);}
|
| 66 |
+
.rule-box{background:#FBF3E4;border:1px solid #E9D9B4;border-radius:8px;padding:14px 16px;margin:10px 0;}
|
| 67 |
+
.rule-box b{color:var(--amber);}
|
| 68 |
+
|
| 69 |
+
.ex-card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:14px;}
|
| 70 |
+
.ex-head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:8px;}
|
| 71 |
+
.ex-num{font-size:11px;font-weight:800;letter-spacing:.06em;color:var(--muted);text-transform:uppercase;}
|
| 72 |
+
.level-tag{font-size:10px;font-weight:800;padding:3px 8px;border-radius:20px;text-transform:uppercase;}
|
| 73 |
+
.level-basico{background:#E6F1EA;color:var(--pos);}
|
| 74 |
+
.level-medio{background:#FBF3E4;color:var(--amber);}
|
| 75 |
+
.level-avanzado{background:var(--neg-bg);color:var(--neg);}
|
| 76 |
+
.level-extremo{background:#F0E3EE;color:#7A2A63;}
|
| 77 |
+
.ex-desc{font-size:14px;line-height:1.5;margin:6px 0 12px;}
|
| 78 |
+
.ex-question{font-size:14px;font-weight:700;margin:10px 0 0;padding:9px 12px;
|
| 79 |
+
background:#F2F0EA;border-left:3px solid var(--red);border-radius:4px;}
|
| 80 |
+
|
| 81 |
+
.stepper{margin-top:14px;}
|
| 82 |
+
.code-panel{background:var(--code-bg);border-radius:8px;padding:10px 0;overflow-x:auto;}
|
| 83 |
+
.code-line{display:flex;gap:12px;padding:2px 16px;font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
|
| 84 |
+
font-size:13px;color:var(--code-text);white-space:pre;line-height:1.65;}
|
| 85 |
+
.code-line .ln{color:var(--code-num);width:20px;text-align:right;flex-shrink:0;user-select:none;}
|
| 86 |
+
.code-line.current{background:var(--code-cur);}
|
| 87 |
+
.code-line.current::before{content:"▶";color:var(--red);position:relative;left:-2px;}
|
| 88 |
+
|
| 89 |
+
.stepper-controls{display:flex;align-items:center;gap:8px;margin-top:10px;flex-wrap:wrap;}
|
| 90 |
+
button.btn{border:none;border-radius:6px;padding:9px 14px;font-size:13px;font-weight:700;cursor:pointer;}
|
| 91 |
+
.btn-primary{background:var(--ink);color:#fff;}
|
| 92 |
+
.btn-primary:disabled{opacity:.35;cursor:default;}
|
| 93 |
+
.btn-secondary{background:#fff;color:var(--ink);border:1px solid var(--line);}
|
| 94 |
+
.btn-secondary:disabled{opacity:.35;cursor:default;}
|
| 95 |
+
.step-count{font-family:"SF Mono",ui-monospace,monospace;font-size:12px;color:var(--muted);margin-left:auto;}
|
| 96 |
+
|
| 97 |
+
.var-table-wrap{margin-top:10px;}
|
| 98 |
+
table.var-table{width:100%;border-collapse:collapse;font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;font-size:13px;}
|
| 99 |
+
table.var-table th{text-align:left;color:var(--muted);font-weight:700;padding:5px 8px;border-bottom:1px solid var(--line);}
|
| 100 |
+
table.var-table td{padding:6px 8px;border-bottom:1px solid var(--line);font-weight:700;}
|
| 101 |
+
table.var-table td.changed{background:var(--var-changed);color:var(--amber);border-radius:4px;}
|
| 102 |
+
.step-note{font-size:13px;color:var(--muted);margin-top:8px;font-style:italic;}
|
| 103 |
+
|
| 104 |
+
.final-box{margin-top:12px;padding:12px 14px;background:#F4F8F6;border:1px solid #CFE4D8;border-radius:8px;
|
| 105 |
+
font-size:14.5px;font-weight:700;color:var(--pos);text-align:center;}
|
| 106 |
+
.final-box.hidden-result{background:#F2F0EA;color:var(--muted);border-color:var(--line);font-weight:600;}
|
| 107 |
+
|
| 108 |
+
.ex-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px;}
|
| 109 |
+
.btn-master{background:#fff;color:var(--muted);border:1px solid var(--line);}
|
| 110 |
+
.btn-master.done{background:var(--pos-bg);color:var(--pos);border-color:var(--pos);}
|
| 111 |
+
|
| 112 |
+
.progress-hero{text-align:center;padding:20px 10px 8px;}
|
| 113 |
+
.progress-num{font-size:46px;font-weight:800;font-family:"SF Mono",ui-monospace,monospace;}
|
| 114 |
+
.progress-label{font-size:13px;color:var(--muted);margin-top:2px;}
|
| 115 |
+
.bar-track{height:8px;background:var(--line);border-radius:6px;overflow:hidden;margin:16px 0 22px;}
|
| 116 |
+
.bar-fill{height:100%;background:var(--red);transition:width .3s ease;}
|
| 117 |
+
.prog-row{display:flex;justify-content:space-between;align-items:center;padding:11px 4px;border-bottom:1px solid var(--line);font-size:14px;}
|
| 118 |
+
.prog-row:last-child{border-bottom:none;}
|
| 119 |
+
.prog-check{width:18px;height:18px;border-radius:50%;flex-shrink:0;}
|
| 120 |
+
.prog-check.on{background:var(--pos);}
|
| 121 |
+
.prog-check.off{background:var(--line);}
|
| 122 |
+
.reset-link{display:block;text-align:center;margin-top:20px;font-size:13px;color:var(--muted);
|
| 123 |
+
text-decoration:underline;cursor:pointer;background:none;border:none;}
|
| 124 |
+
.loading-note{font-size:12px;color:var(--muted);text-align:center;padding:10px;}
|
| 125 |
+
/* ===== Sprint ===== */
|
| 126 |
+
.sprint-setup{text-align:center;padding:10px 0 4px;}
|
| 127 |
+
.sprint-setup p{color:var(--muted);font-size:14px;line-height:1.5;}
|
| 128 |
+
.cat-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:14px 0;}
|
| 129 |
+
.cat-btn{padding:12px 6px;border:1.5px solid var(--line);border-radius:8px;background:#fff;
|
| 130 |
+
font-size:12.5px;font-weight:700;cursor:pointer;color:var(--ink);}
|
| 131 |
+
.cat-btn.sel{border-color:var(--red);background:#FDF1F1;}
|
| 132 |
+
.start-btn{width:100%;padding:15px;background:var(--red);color:#fff;border:none;border-radius:8px;
|
| 133 |
+
font-size:15px;font-weight:800;cursor:pointer;margin-top:6px;}
|
| 134 |
+
.sprint-hud{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;
|
| 135 |
+
font-family:"SF Mono",ui-monospace,monospace;font-weight:800;font-size:14px;}
|
| 136 |
+
.hud-timer.urgent{color:var(--red);}
|
| 137 |
+
.hud-score{color:var(--pos);}
|
| 138 |
+
.sprint-code{background:var(--ink);color:#E8E6DF;border-radius:8px;padding:14px;overflow-x:auto;
|
| 139 |
+
font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;font-size:13.5px;line-height:1.7;white-space:pre;}
|
| 140 |
+
.q-text{font-size:15px;font-weight:700;line-height:1.5;margin:12px 0 12px;}
|
| 141 |
+
.opt-grid2{display:grid;grid-template-columns:1fr 1fr;gap:9px;}
|
| 142 |
+
.opt-btn{padding:14px;border:1.5px solid var(--line);border-radius:8px;background:#fff;text-align:center;
|
| 143 |
+
font-size:16px;font-weight:800;cursor:pointer;font-family:"SF Mono",ui-monospace,monospace;}
|
| 144 |
+
.opt-btn:disabled{cursor:default;}
|
| 145 |
+
.opt-btn.right{border-color:var(--pos);background:#E6F1EA;color:var(--pos);}
|
| 146 |
+
.opt-btn.wrong{border-color:var(--neg);background:#F6E9E3;color:var(--neg);}
|
| 147 |
+
.q-feedback{margin-top:12px;font-size:13.5px;line-height:1.6;padding:12px;border-radius:8px;display:none;}
|
| 148 |
+
.q-feedback.show{display:block;}
|
| 149 |
+
.q-feedback.ok{background:#E6F1EA;color:var(--pos);}
|
| 150 |
+
.q-feedback.ko{background:#F6E9E3;color:var(--neg);}
|
| 151 |
+
.next-btn{margin-top:12px;width:100%;padding:13px;background:var(--ink);color:#fff;border:none;
|
| 152 |
+
border-radius:8px;font-size:14px;font-weight:800;cursor:pointer;display:none;}
|
| 153 |
+
.next-btn.show{display:block;}
|
| 154 |
+
.result-hero{text-align:center;padding:24px 10px;}
|
| 155 |
+
.result-num{font-size:52px;font-weight:800;font-family:"SF Mono",ui-monospace,monospace;}
|
| 156 |
+
.result-label{font-size:14px;color:var(--muted);}
|
| 157 |
+
.result-best{margin-top:8px;font-size:13px;color:var(--amber);font-weight:700;}
|
| 158 |
+
.stat-row2{display:flex;justify-content:space-between;align-items:center;padding:11px 4px;border-bottom:1px solid var(--line);font-size:14px;}
|
| 159 |
+
.stat-row2:last-child{border-bottom:none;}
|
| 160 |
+
.stat-acc{font-family:"SF Mono",ui-monospace,monospace;font-weight:800;}
|
| 161 |
+
.acc-good{color:var(--pos);} .acc-mid{color:var(--amber);} .acc-bad{color:var(--neg);} .acc-none{color:var(--muted);}
|
| 162 |
+
</style>
|
| 163 |
+
</head>
|
| 164 |
+
<body>
|
| 165 |
+
|
| 166 |
+
<header>
|
| 167 |
+
<div class="eyebrow">ICT-Eignungstest · ICT-Grundwissen</div>
|
| 168 |
+
<h1>Analyse und Programmierung</h1>
|
| 169 |
+
<p class="sub">Pseudocódigo abstracto, bucles anidados y trace tables — nivel Applikationsentwicklung. Sigue el código línea por línea como lo haría un procesador.</p>
|
| 170 |
+
<div class="rail"></div>
|
| 171 |
+
</header>
|
| 172 |
+
|
| 173 |
+
<nav class="tabs">
|
| 174 |
+
<button class="tab-btn active" data-tab="teoria">Teoría</button>
|
| 175 |
+
<button class="tab-btn" data-tab="sprint">⚡ Sprint</button>
|
| 176 |
+
<button class="tab-btn" data-tab="practicar">Trace Table</button>
|
| 177 |
+
<button class="tab-btn" data-tab="progreso">Progreso</button>
|
| 178 |
+
</nav>
|
| 179 |
+
|
| 180 |
+
<main>
|
| 181 |
+
|
| 182 |
+
<section id="teoria" class="active">
|
| 183 |
+
|
| 184 |
+
<h2 class="section-title">¿Qué evalúa exactamente?</h2>
|
| 185 |
+
<div class="card">
|
| 186 |
+
<p>No evalúa si memorizaste la sintaxis de un lenguaje. Evalúa tu <b>capacidad de ejecución mental</b>: si tu cerebro puede actuar como un procesador — mantener el valor de varias variables en memoria, aplicar condiciones simultáneas y calcular bucles anidados sin error lógico, bajo presión de tiempo.</p>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<h2 class="section-title">Sintaxis abstracta internacional</h2>
|
| 190 |
+
<div class="card">
|
| 191 |
+
<p>Olvida el pseudocódigo en español (tipo PSeInt: «Escribir», «Si...Entonces»). El examen usa palabras clave universales en inglés: <span class="mono">IF, ELSE, WHILE, FOR, AND, OR, NOT</span>, o diagramas de flujo con símbolos estandarizados. Como ya viste Python en CS50P, esa lógica es exactamente la que necesitas — solo cambia la sintaxis superficial.</p>
|
| 192 |
+
</div>
|
| 193 |
+
|
| 194 |
+
<h2 class="section-title">La estrategia de la Trace Table (tu mejor herramienta)</h2>
|
| 195 |
+
<div class="card">
|
| 196 |
+
<div class="rule-box">
|
| 197 |
+
<p><b>En tu hoja de borrador, dibuja una columna por cada variable.</b> Cada vez que el código cambie una variable, tacha el valor viejo y escribe el nuevo al lado. Nunca calcules bucles de más de 3 iteraciones solo mentalmente — te confundirás bajo presión.</p>
|
| 198 |
+
</div>
|
| 199 |
+
<p>Esta app hace exactamente eso por ti en la pestaña <b>Practicar</b>: verás el código resaltado línea por línea junto con una tabla de variables que se actualiza en vivo. Practica primero prediciendo tú, y usa el stepper para verificar.</p>
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
+
<h2 class="section-title">Las 4 trampas suizas más frecuentes</h2>
|
| 203 |
+
<div class="card">
|
| 204 |
+
<div class="trap-box">
|
| 205 |
+
<p><b>1. Desbordamiento silencioso de variable:</b> el examen cambia el valor de una variable sutilmente justo antes de que termine el bucle. Si no llevas el rastro paso a paso, arrastras el error hasta el final.</p>
|
| 206 |
+
</div>
|
| 207 |
+
<div class="trap-box">
|
| 208 |
+
<p><b>2. Rango de bucle que depende de otra variable:</b> en bucles anidados tipo <span class="mono">FOR J FROM 1 TO I</span>, el número de repeticiones del bucle interno cambia en cada vuelta del externo. Es la trampa #1 en exámenes reales.</p>
|
| 209 |
+
</div>
|
| 210 |
+
<div class="trap-box">
|
| 211 |
+
<p><b>3. Módulo (%) mal interpretado:</b> <span class="mono">10 % 2 == 0</span> es verdadero porque el residuo de 10÷2 es cero. Confundir esto con "es igual a 2" es un error muy común bajo presión.</p>
|
| 212 |
+
</div>
|
| 213 |
+
<div class="trap-box">
|
| 214 |
+
<p><b>4. Banderas lógicas (flags) que no se resetean:</b> en algoritmos con una variable tipo <span class="mono">SWAPPED = TRUE/FALSE</span>, olvidar que se resetea a FALSE al inicio de cada vuelta del bucle es el error más común en ejercicios de ordenamiento.</p>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
</section>
|
| 219 |
+
|
| 220 |
+
<section id="sprint">
|
| 221 |
+
<div id="sprint-area"></div>
|
| 222 |
+
</section>
|
| 223 |
+
|
| 224 |
+
<section id="practicar">
|
| 225 |
+
<h2 class="section-title">6 ejercicios — de básico a nivel examen real</h2>
|
| 226 |
+
<p style="font-size:13px;color:var(--muted);margin-top:-6px;margin-bottom:16px;">
|
| 227 |
+
Predice tú primero el resultado final. Luego usa «Siguiente paso» para verificar tu razonamiento línea por línea con la tabla de variables en vivo.
|
| 228 |
+
</p>
|
| 229 |
+
<div id="exercise-list"></div>
|
| 230 |
+
</section>
|
| 231 |
+
|
| 232 |
+
<section id="progreso">
|
| 233 |
+
<div class="progress-hero">
|
| 234 |
+
<div class="progress-num" id="prog-count">0 / 6</div>
|
| 235 |
+
<div class="progress-label">ejercicios dominados</div>
|
| 236 |
+
<div class="bar-track"><div class="bar-fill" id="prog-bar" style="width:0%"></div></div>
|
| 237 |
+
</div>
|
| 238 |
+
<div class="progress-hero" style="padding-top:0;">
|
| 239 |
+
<div class="progress-num" id="sprint-best" style="font-size:34px;">—</div>
|
| 240 |
+
<div class="progress-label">mejor Sprint (de 8)</div>
|
| 241 |
+
</div>
|
| 242 |
+
<h2 class="section-title">Precisión por tipo (Sprint)</h2>
|
| 243 |
+
<div class="card" id="sprint-stats-list"></div>
|
| 244 |
+
<h2 class="section-title">Detalle por ejercicio (Trace Table)</h2>
|
| 245 |
+
<div class="card" id="prog-list"></div>
|
| 246 |
+
<button class="reset-link" id="reset-btn">Reiniciar todo mi progreso</button>
|
| 247 |
+
<div class="loading-note" id="storage-note"></div>
|
| 248 |
+
</section>
|
| 249 |
+
|
| 250 |
+
</main>
|
| 251 |
+
|
| 252 |
+
<script>
|
| 253 |
+
const exercises = [
|
| 254 |
+
{
|
| 255 |
+
level:"basico", title:"Condiciones anidadas con operadores lógicos",
|
| 256 |
+
desc:"Valores iniciales: X = 12, Y = 5, Z = 3.",
|
| 257 |
+
lines:[
|
| 258 |
+
"INIT X, Y, Z",
|
| 259 |
+
"IF (X > 10 AND Y < 8) THEN",
|
| 260 |
+
" X = X - Y",
|
| 261 |
+
" IF (X + Z >= 10) THEN",
|
| 262 |
+
" Z = Z * 2",
|
| 263 |
+
" ELSE",
|
| 264 |
+
" Z = Z + 5",
|
| 265 |
+
" END_IF",
|
| 266 |
+
"ELSE",
|
| 267 |
+
" X = X + Y",
|
| 268 |
+
"END_IF",
|
| 269 |
+
"PRINT (X + Y + Z)"
|
| 270 |
+
],
|
| 271 |
+
question:"¿Cuál es el valor final impreso?",
|
| 272 |
+
steps:[
|
| 273 |
+
{line:0, vars:{X:12,Y:5,Z:3}, note:"Inicializamos las 3 variables."},
|
| 274 |
+
{line:1, vars:{X:12,Y:5,Z:3}, note:"(12 > 10 AND 5 < 8) → TRUE AND TRUE → entra al THEN."},
|
| 275 |
+
{line:2, vars:{X:7,Y:5,Z:3}, note:"X = X − Y = 12 − 5 = 7."},
|
| 276 |
+
{line:3, vars:{X:7,Y:5,Z:3}, note:"(7 + 3 >= 10) → 10 >= 10 → TRUE → entra al THEN interno."},
|
| 277 |
+
{line:4, vars:{X:7,Y:5,Z:6}, note:"Z = Z × 2 = 3 × 2 = 6."},
|
| 278 |
+
{line:11, vars:{X:7,Y:5,Z:6}, note:"Se ignoran los ELSE. PRINT (X+Y+Z) = 7+5+6."}
|
| 279 |
+
],
|
| 280 |
+
final:"18"
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
level:"basico", title:"El bucle contador invertido (módulo)",
|
| 284 |
+
desc:"TOTAL = 0, COUNT = 10. Recuerda: % es el residuo de la división entera.",
|
| 285 |
+
lines:[
|
| 286 |
+
"INIT TOTAL = 0",
|
| 287 |
+
"INIT COUNT = 10",
|
| 288 |
+
"WHILE (COUNT > 2) DO",
|
| 289 |
+
" IF (COUNT % 2 == 0) THEN",
|
| 290 |
+
" TOTAL = TOTAL + COUNT",
|
| 291 |
+
" ELSE",
|
| 292 |
+
" TOTAL = TOTAL - 1",
|
| 293 |
+
" END_IF",
|
| 294 |
+
" COUNT = COUNT - 2",
|
| 295 |
+
"END_WHILE",
|
| 296 |
+
"PRINT TOTAL"
|
| 297 |
+
],
|
| 298 |
+
question:"¿Cuál es el valor final de TOTAL?",
|
| 299 |
+
steps:[
|
| 300 |
+
{line:0, vars:{TOTAL:0,COUNT:10}, note:"Inicialización."},
|
| 301 |
+
{line:3, vars:{TOTAL:10,COUNT:10}, note:"COUNT=10 es par → TOTAL = 0+10 = 10."},
|
| 302 |
+
{line:8, vars:{TOTAL:10,COUNT:8}, note:"COUNT = 10−2 = 8."},
|
| 303 |
+
{line:3, vars:{TOTAL:18,COUNT:8}, note:"COUNT=8 es par → TOTAL = 10+8 = 18."},
|
| 304 |
+
{line:8, vars:{TOTAL:18,COUNT:6}, note:"COUNT = 8−2 = 6."},
|
| 305 |
+
{line:3, vars:{TOTAL:24,COUNT:6}, note:"COUNT=6 es par → TOTAL = 18+6 = 24."},
|
| 306 |
+
{line:8, vars:{TOTAL:24,COUNT:4}, note:"COUNT = 6−2 = 4."},
|
| 307 |
+
{line:3, vars:{TOTAL:28,COUNT:4}, note:"COUNT=4 es par → TOTAL = 24+4 = 28."},
|
| 308 |
+
{line:8, vars:{TOTAL:28,COUNT:2}, note:"COUNT = 4−2 = 2."},
|
| 309 |
+
{line:2, vars:{TOTAL:28,COUNT:2}, note:"¿COUNT > 2? → 2 > 2 es FALSO. El bucle termina."},
|
| 310 |
+
{line:10, vars:{TOTAL:28,COUNT:2}, note:"PRINT TOTAL."}
|
| 311 |
+
],
|
| 312 |
+
final:"28"
|
| 313 |
+
},
|
| 314 |
+
{
|
| 315 |
+
level:"medio", title:"Bucles anidados — el rango que cambia (¡trampa clásica!)",
|
| 316 |
+
desc:"K = 1. Observa cómo el límite del bucle interno (J) depende del valor actual de I.",
|
| 317 |
+
lines:[
|
| 318 |
+
"INIT K = 1",
|
| 319 |
+
"FOR I FROM 1 TO 3 DO",
|
| 320 |
+
" FOR J FROM 1 TO I DO",
|
| 321 |
+
" K = K + (I * J)",
|
| 322 |
+
" ENDFOR",
|
| 323 |
+
"ENDFOR",
|
| 324 |
+
"PRINT K"
|
| 325 |
+
],
|
| 326 |
+
question:"¿Cuál es el valor final de K?",
|
| 327 |
+
steps:[
|
| 328 |
+
{line:0, vars:{K:1,I:"-",J:"-"}, note:"Inicialización."},
|
| 329 |
+
{line:1, vars:{K:1,I:1,J:"-"}, note:"I = 1. El bucle interno correrá solo J=1 a 1 (1 sola vez)."},
|
| 330 |
+
{line:3, vars:{K:2,I:1,J:1}, note:"K = 1 + (1×1) = 2."},
|
| 331 |
+
{line:1, vars:{K:2,I:2,J:"-"}, note:"I = 2. Ahora J correrá de 1 a 2 (2 veces) — ¡el rango cambió!"},
|
| 332 |
+
{line:3, vars:{K:4,I:2,J:1}, note:"K = 2 + (2×1) = 4."},
|
| 333 |
+
{line:3, vars:{K:8,I:2,J:2}, note:"K = 4 + (2×2) = 8."},
|
| 334 |
+
{line:1, vars:{K:8,I:3,J:"-"}, note:"I = 3. J correrá de 1 a 3 (3 veces)."},
|
| 335 |
+
{line:3, vars:{K:11,I:3,J:1}, note:"K = 8 + (3×1) = 11."},
|
| 336 |
+
{line:3, vars:{K:17,I:3,J:2}, note:"K = 11 + (3×2) = 17."},
|
| 337 |
+
{line:3, vars:{K:26,I:3,J:3}, note:"K = 17 + (3×3) = 26."},
|
| 338 |
+
{line:6, vars:{K:26,I:3,J:3}, note:"PRINT K."}
|
| 339 |
+
],
|
| 340 |
+
final:"26"
|
| 341 |
+
},
|
| 342 |
+
{
|
| 343 |
+
level:"medio", title:"Diagrama de flujo abstracto (decisiones encadenadas)",
|
| 344 |
+
desc:"N = 15. Nota: // significa división entera (se descarta el decimal).",
|
| 345 |
+
lines:[
|
| 346 |
+
"INIT N = 15",
|
| 347 |
+
"IF (N % 3 == 0) THEN",
|
| 348 |
+
" N = N + 5",
|
| 349 |
+
"ELSE",
|
| 350 |
+
" N = N - 2",
|
| 351 |
+
"END_IF",
|
| 352 |
+
"IF (N >= 20) THEN",
|
| 353 |
+
" N = N * 2",
|
| 354 |
+
"ELSE",
|
| 355 |
+
" N = N // 2",
|
| 356 |
+
"END_IF",
|
| 357 |
+
"PRINT N"
|
| 358 |
+
],
|
| 359 |
+
question:"¿Cuál es el valor final de N?",
|
| 360 |
+
steps:[
|
| 361 |
+
{line:0, vars:{N:15}, note:"Inicialización."},
|
| 362 |
+
{line:1, vars:{N:15}, note:"¿15 % 3 == 0? → residuo es 0 → TRUE → entra al primer THEN."},
|
| 363 |
+
{line:2, vars:{N:20}, note:"N = 15 + 5 = 20."},
|
| 364 |
+
{line:6, vars:{N:20}, note:"¿N >= 20? → 20 >= 20 → TRUE → entra al segundo THEN."},
|
| 365 |
+
{line:7, vars:{N:40}, note:"N = 20 × 2 = 40."},
|
| 366 |
+
{line:11, vars:{N:40}, note:"PRINT N."}
|
| 367 |
+
],
|
| 368 |
+
final:"40"
|
| 369 |
+
},
|
| 370 |
+
{
|
| 371 |
+
level:"avanzado", title:"Algoritmo de ordenación con bandera (Bubble Sort)",
|
| 372 |
+
desc:"ARRAY = [4, 2, 7]. Sigue con cuidado los índices y la bandera SWAPPED.",
|
| 373 |
+
lines:[
|
| 374 |
+
"INIT ARRAY = [4, 2, 7]",
|
| 375 |
+
"INIT SWAPPED = TRUE",
|
| 376 |
+
"WHILE (SWAPPED == TRUE) DO",
|
| 377 |
+
" SWAPPED = FALSE",
|
| 378 |
+
" IF (ARRAY[0] > ARRAY[1]) THEN",
|
| 379 |
+
" SWAP(ARRAY[0], ARRAY[1])",
|
| 380 |
+
" SWAPPED = TRUE",
|
| 381 |
+
" END_IF",
|
| 382 |
+
" IF (ARRAY[1] > ARRAY[2]) THEN",
|
| 383 |
+
" SWAP(ARRAY[1], ARRAY[2])",
|
| 384 |
+
" SWAPPED = TRUE",
|
| 385 |
+
" END_IF",
|
| 386 |
+
"END_WHILE",
|
| 387 |
+
"PRINT (ARRAY[0] * ARRAY[1] + ARRAY[2])"
|
| 388 |
+
],
|
| 389 |
+
question:"¿Cuál es el resultado final impreso?",
|
| 390 |
+
steps:[
|
| 391 |
+
{line:0, vars:{ARRAY:"[4,2,7]",SWAPPED:"-"}, note:"Array inicial."},
|
| 392 |
+
{line:1, vars:{ARRAY:"[4,2,7]",SWAPPED:"TRUE"}, note:"SWAPPED empieza en TRUE para entrar al bucle."},
|
| 393 |
+
{line:3, vars:{ARRAY:"[4,2,7]",SWAPPED:"FALSE"}, note:"Al inicio de cada vuelta, SWAPPED se resetea a FALSE."},
|
| 394 |
+
{line:4, vars:{ARRAY:"[4,2,7]",SWAPPED:"FALSE"}, note:"¿ARRAY[0]>ARRAY[1]? → 4>2 → TRUE."},
|
| 395 |
+
{line:6, vars:{ARRAY:"[2,4,7]",SWAPPED:"TRUE"}, note:"Se intercambian: [2,4,7]. SWAPPED = TRUE."},
|
| 396 |
+
{line:8, vars:{ARRAY:"[2,4,7]",SWAPPED:"TRUE"}, note:"¿ARRAY[1]>ARRAY[2]? → 4>7 → FALSE. No hay cambio."},
|
| 397 |
+
{line:2, vars:{ARRAY:"[2,4,7]",SWAPPED:"TRUE"}, note:"SWAPPED era TRUE → el bucle se repite una vez más."},
|
| 398 |
+
{line:3, vars:{ARRAY:"[2,4,7]",SWAPPED:"FALSE"}, note:"Reseteamos SWAPPED a FALSE de nuevo."},
|
| 399 |
+
{line:4, vars:{ARRAY:"[2,4,7]",SWAPPED:"FALSE"}, note:"¿2>4? → FALSE."},
|
| 400 |
+
{line:8, vars:{ARRAY:"[2,4,7]",SWAPPED:"FALSE"}, note:"¿4>7? → FALSE."},
|
| 401 |
+
{line:2, vars:{ARRAY:"[2,4,7]",SWAPPED:"FALSE"}, note:"SWAPPED quedó en FALSE → el bucle termina. Array ordenado."},
|
| 402 |
+
{line:13, vars:{ARRAY:"[2,4,7]",SWAPPED:"FALSE"}, note:"PRINT (2×4)+7 = 8+7."}
|
| 403 |
+
],
|
| 404 |
+
final:"15"
|
| 405 |
+
},
|
| 406 |
+
{
|
| 407 |
+
level:"extremo", title:"Función recursiva — la pila de llamadas (call stack)",
|
| 408 |
+
desc:"Esta variante usa una función que se llama a sí misma. Muy relacionada con lo que ya viste en CS50P.",
|
| 409 |
+
lines:[
|
| 410 |
+
"FUNCTION factorial(n):",
|
| 411 |
+
" IF n <= 1 THEN",
|
| 412 |
+
" RETURN 1",
|
| 413 |
+
" ELSE",
|
| 414 |
+
" RETURN n * factorial(n - 1)",
|
| 415 |
+
" END_IF",
|
| 416 |
+
"END_FUNCTION",
|
| 417 |
+
"",
|
| 418 |
+
"PRINT factorial(4)"
|
| 419 |
+
],
|
| 420 |
+
question:"¿Cuál es el resultado final de factorial(4)?",
|
| 421 |
+
steps:[
|
| 422 |
+
{line:8, vars:{"llamada":"factorial(4)", pila:"[4]"}, note:"factorial(4): 4>1, entonces necesita el resultado de factorial(3) primero."},
|
| 423 |
+
{line:4, vars:{"llamada":"factorial(3)", pila:"[4,3]"}, note:"factorial(3): 3>1, necesita factorial(2)."},
|
| 424 |
+
{line:4, vars:{"llamada":"factorial(2)", pila:"[4,3,2]"}, note:"factorial(2): 2>1, necesita factorial(1)."},
|
| 425 |
+
{line:2, vars:{"llamada":"factorial(1)", pila:"[4,3,2,1]"}, note:"factorial(1): 1<=1 → caso base → RETURN 1 (no llama a nadie más)."},
|
| 426 |
+
{line:4, vars:{"retorna":"factorial(2) = 2×1 = 2", pila:"[4,3,2]"}, note:"factorial(2) recibe el 1 de abajo: 2 × factorial(1) = 2×1 = 2."},
|
| 427 |
+
{line:4, vars:{"retorna":"factorial(3) = 3×2 = 6", pila:"[4,3]"}, note:"factorial(3) recibe el 2: 3 × factorial(2) = 3×2 = 6."},
|
| 428 |
+
{line:4, vars:{"retorna":"factorial(4) = 4×6 = 24", pila:"[4]"}, note:"factorial(4) recibe el 6: 4 × factorial(3) = 4×6 = 24."},
|
| 429 |
+
{line:8, vars:{"resultado final":"24", pila:"[]"}, note:"La pila se vació. PRINT factorial(4) imprime el resultado final."}
|
| 430 |
+
],
|
| 431 |
+
final:"24"
|
| 432 |
+
}
|
| 433 |
+
];
|
| 434 |
+
|
| 435 |
+
const levelLabel = { basico:"Básico", medio:"Medio", avanzado:"Avanzado", extremo:"Extremo" };
|
| 436 |
+
|
| 437 |
+
let mastered = {};
|
| 438 |
+
let cursor = {};
|
| 439 |
+
|
| 440 |
+
function renderExercises(){
|
| 441 |
+
const list = document.getElementById("exercise-list");
|
| 442 |
+
list.innerHTML = exercises.map((ex, i) => {
|
| 443 |
+
if(cursor[i] === undefined) cursor[i] = -1;
|
| 444 |
+
const isMastered = !!mastered[i];
|
| 445 |
+
return `
|
| 446 |
+
<div class="ex-card" data-idx="${i}">
|
| 447 |
+
<div class="ex-head">
|
| 448 |
+
<span class="ex-num">Ejercicio ${i+1} / ${exercises.length} — ${ex.title}</span>
|
| 449 |
+
<span class="level-tag level-${ex.level}">${levelLabel[ex.level]}</span>
|
| 450 |
+
</div>
|
| 451 |
+
<p class="ex-desc">${ex.desc}</p>
|
| 452 |
+
<p class="ex-question">${ex.question}</p>
|
| 453 |
+
<div class="stepper" id="stepper-${i}"></div>
|
| 454 |
+
<div class="ex-actions">
|
| 455 |
+
<button class="btn btn-master ${isMastered ? 'done' : ''}" data-idx="${i}">
|
| 456 |
+
${isMastered ? '✓ Dominado' : 'Marcar dominado'}
|
| 457 |
+
</button>
|
| 458 |
+
</div>
|
| 459 |
+
</div>`;
|
| 460 |
+
}).join("");
|
| 461 |
+
|
| 462 |
+
exercises.forEach((ex, i) => renderStepper(i));
|
| 463 |
+
|
| 464 |
+
list.querySelectorAll(".btn-master").forEach(btn => {
|
| 465 |
+
btn.addEventListener("click", () => toggleMastered(parseInt(btn.dataset.idx)));
|
| 466 |
+
});
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
function renderStepper(i){
|
| 470 |
+
const ex = exercises[i];
|
| 471 |
+
const el = document.getElementById(`stepper-${i}`);
|
| 472 |
+
const step = cursor[i] >= 0 ? ex.steps[cursor[i]] : null;
|
| 473 |
+
const prevStep = cursor[i] > 0 ? ex.steps[cursor[i]-1] : null;
|
| 474 |
+
|
| 475 |
+
const codeHtml = ex.lines.map((ln, li) => {
|
| 476 |
+
const isCurrent = step && step.line === li;
|
| 477 |
+
return `<div class="code-line ${isCurrent ? 'current' : ''}"><span class="ln">${li+1}</span><span>${escapeHtml(ln)}</span></div>`;
|
| 478 |
+
}).join("");
|
| 479 |
+
|
| 480 |
+
let varsHtml = "";
|
| 481 |
+
if(step){
|
| 482 |
+
const keys = Object.keys(step.vars);
|
| 483 |
+
varsHtml = `<div class="var-table-wrap"><table class="var-table"><tr>${keys.map(k=>`<th>${k}</th>`).join("")}</tr><tr>${
|
| 484 |
+
keys.map(k => {
|
| 485 |
+
const changed = prevStep && prevStep.vars[k] !== step.vars[k];
|
| 486 |
+
return `<td class="${changed ? 'changed' : ''}">${step.vars[k]}</td>`;
|
| 487 |
+
}).join("")
|
| 488 |
+
}</tr></table></div><p class="step-note">${step.note}</p>`;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
const atStart = cursor[i] <= -1;
|
| 492 |
+
const atEnd = cursor[i] >= ex.steps.length - 1;
|
| 493 |
+
const showFinal = atEnd;
|
| 494 |
+
|
| 495 |
+
el.innerHTML = `
|
| 496 |
+
<div class="code-panel">${codeHtml}</div>
|
| 497 |
+
${varsHtml}
|
| 498 |
+
<div class="stepper-controls">
|
| 499 |
+
<button class="btn btn-secondary step-prev" data-idx="${i}" ${atStart ? 'disabled' : ''}>◀ Anterior</button>
|
| 500 |
+
<button class="btn btn-primary step-next" data-idx="${i}" ${atEnd ? 'disabled' : ''}>Siguiente paso ▶</button>
|
| 501 |
+
<button class="btn btn-secondary step-reset" data-idx="${i}">↺ Reiniciar</button>
|
| 502 |
+
<span class="step-count">Paso ${Math.max(cursor[i]+1,0)} / ${ex.steps.length}</span>
|
| 503 |
+
</div>
|
| 504 |
+
<div class="final-box ${showFinal ? '' : 'hidden-result'}">
|
| 505 |
+
${showFinal ? `Resultado final: ${ex.final}` : "El resultado final aparecerá cuando termines todos los pasos"}
|
| 506 |
+
</div>
|
| 507 |
+
`;
|
| 508 |
+
|
| 509 |
+
el.querySelector(".step-next").addEventListener("click", () => {
|
| 510 |
+
if(cursor[i] < ex.steps.length - 1){ cursor[i]++; renderStepper(i); }
|
| 511 |
+
});
|
| 512 |
+
el.querySelector(".step-prev").addEventListener("click", () => {
|
| 513 |
+
if(cursor[i] > -1){ cursor[i]--; renderStepper(i); }
|
| 514 |
+
});
|
| 515 |
+
el.querySelector(".step-reset").addEventListener("click", () => {
|
| 516 |
+
cursor[i] = -1; renderStepper(i);
|
| 517 |
+
});
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
function escapeHtml(s){
|
| 521 |
+
return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
async function toggleMastered(idx){
|
| 525 |
+
mastered[idx] = !mastered[idx];
|
| 526 |
+
await saveState();
|
| 527 |
+
renderExercises();
|
| 528 |
+
renderProgress();
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
function renderProgress(){
|
| 532 |
+
const count = Object.values(mastered).filter(Boolean).length;
|
| 533 |
+
document.getElementById("prog-count").textContent = `${count} / ${exercises.length}`;
|
| 534 |
+
document.getElementById("prog-bar").style.width = `${(count/exercises.length)*100}%`;
|
| 535 |
+
const list = document.getElementById("prog-list");
|
| 536 |
+
list.innerHTML = exercises.map((ex, i) => `
|
| 537 |
+
<div class="prog-row">
|
| 538 |
+
<span>Ejercicio ${i+1} · ${ex.title}</span>
|
| 539 |
+
<span class="prog-check ${mastered[i] ? 'on' : 'off'}"></span>
|
| 540 |
+
</div>
|
| 541 |
+
`).join("");
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
async function loadState(){
|
| 545 |
+
try{
|
| 546 |
+
const res = await window.storage.get("analyse-programmierung-progress");
|
| 547 |
+
if(res && res.value){
|
| 548 |
+
const data = JSON.parse(res.value);
|
| 549 |
+
mastered = data.mastered || {};
|
| 550 |
+
sprintStats = data.stats || {};
|
| 551 |
+
sprintBest = data.best || 0;
|
| 552 |
+
}
|
| 553 |
+
}catch(e){ /* no data yet */ }
|
| 554 |
+
}
|
| 555 |
+
async function saveState(){
|
| 556 |
+
try{
|
| 557 |
+
await window.storage.set("analyse-programmierung-progress", JSON.stringify({ mastered, stats: sprintStats, best: sprintBest }));
|
| 558 |
+
}catch(e){
|
| 559 |
+
const note = document.getElementById("storage-note");
|
| 560 |
+
if(note) note.textContent = "No se pudo guardar el progreso (modo sin conexión).";
|
| 561 |
+
}
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
document.getElementById("reset-btn").addEventListener("click", async () => {
|
| 565 |
+
if(!confirm("¿Reiniciar todo tu progreso? Esta acción no se puede deshacer.")) return;
|
| 566 |
+
mastered = {};
|
| 567 |
+
sprintStats = {}; sprintBest = 0;
|
| 568 |
+
await saveState();
|
| 569 |
+
renderExercises(); renderProgress(); renderSprintStats();
|
| 570 |
+
});
|
| 571 |
+
|
| 572 |
+
/* ============================================================
|
| 573 |
+
SPRINT GENERATIVO — verificado con 25.000 programas (0 fallos).
|
| 574 |
+
La respuesta correcta se obtiene EJECUTANDO el programa;
|
| 575 |
+
los distractores, ejecutándolo con los bugs mentales típicos.
|
| 576 |
+
============================================================ */
|
| 577 |
+
let sprintStats = {}, sprintBest = 0;
|
| 578 |
+
const riS = (a,b) => a + Math.floor(Math.random()*(b-a+1));
|
| 579 |
+
const pickS = arr => arr[riS(0,arr.length-1)];
|
| 580 |
+
const shuffleS = arr => { const a=[...arr]; for(let i=a.length-1;i>0;i--){const j=riS(0,i);[a[i],a[j]]=[a[j],a[i]];} return a; };
|
| 581 |
+
|
| 582 |
+
function buildOptsS(correct, wrongs){
|
| 583 |
+
const opts = new Set([correct]);
|
| 584 |
+
for(const w of wrongs){ if(opts.size<4 && w!==correct && Number.isFinite(w) && w>=0) opts.add(w); }
|
| 585 |
+
let guard=0;
|
| 586 |
+
while(opts.size<4 && guard++<50) opts.add(correct + pickS([-2,-1,1,2,3])*pickS([1,2]));
|
| 587 |
+
return shuffleS([...opts].slice(0,4));
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
/* 1 · Bucle con MOD (suma o conteo) */
|
| 591 |
+
function genModLoop(){
|
| 592 |
+
for(let t=0;t<40;t++){
|
| 593 |
+
const lo=riS(1,5), hi=lo+riS(6,12), m=pickS([2,3,4]), mode=pickS(["sum","count"]);
|
| 594 |
+
const run=(hiB,inv)=>{let T=0;for(let x=lo;x<=hiB;x++){if(inv ? x%m!==0 : x%m===0) T+=(mode==="sum"?x:1);}return T;};
|
| 595 |
+
const ans=run(hi,false), offByOne=run(hi-1,false), modInv=run(hi,true);
|
| 596 |
+
if(ans===offByOne && ans===modInv) continue;
|
| 597 |
+
return { type:"modloop",
|
| 598 |
+
code:`TOTAL = 0\nFOR X FROM ${lo} TO ${hi}\n IF X MOD ${m} == 0 THEN\n TOTAL = TOTAL + ${mode==="sum"?"X":"1"}\nPRINT(TOTAL)`,
|
| 599 |
+
ans, options:buildOptsS(ans,[offByOne,modInv]),
|
| 600 |
+
explain:`${mode==="sum"?"Suma":"Cuenta"} los X entre ${lo} y ${hi} divisibles por ${m} → ${ans}. Trampas: parar en ${hi-1} (FROM...TO es inclusivo) daría ${offByOne}; leer MOD al revés daría ${modInv}.` };
|
| 601 |
+
}
|
| 602 |
+
return genNested();
|
| 603 |
+
}
|
| 604 |
+
/* 2 · FOR anidado con límite interno variable */
|
| 605 |
+
function genNested(){
|
| 606 |
+
const n=riS(3,6), mode=pickS(["count","sumi"]);
|
| 607 |
+
const run=(inner)=>{let K=0;for(let a=1;a<=n;a++){for(let b=1;b<=inner(a);b++){K+=(mode==="count"?1:a);}}return K;};
|
| 608 |
+
const ans=run(a=>a), fixedTrap=run(_=>n);
|
| 609 |
+
return { type:"nested",
|
| 610 |
+
code:`K = 0\nFOR I FROM 1 TO ${n}\n FOR J FROM 1 TO I\n K = K + ${mode==="count"?"1":"I"}\nPRINT(K)`,
|
| 611 |
+
ans, options:buildOptsS(ans,[fixedTrap, ans+n, Math.max(1,ans-n)]),
|
| 612 |
+
explain:`El bucle interno corre I veces (varía en cada vuelta): ${[...Array(n)].map((_,i)=>mode==="count"?(i+1):((i+1)*(i+1))).join("+")} = ${ans}. Trampa #1 del examen: creer que J siempre llega a ${n} daría ${fixedTrap}.` };
|
| 613 |
+
}
|
| 614 |
+
/* 3 · Cadena condicional con AND */
|
| 615 |
+
function genCond(){
|
| 616 |
+
for(let t=0;t<40;t++){
|
| 617 |
+
const X=riS(2,15), Y=riS(2,15), Z=riS(2,15);
|
| 618 |
+
const branch=(c1,c2)=> c1&&c2 ? X+Z : c1 ? X-Z : Y*2;
|
| 619 |
+
const ans=branch(X>Y, Z<Y), swapTrap=branch(!(X>Y), Z<Y);
|
| 620 |
+
const others=[X+Z, X-Z, Y*2].filter(v=>v!==ans);
|
| 621 |
+
if(ans===swapTrap && others.length<2) continue;
|
| 622 |
+
return { type:"cond",
|
| 623 |
+
code:`X = ${X} Y = ${Y} Z = ${Z}\nIF X > Y AND Z < Y THEN\n R = X + Z\nELSE IF X > Y THEN\n R = X - Z\nELSE\n R = Y * 2\nPRINT(R)`,
|
| 624 |
+
ans, options:buildOptsS(ans,[...others, swapTrap]),
|
| 625 |
+
explain:`X>Y es ${X>Y ? "VERDADERO" : "FALSO"}${X>Y ? `, Z<Y es ${Z<Y?"VERDADERO":"FALSO"}` : ""} → rama ${ans===X+Z?"1 (X+Z)":ans===X-Z?"2 (X−Z)":"3 (Y×2)"} = ${ans}. Los distractores son los valores de las ramas NO tomadas.` };
|
| 626 |
+
}
|
| 627 |
+
return genNested();
|
| 628 |
+
}
|
| 629 |
+
/* 4 · Secuencia de asignaciones (trazado puro) */
|
| 630 |
+
function genAssign(){
|
| 631 |
+
for(let t=0;t<40;t++){
|
| 632 |
+
const a0=riS(2,9), b0=riS(2,9), c0=riS(2,9);
|
| 633 |
+
let A=a0,B=b0,C=c0;
|
| 634 |
+
A=B+C; B=A-C; C=A+B;
|
| 635 |
+
const ans=C;
|
| 636 |
+
const fullStale=a0+b0; // bug: evaluar C=A+B con los valores INICIALES
|
| 637 |
+
const mixed=(b0+c0)+b0; // bug: A actualizado pero B viejo
|
| 638 |
+
if(ans===fullStale && ans===mixed) continue;
|
| 639 |
+
return { type:"assign",
|
| 640 |
+
code:`A = ${a0} B = ${b0} C = ${c0}\nA = B + C\nB = A - C\nC = A + B\nPRINT(C)`,
|
| 641 |
+
ans, options:buildOptsS(ans,[fullStale,mixed]),
|
| 642 |
+
explain:`Traza: A=${b0}+${c0}=${b0+c0} → B=${b0+c0}−${c0}=${b0} → C=${b0+c0}+${b0}=${ans}. Cada línea usa los valores YA actualizados. Usar los iniciales daría ${fullStale}.` };
|
| 643 |
+
}
|
| 644 |
+
return genNested();
|
| 645 |
+
}
|
| 646 |
+
/* 5 · Recursión (factorial o suma) */
|
| 647 |
+
function genRec(){
|
| 648 |
+
const n=riS(3,6), kind=pickS(["fact","sum"]);
|
| 649 |
+
const f=x=> x<=1 ? 1 : (kind==="fact" ? x*f(x-1) : x+f(x-1));
|
| 650 |
+
const ans=f(n), offTrap=f(n-1);
|
| 651 |
+
return { type:"rec",
|
| 652 |
+
code:`FUNCTION F(N):\n IF N <= 1 THEN RETURN 1\n RETURN N ${kind==="fact"?"*":"+"} F(N - 1)\n\nPRINT(F(${n}))`,
|
| 653 |
+
ans, options:buildOptsS(ans,[offTrap, kind==="fact"?ans/n*(n-1):ans-1, ans+n]),
|
| 654 |
+
explain:`Pila de llamadas: F(${n})${kind==="fact"?"×":"+"}F(${n-1})… hasta F(1)=1 → ${[...Array(n)].map((_,i)=>n-i).join(kind==="fact"?"×":"+")} = ${ans}. Trampa: descontar un nivel de recursión daría ${offTrap}.` };
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
const SPRINT_GENS = {
|
| 658 |
+
modloop:{fn:genModLoop, label:"Bucle + MOD"},
|
| 659 |
+
nested: {fn:genNested, label:"FOR anidado variable"},
|
| 660 |
+
cond: {fn:genCond, label:"Condicionales AND"},
|
| 661 |
+
assign: {fn:genAssign, label:"Asignaciones en cadena"},
|
| 662 |
+
rec: {fn:genRec, label:"Recursión"}
|
| 663 |
+
};
|
| 664 |
+
const SPRINT_N = 8, SPRINT_SECONDS = 300;
|
| 665 |
+
let sprintRun=null, sprintTimer=null, selCats = new Set(Object.keys(SPRINT_GENS));
|
| 666 |
+
|
| 667 |
+
function renderSprintSetup(){
|
| 668 |
+
clearInterval(sprintTimer);
|
| 669 |
+
const area=document.getElementById("sprint-area");
|
| 670 |
+
area.innerHTML=`
|
| 671 |
+
<div class="card sprint-setup">
|
| 672 |
+
<h2 class="section-title" style="margin-top:0;">Sprint: ${SPRINT_N} programas · ${SPRINT_SECONDS/60} minutos</h2>
|
| 673 |
+
<p>Cada programa se genera al azar y su salida se calcula ejecutándolo. Los distractores también son ejecuciones reales — pero con el bug mental típico (límite exclusivo, MOD invertido, valores sin actualizar, un nivel menos de recursión). Predice PRINT() sin papel.</p>
|
| 674 |
+
<div class="cat-grid">
|
| 675 |
+
${Object.entries(SPRINT_GENS).map(([k,g])=>`<button class="cat-btn ${selCats.has(k)?'sel':''}" data-cat="${k}">${g.label}</button>`).join("")}
|
| 676 |
+
</div>
|
| 677 |
+
<button class="start-btn" id="start-sprint">▶ Empezar Sprint</button>
|
| 678 |
+
</div>`;
|
| 679 |
+
area.querySelectorAll(".cat-btn").forEach(b=>b.addEventListener("click",()=>{
|
| 680 |
+
const k=b.dataset.cat;
|
| 681 |
+
if(selCats.has(k)){ if(selCats.size>1) selCats.delete(k); } else selCats.add(k);
|
| 682 |
+
renderSprintSetup();
|
| 683 |
+
}));
|
| 684 |
+
area.querySelector("#start-sprint").addEventListener("click", startSprint);
|
| 685 |
+
}
|
| 686 |
+
function startSprint(){
|
| 687 |
+
const cats=[...selCats];
|
| 688 |
+
const qs=[];
|
| 689 |
+
for(let i=0;i<SPRINT_N;i++) qs.push(SPRINT_GENS[cats[i%cats.length]].fn());
|
| 690 |
+
sprintRun={ qs:shuffleS(qs), idx:0, score:0, remaining:SPRINT_SECONDS };
|
| 691 |
+
sprintTimer=setInterval(()=>{
|
| 692 |
+
sprintRun.remaining--;
|
| 693 |
+
const el=document.getElementById("hud-timer");
|
| 694 |
+
if(el){
|
| 695 |
+
el.textContent=`${Math.floor(sprintRun.remaining/60)}:${(sprintRun.remaining%60).toString().padStart(2,"0")}`;
|
| 696 |
+
el.classList.toggle("urgent", sprintRun.remaining<=30);
|
| 697 |
+
}
|
| 698 |
+
if(sprintRun.remaining<=0) endSprint(true);
|
| 699 |
+
},1000);
|
| 700 |
+
renderSprintQ();
|
| 701 |
+
}
|
| 702 |
+
function renderSprintQ(){
|
| 703 |
+
const q=sprintRun.qs[sprintRun.idx];
|
| 704 |
+
const area=document.getElementById("sprint-area");
|
| 705 |
+
area.innerHTML=`
|
| 706 |
+
<div class="card">
|
| 707 |
+
<div class="sprint-hud">
|
| 708 |
+
<span>${sprintRun.idx+1} / ${SPRINT_N}</span>
|
| 709 |
+
<span class="hud-timer" id="hud-timer">${Math.floor(sprintRun.remaining/60)}:${(sprintRun.remaining%60).toString().padStart(2,"0")}</span>
|
| 710 |
+
<span class="hud-score">✓ ${sprintRun.score}</span>
|
| 711 |
+
</div>
|
| 712 |
+
<div class="sprint-code">${escapeHtml(q.code)}</div>
|
| 713 |
+
<p class="q-text">¿Qué imprime PRINT()?</p>
|
| 714 |
+
<div class="opt-grid2">
|
| 715 |
+
${q.options.map(o=>`<button class="opt-btn" data-val="${o}">${o}</button>`).join("")}
|
| 716 |
+
</div>
|
| 717 |
+
<div class="q-feedback" id="q-feedback"></div>
|
| 718 |
+
<button class="next-btn" id="next-btn">Siguiente →</button>
|
| 719 |
+
</div>`;
|
| 720 |
+
area.querySelectorAll(".opt-btn").forEach(btn=>btn.addEventListener("click",()=>answerSprint(btn,q)));
|
| 721 |
+
}
|
| 722 |
+
function answerSprint(btn,q){
|
| 723 |
+
const area=document.getElementById("sprint-area");
|
| 724 |
+
const isRight=parseInt(btn.dataset.val,10)===q.ans;
|
| 725 |
+
area.querySelectorAll(".opt-btn").forEach(b=>{
|
| 726 |
+
b.disabled=true;
|
| 727 |
+
if(parseInt(b.dataset.val,10)===q.ans) b.classList.add("right");
|
| 728 |
+
else if(b===btn&&!isRight) b.classList.add("wrong");
|
| 729 |
+
});
|
| 730 |
+
if(isRight) sprintRun.score++;
|
| 731 |
+
if(!sprintStats[q.type]) sprintStats[q.type]={ok:0,total:0};
|
| 732 |
+
sprintStats[q.type].total++; if(isRight) sprintStats[q.type].ok++;
|
| 733 |
+
saveState();
|
| 734 |
+
const fb=document.getElementById("q-feedback");
|
| 735 |
+
fb.className=`q-feedback show ${isRight?'ok':'ko'}`;
|
| 736 |
+
fb.innerHTML=`${isRight?"✓ Correcto. ":"✗ Incorrecto. "}${q.explain}`;
|
| 737 |
+
document.querySelector(".hud-score").textContent=`✓ ${sprintRun.score}`;
|
| 738 |
+
const nb=document.getElementById("next-btn");
|
| 739 |
+
nb.classList.add("show");
|
| 740 |
+
nb.textContent=sprintRun.idx+1<SPRINT_N?"Siguiente →":"Ver resultado";
|
| 741 |
+
nb.addEventListener("click",()=>{ sprintRun.idx++; sprintRun.idx<SPRINT_N ? renderSprintQ() : endSprint(false); });
|
| 742 |
+
}
|
| 743 |
+
function endSprint(timedOut){
|
| 744 |
+
clearInterval(sprintTimer);
|
| 745 |
+
const prev=sprintBest;
|
| 746 |
+
if(sprintRun.score>sprintBest){ sprintBest=sprintRun.score; saveState(); }
|
| 747 |
+
const area=document.getElementById("sprint-area");
|
| 748 |
+
area.innerHTML=`
|
| 749 |
+
<div class="card result-hero">
|
| 750 |
+
<div class="result-num">${sprintRun.score} / ${SPRINT_N}</div>
|
| 751 |
+
<div class="result-label">${timedOut?"⏱ Se acabó el tiempo":"Sprint completado"}</div>
|
| 752 |
+
${sprintRun.score>prev?`<div class="result-best">★ ¡Nuevo récord personal!</div>`:`<div class="result-best" style="color:var(--muted)">Mejor marca: ${sprintBest}/${SPRINT_N}</div>`}
|
| 753 |
+
<button class="start-btn" id="again">▶ Otro Sprint</button>
|
| 754 |
+
<button class="reset-link" id="back-setup">Cambiar tipos de programa</button>
|
| 755 |
+
</div>`;
|
| 756 |
+
area.querySelector("#again").addEventListener("click", startSprint);
|
| 757 |
+
area.querySelector("#back-setup").addEventListener("click", renderSprintSetup);
|
| 758 |
+
renderSprintStats();
|
| 759 |
+
}
|
| 760 |
+
function accClassS(p){ return p>=80?"acc-good":p>=50?"acc-mid":"acc-bad"; }
|
| 761 |
+
function renderSprintStats(){
|
| 762 |
+
const bestEl=document.getElementById("sprint-best");
|
| 763 |
+
if(bestEl) bestEl.textContent = sprintBest>0?`${sprintBest} / ${SPRINT_N}`:"—";
|
| 764 |
+
const list=document.getElementById("sprint-stats-list");
|
| 765 |
+
if(list) list.innerHTML = Object.entries(SPRINT_GENS).map(([k,g])=>{
|
| 766 |
+
const s=sprintStats[k];
|
| 767 |
+
const inner = s&&s.total>0
|
| 768 |
+
? `<span class="stat-acc ${accClassS(Math.round(s.ok/s.total*100))}">${Math.round(s.ok/s.total*100)}% (${s.ok}/${s.total})</span>`
|
| 769 |
+
: `<span class="stat-acc acc-none">sin datos</span>`;
|
| 770 |
+
return `<div class="stat-row2"><span>${g.label}</span>${inner}</div>`;
|
| 771 |
+
}).join("");
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
document.querySelectorAll(".tab-btn").forEach(btn => {
|
| 775 |
+
btn.addEventListener("click", () => {
|
| 776 |
+
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
| 777 |
+
document.querySelectorAll("main > section").forEach(s => s.classList.remove("active"));
|
| 778 |
+
btn.classList.add("active");
|
| 779 |
+
document.getElementById(btn.dataset.tab).classList.add("active");
|
| 780 |
+
});
|
| 781 |
+
});
|
| 782 |
+
|
| 783 |
+
(async function init(){
|
| 784 |
+
await loadState();
|
| 785 |
+
renderExercises();
|
| 786 |
+
renderProgress();
|
| 787 |
+
renderSprintSetup();
|
| 788 |
+
renderSprintStats();
|
| 789 |
+
})();
|
| 790 |
+
</script>
|
| 791 |
+
</body>
|
| 792 |
+
</html>
|
apps/konzentration-merkfaehigkeit-app.html
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="es">
|
| 3 |
+
<head>
|
| 4 |
+
<script>
|
| 5 |
+
/* BrainBit storage shim: sustituye window.storage (entorno artifacts de Claude.ai)
|
| 6 |
+
por localStorage con la misma interfaz async {value}. Notifica el progreso al parent. */
|
| 7 |
+
window.storage = {
|
| 8 |
+
async get(key){ const v = localStorage.getItem(key); return v === null ? null : { key, value: v }; },
|
| 9 |
+
async set(key, value){
|
| 10 |
+
localStorage.setItem(key, String(value));
|
| 11 |
+
try { if (window.parent !== window) window.parent.postMessage({ source: "brainbit-app", type: "progress", key: key, value: String(value) }, "*"); } catch(e){}
|
| 12 |
+
}
|
| 13 |
+
};
|
| 14 |
+
window.addEventListener("load", () => {
|
| 15 |
+
try {
|
| 16 |
+
if (window.parent === window) return;
|
| 17 |
+
for (let i = 0; i < localStorage.length; i++) {
|
| 18 |
+
const key = localStorage.key(i);
|
| 19 |
+
if (key && key.endsWith("-progress")) {
|
| 20 |
+
window.parent.postMessage({ source: "brainbit-app", type: "progress", key: key, value: localStorage.getItem(key) }, "*");
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
} catch(e){}
|
| 24 |
+
});
|
| 25 |
+
</script>
|
| 26 |
+
|
| 27 |
+
<meta charset="UTF-8">
|
| 28 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 29 |
+
<title>Konzentration & Merkfähigkeit — ICT-Eignungstest</title>
|
| 30 |
+
<style>
|
| 31 |
+
:root{
|
| 32 |
+
--bg:#F7F6F3; --panel:#FFFFFF; --ink:#1C1C1C; --muted:#6B6864; --line:#D8D5CC;
|
| 33 |
+
--red:#D8232A; --pos:#1F7A4C; --pos-bg:#E6F1EA; --neg:#A63A22; --neg-bg:#F6E9E3;
|
| 34 |
+
--amber:#B8860B; --purple:#7A2A63; --purple-bg:#F0E3EE; --blue:#2E6DA4;
|
| 35 |
+
}
|
| 36 |
+
*{box-sizing:border-box;}
|
| 37 |
+
html,body{margin:0;padding:0;background:var(--bg);color:var(--ink);
|
| 38 |
+
font-family:"Helvetica Neue",Arial,sans-serif;-webkit-font-smoothing:antialiased;}
|
| 39 |
+
.mono{font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;}
|
| 40 |
+
|
| 41 |
+
header{background:var(--ink);color:#fff;padding:28px 20px 22px;position:relative;overflow:hidden;}
|
| 42 |
+
header .rail{position:absolute;left:0;right:0;bottom:0;height:4px;
|
| 43 |
+
background:repeating-linear-gradient(90deg,var(--red) 0 18px, transparent 18px 30px);opacity:.9;}
|
| 44 |
+
.eyebrow{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:#C9C6BD;font-weight:700;margin-bottom:8px;}
|
| 45 |
+
h1{font-size:23px;margin:0 0 4px;font-weight:800;letter-spacing:-0.01em;}
|
| 46 |
+
.sub{font-size:14px;color:#C9C6BD;margin:0;max-width:560px;line-height:1.4;}
|
| 47 |
+
|
| 48 |
+
nav.tabs{display:flex;background:var(--panel);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20;}
|
| 49 |
+
nav.tabs button{flex:1;padding:14px 4px;font-size:12px;font-weight:700;background:none;border:none;
|
| 50 |
+
color:var(--muted);cursor:pointer;border-bottom:3px solid transparent;}
|
| 51 |
+
nav.tabs button.active{color:var(--ink);border-bottom-color:var(--red);}
|
| 52 |
+
|
| 53 |
+
main{max-width:760px;margin:0 auto;padding:20px 16px 60px;}
|
| 54 |
+
section{display:none;}
|
| 55 |
+
section.active{display:block;}
|
| 56 |
+
|
| 57 |
+
h2.section-title{font-size:19px;font-weight:800;margin:26px 0 10px;}
|
| 58 |
+
h2.section-title:first-child{margin-top:4px;}
|
| 59 |
+
|
| 60 |
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;margin-bottom:14px;}
|
| 61 |
+
.card p{line-height:1.55;font-size:14.5px;margin:0 0 10px;}
|
| 62 |
+
.card p:last-child{margin-bottom:0;}
|
| 63 |
+
.rule-box{background:#FBF3E4;border:1px solid #E9D9B4;border-radius:8px;padding:14px 16px;margin:10px 0;}
|
| 64 |
+
.rule-box b{color:var(--amber);}
|
| 65 |
+
|
| 66 |
+
.sprint-setup{text-align:center;padding:10px 0 4px;}
|
| 67 |
+
.sprint-setup p{color:var(--muted);font-size:14px;line-height:1.5;}
|
| 68 |
+
.cat-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:14px 0;}
|
| 69 |
+
.cat-btn{padding:13px 8px;border:1.5px solid var(--line);border-radius:8px;background:#fff;
|
| 70 |
+
font-size:13px;font-weight:700;cursor:pointer;color:var(--ink);}
|
| 71 |
+
.cat-btn.sel{border-color:var(--red);background:#FDF1F1;}
|
| 72 |
+
.start-btn{width:100%;padding:15px;background:var(--red);color:#fff;border:none;border-radius:8px;
|
| 73 |
+
font-size:15px;font-weight:800;cursor:pointer;margin-top:6px;}
|
| 74 |
+
|
| 75 |
+
.sprint-hud{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;
|
| 76 |
+
font-family:"SF Mono",monospace;font-weight:800;font-size:14px;}
|
| 77 |
+
.hud-timer.urgent{color:var(--red);}
|
| 78 |
+
.hud-score{color:var(--pos);}
|
| 79 |
+
.q-text{font-size:16px;font-weight:700;line-height:1.5;margin:6px 0 14px;}
|
| 80 |
+
.q-context{font-size:13.5px;color:var(--muted);line-height:1.5;margin-bottom:8px;}
|
| 81 |
+
.opt-list{display:flex;flex-direction:column;gap:9px;}
|
| 82 |
+
.opt-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px;}
|
| 83 |
+
.opt-btn{padding:13px;border:1.5px solid var(--line);border-radius:8px;background:#fff;text-align:center;
|
| 84 |
+
font-size:15px;font-weight:700;cursor:pointer;font-family:"SF Mono",monospace;}
|
| 85 |
+
.opt-btn:disabled{cursor:default;}
|
| 86 |
+
.opt-btn.right{border-color:var(--pos);background:var(--pos-bg);color:var(--pos);}
|
| 87 |
+
.opt-btn.wrong{border-color:var(--neg);background:var(--neg-bg);color:var(--neg);}
|
| 88 |
+
.q-feedback{margin-top:12px;font-size:13.5px;line-height:1.55;padding:12px;border-radius:8px;display:none;}
|
| 89 |
+
.q-feedback.show{display:block;}
|
| 90 |
+
.q-feedback.ok{background:var(--pos-bg);color:var(--pos);}
|
| 91 |
+
.q-feedback.ko{background:var(--neg-bg);color:var(--neg);}
|
| 92 |
+
.next-btn{margin-top:12px;width:100%;padding:13px;background:var(--ink);color:#fff;border:none;
|
| 93 |
+
border-radius:8px;font-size:14px;font-weight:800;cursor:pointer;display:none;}
|
| 94 |
+
.next-btn.show{display:block;}
|
| 95 |
+
|
| 96 |
+
.compare-wrap{background:var(--ink);border-radius:8px;padding:14px;overflow-x:auto;margin-bottom:6px;}
|
| 97 |
+
.compare-row{display:flex;gap:4px;margin-bottom:6px;font-family:"SF Mono",monospace;font-size:15px;flex-wrap:nowrap;}
|
| 98 |
+
.compare-row:last-child{margin-bottom:0;}
|
| 99 |
+
.compare-row .rlabel{color:#8A8780;width:44px;flex-shrink:0;font-size:11px;padding-top:4px;}
|
| 100 |
+
.blk{color:#E8E6DF;padding:2px 4px;border-radius:3px;white-space:nowrap;}
|
| 101 |
+
.blk.sep{color:#6B6864;padding:2px 0;}
|
| 102 |
+
.blk.hl{background:var(--red);color:#fff;font-weight:800;}
|
| 103 |
+
.blk-idx{color:#8A8780;font-size:9px;display:block;text-align:center;}
|
| 104 |
+
|
| 105 |
+
.grid-wrap{overflow-x:auto;margin:10px 0;}
|
| 106 |
+
.coord-grid{display:inline-grid;grid-template-columns:20px repeat(10, 26px);gap:2px;font-family:"SF Mono",monospace;font-size:10px;}
|
| 107 |
+
.g-corner{width:20px;height:20px;}
|
| 108 |
+
.g-colhead{width:26px;height:20px;display:flex;align-items:center;justify-content:center;font-weight:800;color:var(--muted);}
|
| 109 |
+
.g-rowhead{width:20px;height:26px;display:flex;align-items:center;justify-content:center;font-weight:800;color:var(--muted);}
|
| 110 |
+
.g-cell{width:26px;height:26px;background:#EFEDE7;border-radius:3px;display:flex;align-items:center;justify-content:center;
|
| 111 |
+
font-weight:800;color:transparent;font-size:11px;}
|
| 112 |
+
.g-cell.m-a{background:var(--blue);color:#fff;}
|
| 113 |
+
.g-cell.m-b{background:var(--purple);color:#fff;}
|
| 114 |
+
.g-cell.m-sol{background:var(--pos);color:#fff;}
|
| 115 |
+
|
| 116 |
+
.result-hero{text-align:center;padding:24px 10px;}
|
| 117 |
+
.result-num{font-size:52px;font-weight:800;font-family:"SF Mono",monospace;}
|
| 118 |
+
.result-label{font-size:14px;color:var(--muted);}
|
| 119 |
+
.result-best{margin-top:8px;font-size:13px;color:var(--amber);font-weight:700;}
|
| 120 |
+
|
| 121 |
+
.record-card{background:#F4F8F6;border:1px solid #CFE4D8;border-radius:8px;padding:12px 14px;margin-bottom:8px;font-size:13.5px;line-height:1.6;}
|
| 122 |
+
.record-card b{color:var(--pos);}
|
| 123 |
+
.hidden-note{background:var(--neg-bg);border:1px solid #E3C4B5;border-radius:8px;padding:12px 14px;font-size:13.5px;color:var(--neg);margin-bottom:10px;}
|
| 124 |
+
.mem-timer{text-align:center;font-family:"SF Mono",monospace;font-size:22px;font-weight:800;margin:10px 0;}
|
| 125 |
+
.mem-timer.urgent{color:var(--red);}
|
| 126 |
+
.distractor-q{background:#F2F0EA;border-radius:6px;padding:12px;margin-bottom:9px;font-size:14px;font-weight:700;}
|
| 127 |
+
|
| 128 |
+
.stat-row{display:flex;justify-content:space-between;align-items:center;padding:11px 4px;border-bottom:1px solid var(--line);font-size:14px;}
|
| 129 |
+
.stat-row:last-child{border-bottom:none;}
|
| 130 |
+
.stat-acc{font-family:"SF Mono",monospace;font-weight:800;}
|
| 131 |
+
.acc-good{color:var(--pos);} .acc-mid{color:var(--amber);} .acc-bad{color:var(--neg);} .acc-none{color:var(--muted);}
|
| 132 |
+
.progress-hero{text-align:center;padding:20px 10px 8px;}
|
| 133 |
+
.progress-num{font-size:42px;font-weight:800;font-family:"SF Mono",monospace;}
|
| 134 |
+
.progress-label{font-size:13px;color:var(--muted);margin-top:2px;}
|
| 135 |
+
.reset-link{display:block;text-align:center;margin-top:20px;font-size:13px;color:var(--muted);
|
| 136 |
+
text-decoration:underline;cursor:pointer;background:none;border:none;}
|
| 137 |
+
</style>
|
| 138 |
+
</head>
|
| 139 |
+
<body>
|
| 140 |
+
|
| 141 |
+
<header>
|
| 142 |
+
<div class="eyebrow">ICT-Eignungstest · Kognitive Fähigkeiten</div>
|
| 143 |
+
<h1>Konzentration & Merkfähigkeit</h1>
|
| 144 |
+
<p class="sub">Motor infinito: cadenas, coordenadas y registros de memoria generados al azar — la respuesta correcta siempre se construye por código.</p>
|
| 145 |
+
<div class="rail"></div>
|
| 146 |
+
</header>
|
| 147 |
+
|
| 148 |
+
<nav class="tabs">
|
| 149 |
+
<button class="tab-btn active" data-tab="teoria">Teoría</button>
|
| 150 |
+
<button class="tab-btn" data-tab="sprint">⚡ Sprint</button>
|
| 151 |
+
<button class="tab-btn" data-tab="memoria">🧠 Memoria</button>
|
| 152 |
+
<button class="tab-btn" data-tab="progreso">Progreso</button>
|
| 153 |
+
</nav>
|
| 154 |
+
|
| 155 |
+
<main>
|
| 156 |
+
|
| 157 |
+
<!-- ============ TEORÍA ============ -->
|
| 158 |
+
<section id="teoria" class="active">
|
| 159 |
+
<h2 class="section-title">¿Qué evalúa exactamente?</h2>
|
| 160 |
+
<div class="card">
|
| 161 |
+
<p>No mide inteligencia abstracta: mide <b>resistencia cognitiva bajo fatiga</b>. Tres sub-habilidades: escaneo visual de discrepancias en cadenas, lectura veloz de coordenadas, y memoria diferida (te preguntan 30-40 min después de leer los datos, sin aviso).</p>
|
| 162 |
+
</div>
|
| 163 |
+
<h2 class="section-title">Las 3 técnicas</h2>
|
| 164 |
+
<div class="card">
|
| 165 |
+
<div class="rule-box"><p><b>Chunking:</b> compara bloque contra bloque, nunca carácter por carácter linealmente. El ojo detecta diferencias de patrón grupal de forma casi instantánea; leer secuencialmente es 3-4× más lento.</p></div>
|
| 166 |
+
<div class="rule-box"><p><b>Anclaje de coordenadas:</b> memoriza los hitos del tablero (columnas A-E-J, filas 1-5-10) para saltar directo a la zona en vez de contar desde el borde. En vectores: procesa un eje a la vez, siempre en el mismo orden (primero columnas, luego filas).</p></div>
|
| 167 |
+
<div class="rule-box"><p><b>Palacio de la memoria:</b> convierte cada registro técnico en una imagen absurda. "SRV-LX-04, IP .45, Stefan Meier" → Stefan con 4 laptops Linux gritando "¡45!". Los datos puros se borran tras 30 min de interferencia; las imágenes absurdas no.</p></div>
|
| 168 |
+
</div>
|
| 169 |
+
<h2 class="section-title">La trampa del punto medio</h2>
|
| 170 |
+
<div class="card">
|
| 171 |
+
<p>Si dos celdas están a distancia <b>impar</b> en un eje (ej. fila 2 → fila 7 = 5 de diferencia), el punto medio matemático (4.5) <b>no es una celda</b> — no existe respuesta de celda única. El Sprint incluye este caso a propósito: cuando aparezca, la respuesta correcta es «No hay celda única». Reconocerlo vale puntos; forzar una respuesta los pierde.</p>
|
| 172 |
+
</div>
|
| 173 |
+
</section>
|
| 174 |
+
|
| 175 |
+
<!-- ============ SPRINT ============ -->
|
| 176 |
+
<section id="sprint">
|
| 177 |
+
<div id="sprint-area"></div>
|
| 178 |
+
</section>
|
| 179 |
+
|
| 180 |
+
<!-- ============ MEMORIA ============ -->
|
| 181 |
+
<section id="memoria">
|
| 182 |
+
<div id="memory-area"></div>
|
| 183 |
+
</section>
|
| 184 |
+
|
| 185 |
+
<!-- ============ PROGRESO ============ -->
|
| 186 |
+
<section id="progreso">
|
| 187 |
+
<div class="progress-hero">
|
| 188 |
+
<div class="progress-num" id="prog-sprint">—</div>
|
| 189 |
+
<div class="progress-label">mejor Sprint (de 10)</div>
|
| 190 |
+
</div>
|
| 191 |
+
<div class="progress-hero" style="padding-top:0;">
|
| 192 |
+
<div class="progress-num" id="prog-mem" style="font-size:34px;">—</div>
|
| 193 |
+
<div class="progress-label">mejor ronda de Memoria (de 3)</div>
|
| 194 |
+
</div>
|
| 195 |
+
<h2 class="section-title">Precisión por tipo (Sprint)</h2>
|
| 196 |
+
<div class="card" id="stats-list"></div>
|
| 197 |
+
<button class="reset-link" id="reset-btn">Reiniciar todo mi progreso</button>
|
| 198 |
+
<div style="font-size:12px;color:var(--muted);text-align:center;padding:10px;" id="storage-note"></div>
|
| 199 |
+
</section>
|
| 200 |
+
|
| 201 |
+
</main>
|
| 202 |
+
|
| 203 |
+
<script>
|
| 204 |
+
/* ============================================================
|
| 205 |
+
UTILIDADES
|
| 206 |
+
============================================================ */
|
| 207 |
+
const ri = (a,b) => a + Math.floor(Math.random()*(b-a+1));
|
| 208 |
+
const pick = arr => arr[ri(0,arr.length-1)];
|
| 209 |
+
const shuffle = arr => { const a=[...arr]; for(let i=a.length-1;i>0;i--){const j=ri(0,i);[a[i],a[j]]=[a[j],a[i]];} return a; };
|
| 210 |
+
const COLS = ["A","B","C","D","E","F","G","H","I","J"];
|
| 211 |
+
const cellName = (c,r) => COLS[c-1]+r;
|
| 212 |
+
|
| 213 |
+
/* ============================================================
|
| 214 |
+
GENERADOR: cadenas de bloques
|
| 215 |
+
============================================================ */
|
| 216 |
+
const CHARS_L = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // sin I ni O (se confunden con 1 y 0)
|
| 217 |
+
const CHARS_D = "0123456789";
|
| 218 |
+
function randBlock(len){
|
| 219 |
+
let s = "";
|
| 220 |
+
const mode = ri(0,2); // 0 letras, 1 dígitos, 2 mixto
|
| 221 |
+
for(let i=0;i<len;i++){
|
| 222 |
+
if(mode===0) s += CHARS_L[ri(0,CHARS_L.length-1)];
|
| 223 |
+
else if(mode===1) s += CHARS_D[ri(0,9)];
|
| 224 |
+
else s += (i<2 ? CHARS_L[ri(0,CHARS_L.length-1)] : CHARS_D[ri(0,9)]);
|
| 225 |
+
}
|
| 226 |
+
return s;
|
| 227 |
+
}
|
| 228 |
+
// Mutación sutil: cambia un carácter por otro visualmente parecido si es posible
|
| 229 |
+
const CONFUSABLE = {"M":"N","N":"M","B":"R","R":"B","E":"F","F":"E","C":"G","G":"C","U":"V","V":"U","K":"X","X":"K",
|
| 230 |
+
"1":"7","7":"1","3":"8","8":"3","5":"6","6":"5","0":"9","9":"0","2":"7","4":"9"};
|
| 231 |
+
function mutate(block){
|
| 232 |
+
const idx = ri(0, block.length-1);
|
| 233 |
+
const ch = block[idx];
|
| 234 |
+
let nu = CONFUSABLE[ch] || (CHARS_D.includes(ch) ? CHARS_D[(CHARS_D.indexOf(ch)+ri(1,8))%10]
|
| 235 |
+
: CHARS_L[(CHARS_L.indexOf(ch)+ri(1,20))%CHARS_L.length]);
|
| 236 |
+
return { mutated: block.slice(0,idx)+nu+block.slice(idx+1), idx, from:ch, to:nu };
|
| 237 |
+
}
|
| 238 |
+
function makeCode(nBlocks){
|
| 239 |
+
const blocks = [];
|
| 240 |
+
for(let i=0;i<nBlocks;i++) blocks.push(randBlock(pick([2,4,4,4])));
|
| 241 |
+
return blocks;
|
| 242 |
+
}
|
| 243 |
+
function renderCode(label, blocks, hlBlock=-1, showIdx=false){
|
| 244 |
+
let html = `<div class="compare-row"><span class="rlabel">${label}</span>`;
|
| 245 |
+
blocks.forEach((b,i) => {
|
| 246 |
+
html += `<span class="blk ${i===hlBlock?'hl':''}">${b}${showIdx?`<span class="blk-idx">${i+1}</span>`:""}</span>`;
|
| 247 |
+
if(i<blocks.length-1) html += `<span class="blk sep">-</span>`;
|
| 248 |
+
});
|
| 249 |
+
return html + `</div>`;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/* --- Tipo 1: ¿qué bloque difiere? --- */
|
| 253 |
+
function genBlockDiff(){
|
| 254 |
+
const n = pick([5,6,7]);
|
| 255 |
+
const a = makeCode(n);
|
| 256 |
+
const target = ri(0,n-1);
|
| 257 |
+
const b = [...a];
|
| 258 |
+
const m = mutate(a[target]);
|
| 259 |
+
b[target] = m.mutated;
|
| 260 |
+
const opts = new Set([target+1]);
|
| 261 |
+
while(opts.size<4) opts.add(ri(1,n));
|
| 262 |
+
return {
|
| 263 |
+
type:"blockdiff", typeLabel:"¿Qué bloque difiere?",
|
| 264 |
+
html:`<div class="compare-wrap">${renderCode("Fila A",a,-1,true)}${renderCode("Fila B",b,-1,true)}</div>`,
|
| 265 |
+
text:"Las dos filas difieren en exactamente un carácter. ¿En qué BLOQUE está?",
|
| 266 |
+
options:shuffle([...opts]).map(x=>`Bloque ${x}`), correct:`Bloque ${target+1}`,
|
| 267 |
+
explain:`Bloque ${target+1}: «${a[target]}» vs «${b[target]}» (el carácter ${m.idx+1} cambió de ${m.from} a ${m.to}).`,
|
| 268 |
+
grid:null
|
| 269 |
+
};
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/* --- Tipo 2: ¿idénticas o diferentes? (velocidad pura) --- */
|
| 273 |
+
function genSameDiff(){
|
| 274 |
+
const n = pick([5,6,7]);
|
| 275 |
+
const a = makeCode(n);
|
| 276 |
+
const differ = Math.random()<0.5;
|
| 277 |
+
let b = [...a], detail = "Las dos filas son idénticas, carácter por carácter.";
|
| 278 |
+
if(differ){
|
| 279 |
+
const t = ri(0,n-1);
|
| 280 |
+
const m = mutate(a[t]);
|
| 281 |
+
b[t] = m.mutated;
|
| 282 |
+
detail = `Difieren en el bloque ${t+1}: «${a[t]}» vs «${b[t]}».`;
|
| 283 |
+
}
|
| 284 |
+
return {
|
| 285 |
+
type:"samediff", typeLabel:"¿Idénticas o no?",
|
| 286 |
+
html:`<div class="compare-wrap">${renderCode("Fila A",a)}${renderCode("Fila B",b)}</div>`,
|
| 287 |
+
text:"Decide lo más rápido posible:",
|
| 288 |
+
options:["IDÉNTICAS","DIFERENTES"], correct: differ?"DIFERENTES":"IDÉNTICAS",
|
| 289 |
+
explain:detail, grid:null
|
| 290 |
+
};
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/* --- Tipo 3: vector sobre el tablero --- */
|
| 294 |
+
function genVector(){
|
| 295 |
+
const c = ri(3,8), r = ri(3,8);
|
| 296 |
+
let dc=0, dr=0;
|
| 297 |
+
while(dc===0 && dr===0){ dc=ri(-2,2); dr=ri(-2,2); }
|
| 298 |
+
const nc=c+dc, nr=r+dr;
|
| 299 |
+
const correct = cellName(nc,nr);
|
| 300 |
+
// distractores = errores reales: invertir signo de fila, intercambiar ejes
|
| 301 |
+
const opts = new Set([correct]);
|
| 302 |
+
const inv = cellName(nc, Math.min(10,Math.max(1, r-dr)));
|
| 303 |
+
opts.add(inv);
|
| 304 |
+
const swapC = Math.min(10,Math.max(1, c+dr)), swapR = Math.min(10,Math.max(1, r+dc));
|
| 305 |
+
opts.add(cellName(swapC,swapR));
|
| 306 |
+
while(opts.size<4) opts.add(cellName(Math.min(10,Math.max(1,nc+ri(-1,1))), Math.min(10,Math.max(1,nr+ri(-1,1)))));
|
| 307 |
+
const dirC = dc>0?`${dc} a la derecha`:dc<0?`${-dc} a la izquierda`:null;
|
| 308 |
+
const dirR = dr>0?`${dr} hacia abajo`:dr<0?`${-dr} hacia arriba`:null;
|
| 309 |
+
const vecTxt = [dirC,dirR].filter(Boolean).join(", ");
|
| 310 |
+
return {
|
| 311 |
+
type:"vector", typeLabel:"Vector en tablero",
|
| 312 |
+
html:null,
|
| 313 |
+
text:`Desde ${cellName(c,r)}, aplica el vector [${vecTxt}]. ¿Celda resultante?`,
|
| 314 |
+
context:"Columnas = letras (A→J, izquierda→derecha). Filas = números (1 arriba → 10 abajo).",
|
| 315 |
+
options:shuffle([...opts].slice(0,4)), correct,
|
| 316 |
+
explain:`Columna: ${COLS[c-1]}${dc!==0?` ${dc>0?"+":""}${dc} → ${COLS[nc-1]}`:" (sin cambio)"}. Fila: ${r}${dr!==0?` ${dr>0?"+":""}${dr} → ${nr}`:" (sin cambio)"}. Resultado: ${correct}.`,
|
| 317 |
+
grid:{marks:{a:{col:c,row:r}}, sol:{col:nc,row:nr}}
|
| 318 |
+
};
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
/* --- Tipo 4: punto medio (con trampa de distancia impar) --- */
|
| 322 |
+
function genMidpoint(){
|
| 323 |
+
for(let tries=0;tries<100;tries++){
|
| 324 |
+
const exact = Math.random()<0.6;
|
| 325 |
+
const c1=ri(1,10), r1=ri(1,10);
|
| 326 |
+
let dc, dr;
|
| 327 |
+
if(exact){ dc=2*pick([-2,-1,1,2]); dr=2*pick([-2,-1,1,2]); }
|
| 328 |
+
else {
|
| 329 |
+
// al menos un eje con diferencia impar
|
| 330 |
+
dc = pick([-3,-1,1,3,2,-2]); dr = pick([-3,-1,1,3]);
|
| 331 |
+
if(dc%2===0 && dr%2===0) continue;
|
| 332 |
+
}
|
| 333 |
+
const c2=c1+dc, r2=r1+dr;
|
| 334 |
+
if(c2<1||c2>10||r2<1||r2>10) continue;
|
| 335 |
+
const A=cellName(c1,r1), B=cellName(c2,r2);
|
| 336 |
+
const NOCELL = "No hay celda única";
|
| 337 |
+
if(exact){
|
| 338 |
+
const mc=(c1+c2)/2, mr=(r1+r2)/2;
|
| 339 |
+
const correct = cellName(mc,mr);
|
| 340 |
+
const opts = new Set([correct, NOCELL]);
|
| 341 |
+
while(opts.size<4) opts.add(cellName(Math.min(10,Math.max(1,mc+ri(-1,1))), Math.min(10,Math.max(1,mr+ri(-1,1)))));
|
| 342 |
+
return {
|
| 343 |
+
type:"midpoint", typeLabel:"Punto medio",
|
| 344 |
+
html:null,
|
| 345 |
+
text:`¿Cuál es el punto medio exacto entre ${A} y ${B}?`,
|
| 346 |
+
options:shuffle([...opts].slice(0,4)), correct,
|
| 347 |
+
explain:`Columnas: (${c1}+${c2})/2 = ${mc} → ${COLS[mc-1]}. Filas: (${r1}+${r2})/2 = ${mr}. Punto medio: ${correct}.`,
|
| 348 |
+
grid:{marks:{a:{col:c1,row:r1}, b:{col:c2,row:r2}}, sol:{col:mc,row:mr}}
|
| 349 |
+
};
|
| 350 |
+
} else {
|
| 351 |
+
const mcRaw=(c1+c2)/2, mrRaw=(r1+r2)/2;
|
| 352 |
+
const near1 = cellName(Math.floor(mcRaw), Math.floor(mrRaw));
|
| 353 |
+
const near2 = cellName(Math.ceil(mcRaw), Math.ceil(mrRaw));
|
| 354 |
+
const opts = new Set([NOCELL, near1, near2]);
|
| 355 |
+
while(opts.size<4) opts.add(cellName(ri(Math.max(1,Math.floor(mcRaw)-1),Math.min(10,Math.ceil(mcRaw)+1)), ri(Math.max(1,Math.floor(mrRaw)-1),Math.min(10,Math.ceil(mrRaw)+1))));
|
| 356 |
+
return {
|
| 357 |
+
type:"midpoint", typeLabel:"Punto medio",
|
| 358 |
+
html:null,
|
| 359 |
+
text:`¿Cuál es el punto medio exacto entre ${A} y ${B}?`,
|
| 360 |
+
options:shuffle([...opts].slice(0,4)), correct:NOCELL,
|
| 361 |
+
explain:`Columnas: (${c1}+${c2})/2 = ${mcRaw}. Filas: (${r1}+${r2})/2 = ${mrRaw}. Al menos un eje da un valor no entero → no existe una celda única de punto medio. Esta es la trampa de distancia impar.`,
|
| 362 |
+
grid:{marks:{a:{col:c1,row:r1}, b:{col:c2,row:r2}}, sol:null}
|
| 363 |
+
};
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
return genVector();
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
const GENERATORS = {
|
| 370 |
+
blockdiff:{fn:genBlockDiff, label:"¿Qué bloque difiere?"},
|
| 371 |
+
samediff: {fn:genSameDiff, label:"¿Idénticas o no?"},
|
| 372 |
+
vector: {fn:genVector, label:"Vector en tablero"},
|
| 373 |
+
midpoint: {fn:genMidpoint, label:"Punto medio (+trampa)"}
|
| 374 |
+
};
|
| 375 |
+
|
| 376 |
+
/* ============================================================
|
| 377 |
+
SPRINT
|
| 378 |
+
============================================================ */
|
| 379 |
+
const SPRINT_N = 10, SPRINT_SECONDS = 180;
|
| 380 |
+
let sprint=null, sprintTimer=null, selectedCats = new Set(Object.keys(GENERATORS));
|
| 381 |
+
|
| 382 |
+
function buildMiniGrid(g, revealSol){
|
| 383 |
+
let html = `<div class="grid-wrap"><div class="coord-grid"><div class="g-corner"></div>`;
|
| 384 |
+
COLS.forEach(l => html += `<div class="g-colhead">${l}</div>`);
|
| 385 |
+
for(let r=1;r<=10;r++){
|
| 386 |
+
html += `<div class="g-rowhead">${r}</div>`;
|
| 387 |
+
for(let c=1;c<=10;c++){
|
| 388 |
+
let cls="", label="";
|
| 389 |
+
if(g.marks.a && g.marks.a.col===c && g.marks.a.row===r){ cls="m-a"; label="●"; }
|
| 390 |
+
else if(g.marks.b && g.marks.b.col===c && g.marks.b.row===r){ cls="m-b"; label="●"; }
|
| 391 |
+
else if(revealSol && g.sol && g.sol.col===c && g.sol.row===r){ cls="m-sol"; label="✓"; }
|
| 392 |
+
html += `<div class="g-cell ${cls}">${label}</div>`;
|
| 393 |
+
}
|
| 394 |
+
}
|
| 395 |
+
return html + `</div></div>`;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
function renderSprintSetup(){
|
| 399 |
+
clearInterval(sprintTimer);
|
| 400 |
+
const area = document.getElementById("sprint-area");
|
| 401 |
+
area.innerHTML = `
|
| 402 |
+
<div class="card sprint-setup">
|
| 403 |
+
<h2 class="section-title" style="margin-top:0;">Sprint: ${SPRINT_N} preguntas · ${SPRINT_SECONDS/60} minutos</h2>
|
| 404 |
+
<p>Cadenas y tableros generados al azar en cada ronda — infinitos, nunca memorizables. Los distractores son los errores reales: invertir la dirección del vector, intercambiar ejes, forzar un punto medio que no existe.</p>
|
| 405 |
+
<div class="cat-grid">
|
| 406 |
+
${Object.entries(GENERATORS).map(([k,g]) => `<button class="cat-btn ${selectedCats.has(k)?'sel':''}" data-cat="${k}">${g.label}</button>`).join("")}
|
| 407 |
+
</div>
|
| 408 |
+
<button class="start-btn" id="start-sprint">▶ Empezar Sprint</button>
|
| 409 |
+
</div>`;
|
| 410 |
+
area.querySelectorAll(".cat-btn").forEach(b => b.addEventListener("click", () => {
|
| 411 |
+
const k=b.dataset.cat;
|
| 412 |
+
if(selectedCats.has(k)){ if(selectedCats.size>1) selectedCats.delete(k); } else selectedCats.add(k);
|
| 413 |
+
renderSprintSetup();
|
| 414 |
+
}));
|
| 415 |
+
area.querySelector("#start-sprint").addEventListener("click", startSprint);
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
function startSprint(){
|
| 419 |
+
const cats=[...selectedCats];
|
| 420 |
+
const qs=[];
|
| 421 |
+
for(let i=0;i<SPRINT_N;i++) qs.push(GENERATORS[cats[i%cats.length]].fn());
|
| 422 |
+
sprint = { qs:shuffle(qs), idx:0, score:0, remaining:SPRINT_SECONDS };
|
| 423 |
+
sprintTimer = setInterval(() => {
|
| 424 |
+
sprint.remaining--;
|
| 425 |
+
const el=document.getElementById("hud-timer");
|
| 426 |
+
if(el){
|
| 427 |
+
el.textContent = `${Math.floor(sprint.remaining/60)}:${(sprint.remaining%60).toString().padStart(2,"0")}`;
|
| 428 |
+
el.classList.toggle("urgent", sprint.remaining<=20);
|
| 429 |
+
}
|
| 430 |
+
if(sprint.remaining<=0) endSprint(true);
|
| 431 |
+
},1000);
|
| 432 |
+
renderQuestion();
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
function renderQuestion(){
|
| 436 |
+
const q = sprint.qs[sprint.idx];
|
| 437 |
+
const area = document.getElementById("sprint-area");
|
| 438 |
+
const gridHtml = q.grid ? buildMiniGrid(q.grid, false) : "";
|
| 439 |
+
const useGrid2col = q.options.every(o=>o.length<=18);
|
| 440 |
+
area.innerHTML = `
|
| 441 |
+
<div class="card">
|
| 442 |
+
<div class="sprint-hud">
|
| 443 |
+
<span>${sprint.idx+1} / ${SPRINT_N}</span>
|
| 444 |
+
<span class="hud-timer" id="hud-timer">${Math.floor(sprint.remaining/60)}:${(sprint.remaining%60).toString().padStart(2,"0")}</span>
|
| 445 |
+
<span class="hud-score">✓ ${sprint.score}</span>
|
| 446 |
+
</div>
|
| 447 |
+
${q.context ? `<p class="q-context">${q.context}</p>` : ""}
|
| 448 |
+
${q.html || ""}
|
| 449 |
+
${gridHtml}
|
| 450 |
+
<p class="q-text">${q.text}</p>
|
| 451 |
+
<div class="${useGrid2col?'opt-grid':'opt-list'}">
|
| 452 |
+
${q.options.map(o=>`<button class="opt-btn" data-val="${o}">${o}</button>`).join("")}
|
| 453 |
+
</div>
|
| 454 |
+
<div class="q-feedback" id="q-feedback"></div>
|
| 455 |
+
<button class="next-btn" id="next-btn">Siguiente →</button>
|
| 456 |
+
</div>`;
|
| 457 |
+
area.querySelectorAll(".opt-btn").forEach(btn => btn.addEventListener("click", () => answer(btn,q)));
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
function answer(btn,q){
|
| 461 |
+
const area=document.getElementById("sprint-area");
|
| 462 |
+
const isRight = btn.dataset.val===q.correct;
|
| 463 |
+
area.querySelectorAll(".opt-btn").forEach(b=>{
|
| 464 |
+
b.disabled=true;
|
| 465 |
+
if(b.dataset.val===q.correct) b.classList.add("right");
|
| 466 |
+
else if(b===btn && !isRight) b.classList.add("wrong");
|
| 467 |
+
});
|
| 468 |
+
if(isRight) sprint.score++;
|
| 469 |
+
if(!stats[q.type]) stats[q.type]={ok:0,total:0};
|
| 470 |
+
stats[q.type].total++; if(isRight) stats[q.type].ok++;
|
| 471 |
+
saveState();
|
| 472 |
+
// si es pregunta de tablero, revelar la celda solución en verde
|
| 473 |
+
if(q.grid && q.grid.sol){
|
| 474 |
+
const gw = area.querySelector(".grid-wrap");
|
| 475 |
+
if(gw) gw.outerHTML = buildMiniGrid(q.grid, true);
|
| 476 |
+
}
|
| 477 |
+
const fb=document.getElementById("q-feedback");
|
| 478 |
+
fb.className=`q-feedback show ${isRight?'ok':'ko'}`;
|
| 479 |
+
fb.innerHTML = `${isRight?"✓ Correcto. ":"✗ Incorrecto. "}${q.explain}`;
|
| 480 |
+
document.querySelector(".hud-score").textContent=`✓ ${sprint.score}`;
|
| 481 |
+
const nb=document.getElementById("next-btn");
|
| 482 |
+
nb.classList.add("show");
|
| 483 |
+
nb.textContent = sprint.idx+1<SPRINT_N ? "Siguiente →" : "Ver resultado";
|
| 484 |
+
nb.addEventListener("click", ()=>{ sprint.idx++; sprint.idx<SPRINT_N ? renderQuestion() : endSprint(false); });
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
function endSprint(timedOut){
|
| 488 |
+
clearInterval(sprintTimer);
|
| 489 |
+
const prev=best;
|
| 490 |
+
if(sprint.score>best){ best=sprint.score; saveState(); }
|
| 491 |
+
const area=document.getElementById("sprint-area");
|
| 492 |
+
area.innerHTML=`
|
| 493 |
+
<div class="card result-hero">
|
| 494 |
+
<div class="result-num">${sprint.score} / ${SPRINT_N}</div>
|
| 495 |
+
<div class="result-label">${timedOut?"⏱ Se acabó el tiempo":"Sprint completado"}</div>
|
| 496 |
+
${sprint.score>prev?`<div class="result-best">★ ¡Nuevo récord personal!</div>`:`<div class="result-best" style="color:var(--muted)">Mejor marca: ${best}/${SPRINT_N}</div>`}
|
| 497 |
+
<button class="start-btn" id="again">▶ Otro Sprint</button>
|
| 498 |
+
<button class="reset-link" id="back-setup">Cambiar tipos de pregunta</button>
|
| 499 |
+
</div>`;
|
| 500 |
+
area.querySelector("#again").addEventListener("click", startSprint);
|
| 501 |
+
area.querySelector("#back-setup").addEventListener("click", renderSprintSetup);
|
| 502 |
+
renderProgress();
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
/* ============================================================
|
| 506 |
+
MEMORIA DIFERIDA GENERATIVA
|
| 507 |
+
============================================================ */
|
| 508 |
+
const FIRST=["Stefan","Anna","Urs","Nadia","Beat","Laura","Marco","Petra","Reto","Silvia","Thomas","Corinne"];
|
| 509 |
+
const LAST=["Meier","Keller","Widmer","Frei","Hofer","Roos","Bühler","Steiner","Gerber","Baumann"];
|
| 510 |
+
const DEVTYPES=[
|
| 511 |
+
{pre:"SRV-LX", what:"Servidor Linux"},
|
| 512 |
+
{pre:"DB-DATA", what:"Base de datos"},
|
| 513 |
+
{pre:"SW-CORE", what:"Switch de fibra"},
|
| 514 |
+
{pre:"FW-EDGE", what:"Firewall"},
|
| 515 |
+
{pre:"RT-CORE", what:"Router"},
|
| 516 |
+
{pre:"BK-NIGHT", what:"Sistema de backup"},
|
| 517 |
+
{pre:"AP-WIFI", what:"Punto de acceso"},
|
| 518 |
+
{pre:"NAS-STOR", what:"Almacenamiento NAS"}
|
| 519 |
+
];
|
| 520 |
+
function genIP(){
|
| 521 |
+
return pick([
|
| 522 |
+
()=>`192.168.${ri(0,9)}.${ri(10,99)}`,
|
| 523 |
+
()=>`10.0.${ri(0,9)}.${ri(10,99)}`,
|
| 524 |
+
()=>`172.16.${ri(0,30)}.${ri(2,99)}`
|
| 525 |
+
])();
|
| 526 |
+
}
|
| 527 |
+
function genIssue(){
|
| 528 |
+
return pick([
|
| 529 |
+
()=>({txt:`Fallo en el puerto ${pick([8080,443,3306,22,8443,5432])}`, key:"puerto", val:null}),
|
| 530 |
+
()=>{const v=ri(85,98); return {txt:`Disco al ${v}%`, key:"pct", val:`${v}%`};},
|
| 531 |
+
()=>{const v=ri(120,480); return {txt:`Latencia de ${v} ms`, key:"lat", val:`${v} ms`};},
|
| 532 |
+
()=>{const v=ri(20,70); return {txt:`Caída de rendimiento del ${v}%`, key:"pct", val:`${v}%`};},
|
| 533 |
+
()=>{const v=ri(40,300); return {txt:`Bloqueó ${v} intentos de acceso`, key:"num", val:String(v)};}
|
| 534 |
+
])();
|
| 535 |
+
}
|
| 536 |
+
function genRecords(){
|
| 537 |
+
const devs = shuffle(DEVTYPES).slice(0,3);
|
| 538 |
+
const names = shuffle(FIRST).slice(0,3).map((f,i)=>`${f} ${shuffle(LAST)[i]}`);
|
| 539 |
+
return devs.map((d,i)=>({
|
| 540 |
+
device:`${d.pre}-${String(ri(1,99)).padStart(2,"0")}`,
|
| 541 |
+
what:d.what, ip:genIP(), issue:genIssue(), tech:names[i],
|
| 542 |
+
time:`${String(ri(7,16)).padStart(2,"0")}:${String(ri(0,59)).padStart(2,"0")}h`
|
| 543 |
+
}));
|
| 544 |
+
}
|
| 545 |
+
function genMathDistractor(){
|
| 546 |
+
return pick([
|
| 547 |
+
()=>{const a=ri(12,49),b=ri(12,49);return{q:`${a} + ${b} = ?`,a:String(a+b)};},
|
| 548 |
+
()=>{const a=ri(6,12),b=ri(6,12);return{q:`${a} × ${b} = ?`,a:String(a*b)};},
|
| 549 |
+
()=>{const p=pick([10,20,25,50]),N=ri(2,20)*20;return{q:`${p}% de ${N} = ?`,a:String(N*p/100)};},
|
| 550 |
+
()=>{const s=ri(3,9);return{q:`Serie: ${s}, ${s*2}, ${s*3}, ___`,a:String(s*4)};}
|
| 551 |
+
])();
|
| 552 |
+
}
|
| 553 |
+
function genMemQuiz(records){
|
| 554 |
+
const qs = [];
|
| 555 |
+
const r0 = records[0], r1 = records[1], r2 = records[2];
|
| 556 |
+
qs.push({
|
| 557 |
+
q:`¿Qué técnico estaba asignado a ${r0.device}?`,
|
| 558 |
+
options: shuffle(records.map(r=>r.tech)), correct:r0.tech
|
| 559 |
+
});
|
| 560 |
+
qs.push({
|
| 561 |
+
q:`¿Cuál era la IP de ${r1.device}?`,
|
| 562 |
+
options: shuffle(records.map(r=>r.ip)), correct:r1.ip
|
| 563 |
+
});
|
| 564 |
+
qs.push({
|
| 565 |
+
q:`¿Qué problema tenía ${r2.device}?`,
|
| 566 |
+
options: shuffle(records.map(r=>r.issue.txt)), correct:r2.issue.txt
|
| 567 |
+
});
|
| 568 |
+
return shuffle(qs);
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
let mem=null, memTimer=null;
|
| 572 |
+
const MEM_READ_SECONDS = 40;
|
| 573 |
+
|
| 574 |
+
function renderMemorySetup(){
|
| 575 |
+
clearInterval(memTimer);
|
| 576 |
+
const area=document.getElementById("memory-area");
|
| 577 |
+
area.innerHTML=`
|
| 578 |
+
<div class="card sprint-setup">
|
| 579 |
+
<h2 class="section-title" style="margin-top:0;">Simulación de memoria diferida</h2>
|
| 580 |
+
<p>Tres registros técnicos generados al azar (dispositivos, IPs, técnicos, incidencias — nunca los mismos). Tienes <b>${MEM_READ_SECONDS} segundos</b> para memorizarlos. Luego desaparecen de verdad, resuelves matemática de interferencia, y respondes 3 preguntas de memoria con opción múltiple.</p>
|
| 581 |
+
<button class="start-btn" id="start-mem">▶ Empezar simulación</button>
|
| 582 |
+
</div>`;
|
| 583 |
+
area.querySelector("#start-mem").addEventListener("click", startMemory);
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
function startMemory(){
|
| 587 |
+
mem = { records:genRecords(), stage:"reading", remaining:MEM_READ_SECONDS, score:0, quizIdx:0, quiz:null, distractors:[genMathDistractor(),genMathDistractor(),genMathDistractor()] };
|
| 588 |
+
memTimer = setInterval(()=>{
|
| 589 |
+
mem.remaining--;
|
| 590 |
+
const el=document.getElementById("mem-timer");
|
| 591 |
+
if(el){ el.textContent=`${mem.remaining}s`; el.classList.toggle("urgent",mem.remaining<=10); }
|
| 592 |
+
if(mem.remaining<=0) advanceToDistractor();
|
| 593 |
+
},1000);
|
| 594 |
+
renderMemory();
|
| 595 |
+
}
|
| 596 |
+
function advanceToDistractor(){
|
| 597 |
+
clearInterval(memTimer);
|
| 598 |
+
mem.stage="distractor";
|
| 599 |
+
renderMemory();
|
| 600 |
+
}
|
| 601 |
+
function renderMemory(){
|
| 602 |
+
const area=document.getElementById("memory-area");
|
| 603 |
+
if(mem.stage==="reading"){
|
| 604 |
+
area.innerHTML=`
|
| 605 |
+
<div class="card">
|
| 606 |
+
<div class="mem-timer" id="mem-timer">${mem.remaining}s</div>
|
| 607 |
+
${mem.records.map(r=>`<div class="record-card"><b>${r.time} · ${r.what} ${r.device}</b><br>IP ${r.ip}. ${r.issue.txt}. Técnico: <b>${r.tech}</b>.</div>`).join("")}
|
| 608 |
+
<button class="start-btn" id="mem-done">Ya los memoricé — continuar</button>
|
| 609 |
+
</div>`;
|
| 610 |
+
area.querySelector("#mem-done").addEventListener("click", advanceToDistractor);
|
| 611 |
+
} else if(mem.stage==="distractor"){
|
| 612 |
+
area.innerHTML=`
|
| 613 |
+
<div class="card">
|
| 614 |
+
<div class="hidden-note">Los registros ya no existen en la página. Interferencia: resuelve mentalmente antes de seguir.</div>
|
| 615 |
+
${mem.distractors.map((d,i)=>`<div class="distractor-q">${i+1}. ${d.q} <span class="mono" style="color:var(--muted);font-weight:400;"> (respuesta: ${d.a})</span></div>`).join("")}
|
| 616 |
+
<button class="start-btn" id="mem-quiz">Continuar a la ronda de memoria</button>
|
| 617 |
+
</div>`;
|
| 618 |
+
area.querySelector("#mem-quiz").addEventListener("click", ()=>{ mem.stage="quiz"; mem.quiz=genMemQuiz(mem.records); renderMemory(); });
|
| 619 |
+
} else if(mem.stage==="quiz"){
|
| 620 |
+
const q = mem.quiz[mem.quizIdx];
|
| 621 |
+
area.innerHTML=`
|
| 622 |
+
<div class="card">
|
| 623 |
+
<div class="sprint-hud"><span>Pregunta ${mem.quizIdx+1} / 3</span><span class="hud-score">✓ ${mem.score}</span></div>
|
| 624 |
+
<p class="q-text">${q.q}</p>
|
| 625 |
+
<div class="opt-list">${q.options.map(o=>`<button class="opt-btn" data-val="${o}">${o}</button>`).join("")}</div>
|
| 626 |
+
<div class="q-feedback" id="q-feedback"></div>
|
| 627 |
+
<button class="next-btn" id="mem-next">Siguiente →</button>
|
| 628 |
+
</div>`;
|
| 629 |
+
area.querySelectorAll(".opt-btn").forEach(btn=>btn.addEventListener("click",()=>{
|
| 630 |
+
const isRight = btn.dataset.val===q.correct;
|
| 631 |
+
area.querySelectorAll(".opt-btn").forEach(b=>{
|
| 632 |
+
b.disabled=true;
|
| 633 |
+
if(b.dataset.val===q.correct) b.classList.add("right");
|
| 634 |
+
else if(b===btn&&!isRight) b.classList.add("wrong");
|
| 635 |
+
});
|
| 636 |
+
if(isRight) mem.score++;
|
| 637 |
+
const fb=document.getElementById("q-feedback");
|
| 638 |
+
fb.className=`q-feedback show ${isRight?'ok':'ko'}`;
|
| 639 |
+
fb.textContent = isRight?"✓ Correcto.":`✗ Incorrecto. Era: ${q.correct}`;
|
| 640 |
+
document.querySelector(".hud-score").textContent=`✓ ${mem.score}`;
|
| 641 |
+
const nb=document.getElementById("mem-next");
|
| 642 |
+
nb.classList.add("show");
|
| 643 |
+
nb.textContent = mem.quizIdx+1<3 ? "Siguiente →" : "Ver resultado";
|
| 644 |
+
nb.addEventListener("click",()=>{
|
| 645 |
+
mem.quizIdx++;
|
| 646 |
+
if(mem.quizIdx<3) renderMemory();
|
| 647 |
+
else {
|
| 648 |
+
const prev=memBest;
|
| 649 |
+
if(mem.score>memBest){ memBest=mem.score; saveState(); }
|
| 650 |
+
area.innerHTML=`
|
| 651 |
+
<div class="card result-hero">
|
| 652 |
+
<div class="result-num">${mem.score} / 3</div>
|
| 653 |
+
<div class="result-label">memoria bajo interferencia</div>
|
| 654 |
+
${mem.score>prev?`<div class="result-best">★ ¡Nuevo récord!</div>`:`<div class="result-best" style="color:var(--muted)">Mejor marca: ${memBest}/3</div>`}
|
| 655 |
+
<button class="start-btn" id="mem-again">▶ Nueva simulación (datos nuevos)</button>
|
| 656 |
+
</div>`;
|
| 657 |
+
area.querySelector("#mem-again").addEventListener("click", startMemory);
|
| 658 |
+
renderProgress();
|
| 659 |
+
}
|
| 660 |
+
});
|
| 661 |
+
}));
|
| 662 |
+
}
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
/* ============================================================
|
| 666 |
+
PROGRESO + STORAGE
|
| 667 |
+
============================================================ */
|
| 668 |
+
let stats={}, best=0, memBest=0;
|
| 669 |
+
function accClass(p){ return p>=80?"acc-good":p>=50?"acc-mid":"acc-bad"; }
|
| 670 |
+
function renderProgress(){
|
| 671 |
+
document.getElementById("prog-sprint").textContent = best>0?`${best} / ${SPRINT_N}`:"—";
|
| 672 |
+
document.getElementById("prog-mem").textContent = memBest>0?`${memBest} / 3`:"—";
|
| 673 |
+
document.getElementById("stats-list").innerHTML = Object.entries(GENERATORS).map(([k,g])=>{
|
| 674 |
+
const s=stats[k];
|
| 675 |
+
const inner = s&&s.total>0
|
| 676 |
+
? `<span class="stat-acc ${accClass(Math.round(s.ok/s.total*100))}">${Math.round(s.ok/s.total*100)}% (${s.ok}/${s.total})</span>`
|
| 677 |
+
: `<span class="stat-acc acc-none">sin datos</span>`;
|
| 678 |
+
return `<div class="stat-row"><span>${g.label}</span>${inner}</div>`;
|
| 679 |
+
}).join("");
|
| 680 |
+
}
|
| 681 |
+
async function loadState(){
|
| 682 |
+
try{
|
| 683 |
+
const res=await window.storage.get("konzentration-merkfaehigkeit-progress");
|
| 684 |
+
if(res&&res.value){
|
| 685 |
+
const d=JSON.parse(res.value);
|
| 686 |
+
stats=d.stats||{}; best=d.best||0; memBest=d.memBest||0;
|
| 687 |
+
}
|
| 688 |
+
}catch(e){}
|
| 689 |
+
}
|
| 690 |
+
async function saveState(){
|
| 691 |
+
try{ await window.storage.set("konzentration-merkfaehigkeit-progress", JSON.stringify({stats,best,memBest})); }
|
| 692 |
+
catch(e){ const n=document.getElementById("storage-note"); if(n) n.textContent="No se pudo guardar el progreso."; }
|
| 693 |
+
}
|
| 694 |
+
document.getElementById("reset-btn").addEventListener("click", async ()=>{
|
| 695 |
+
if(!confirm("¿Reiniciar todo tu progreso?")) return;
|
| 696 |
+
stats={}; best=0; memBest=0;
|
| 697 |
+
await saveState(); renderProgress();
|
| 698 |
+
});
|
| 699 |
+
document.querySelectorAll(".tab-btn").forEach(btn=>btn.addEventListener("click",()=>{
|
| 700 |
+
document.querySelectorAll(".tab-btn").forEach(b=>b.classList.remove("active"));
|
| 701 |
+
document.querySelectorAll("main > section").forEach(s=>s.classList.remove("active"));
|
| 702 |
+
btn.classList.add("active");
|
| 703 |
+
document.getElementById(btn.dataset.tab).classList.add("active");
|
| 704 |
+
}));
|
| 705 |
+
|
| 706 |
+
(async function init(){
|
| 707 |
+
await loadState();
|
| 708 |
+
renderSprintSetup();
|
| 709 |
+
renderMemorySetup();
|
| 710 |
+
renderProgress();
|
| 711 |
+
})();
|
| 712 |
+
</script>
|
| 713 |
+
</body>
|
| 714 |
+
</html>
|
apps/mathematik-app.html
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="es">
|
| 3 |
+
<head>
|
| 4 |
+
<script>
|
| 5 |
+
/* BrainBit storage shim: sustituye window.storage (entorno artifacts de Claude.ai)
|
| 6 |
+
por localStorage con la misma interfaz async {value}. Notifica el progreso al parent. */
|
| 7 |
+
window.storage = {
|
| 8 |
+
async get(key){ const v = localStorage.getItem(key); return v === null ? null : { key, value: v }; },
|
| 9 |
+
async set(key, value){
|
| 10 |
+
localStorage.setItem(key, String(value));
|
| 11 |
+
try { if (window.parent !== window) window.parent.postMessage({ source: "brainbit-app", type: "progress", key: key, value: String(value) }, "*"); } catch(e){}
|
| 12 |
+
}
|
| 13 |
+
};
|
| 14 |
+
window.addEventListener("load", () => {
|
| 15 |
+
try {
|
| 16 |
+
if (window.parent === window) return;
|
| 17 |
+
for (let i = 0; i < localStorage.length; i++) {
|
| 18 |
+
const key = localStorage.key(i);
|
| 19 |
+
if (key && key.endsWith("-progress")) {
|
| 20 |
+
window.parent.postMessage({ source: "brainbit-app", type: "progress", key: key, value: localStorage.getItem(key) }, "*");
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
} catch(e){}
|
| 24 |
+
});
|
| 25 |
+
</script>
|
| 26 |
+
|
| 27 |
+
<meta charset="UTF-8">
|
| 28 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 29 |
+
<title>Mathematik — ICT-Eignungstest</title>
|
| 30 |
+
<style>
|
| 31 |
+
:root{
|
| 32 |
+
--bg:#F7F6F3; --panel:#FFFFFF; --ink:#1C1C1C; --muted:#6B6864; --line:#D8D5CC;
|
| 33 |
+
--red:#D8232A; --pos:#1F7A4C; --pos-bg:#E6F1EA; --neg:#A63A22; --neg-bg:#F6E9E3;
|
| 34 |
+
--amber:#B8860B; --purple:#7A2A63; --purple-bg:#F0E3EE; --blue:#2E6DA4;
|
| 35 |
+
}
|
| 36 |
+
*{box-sizing:border-box;}
|
| 37 |
+
html,body{margin:0;padding:0;background:var(--bg);color:var(--ink);
|
| 38 |
+
font-family:"Helvetica Neue",Arial,sans-serif;-webkit-font-smoothing:antialiased;}
|
| 39 |
+
.mono{font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;}
|
| 40 |
+
|
| 41 |
+
header{background:var(--ink);color:#fff;padding:28px 20px 22px;position:relative;overflow:hidden;}
|
| 42 |
+
header .rail{position:absolute;left:0;right:0;bottom:0;height:4px;
|
| 43 |
+
background:repeating-linear-gradient(90deg,var(--red) 0 18px, transparent 18px 30px);opacity:.9;}
|
| 44 |
+
.eyebrow{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:#C9C6BD;font-weight:700;margin-bottom:8px;}
|
| 45 |
+
h1{font-size:26px;margin:0 0 4px;font-weight:800;letter-spacing:-0.01em;}
|
| 46 |
+
.sub{font-size:14px;color:#C9C6BD;margin:0;max-width:560px;line-height:1.4;}
|
| 47 |
+
|
| 48 |
+
nav.tabs{display:flex;background:var(--panel);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20;}
|
| 49 |
+
nav.tabs button{flex:1;padding:14px 4px;font-size:12px;font-weight:700;background:none;border:none;
|
| 50 |
+
color:var(--muted);cursor:pointer;border-bottom:3px solid transparent;}
|
| 51 |
+
nav.tabs button.active{color:var(--ink);border-bottom-color:var(--red);}
|
| 52 |
+
|
| 53 |
+
main{max-width:760px;margin:0 auto;padding:20px 16px 60px;}
|
| 54 |
+
section{display:none;}
|
| 55 |
+
section.active{display:block;}
|
| 56 |
+
|
| 57 |
+
h2.section-title{font-size:19px;font-weight:800;margin:26px 0 10px;}
|
| 58 |
+
h2.section-title:first-child{margin-top:4px;}
|
| 59 |
+
h3.sub-title{font-size:15px;font-weight:800;margin:18px 0 8px;color:var(--muted);text-transform:uppercase;letter-spacing:.03em;}
|
| 60 |
+
|
| 61 |
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;margin-bottom:14px;}
|
| 62 |
+
.card p{line-height:1.55;font-size:14.5px;margin:0 0 10px;}
|
| 63 |
+
.card p:last-child{margin-bottom:0;}
|
| 64 |
+
.rule-box{background:#FBF3E4;border:1px solid #E9D9B4;border-radius:8px;padding:14px 16px;margin:10px 0;}
|
| 65 |
+
.rule-box b{color:var(--amber);}
|
| 66 |
+
.fix-box{background:var(--purple-bg);border:1px solid #DCC0D6;border-radius:8px;padding:14px 16px;margin:10px 0;}
|
| 67 |
+
.fix-box b{color:var(--purple);}
|
| 68 |
+
|
| 69 |
+
table.ref{width:100%;border-collapse:collapse;font-size:13.5px;margin:8px 0;}
|
| 70 |
+
table.ref th{text-align:left;padding:7px 8px;background:#F2F0EA;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);}
|
| 71 |
+
table.ref td{padding:7px 8px;border-bottom:1px solid var(--line);font-family:"SF Mono",monospace;font-size:13px;}
|
| 72 |
+
table.ref tr:last-child td{border-bottom:none;}
|
| 73 |
+
|
| 74 |
+
/* ===== Sprint ===== */
|
| 75 |
+
.sprint-setup{text-align:center;padding:10px 0 4px;}
|
| 76 |
+
.sprint-setup p{color:var(--muted);font-size:14px;line-height:1.5;}
|
| 77 |
+
.cat-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:14px 0;}
|
| 78 |
+
.cat-btn{padding:13px 8px;border:1.5px solid var(--line);border-radius:8px;background:#fff;
|
| 79 |
+
font-size:13px;font-weight:700;cursor:pointer;color:var(--ink);}
|
| 80 |
+
.cat-btn.sel{border-color:var(--red);background:#FDF1F1;}
|
| 81 |
+
.start-btn{width:100%;padding:15px;background:var(--red);color:#fff;border:none;border-radius:8px;
|
| 82 |
+
font-size:15px;font-weight:800;cursor:pointer;margin-top:6px;}
|
| 83 |
+
|
| 84 |
+
.sprint-hud{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;
|
| 85 |
+
font-family:"SF Mono",monospace;font-weight:800;font-size:14px;}
|
| 86 |
+
.hud-timer.urgent{color:var(--red);}
|
| 87 |
+
.hud-score{color:var(--pos);}
|
| 88 |
+
.q-text{font-size:16.5px;font-weight:700;line-height:1.5;margin:6px 0 16px;}
|
| 89 |
+
.q-context{font-size:13.5px;color:var(--muted);line-height:1.5;margin-bottom:8px;}
|
| 90 |
+
.opt-list{display:flex;flex-direction:column;gap:9px;}
|
| 91 |
+
.opt-btn{padding:14px;border:1.5px solid var(--line);border-radius:8px;background:#fff;text-align:left;
|
| 92 |
+
font-size:15px;font-weight:700;cursor:pointer;font-family:"SF Mono",monospace;}
|
| 93 |
+
.opt-btn:disabled{cursor:default;}
|
| 94 |
+
.opt-btn.right{border-color:var(--pos);background:var(--pos-bg);color:var(--pos);}
|
| 95 |
+
.opt-btn.wrong{border-color:var(--neg);background:var(--neg-bg);color:var(--neg);}
|
| 96 |
+
.q-feedback{margin-top:12px;font-size:13.5px;line-height:1.55;padding:12px;border-radius:8px;display:none;}
|
| 97 |
+
.q-feedback.show{display:block;}
|
| 98 |
+
.q-feedback.ok{background:var(--pos-bg);color:var(--pos);}
|
| 99 |
+
.q-feedback.ko{background:var(--neg-bg);color:var(--neg);}
|
| 100 |
+
.next-btn{margin-top:12px;width:100%;padding:13px;background:var(--ink);color:#fff;border:none;
|
| 101 |
+
border-radius:8px;font-size:14px;font-weight:800;cursor:pointer;display:none;}
|
| 102 |
+
.next-btn.show{display:block;}
|
| 103 |
+
|
| 104 |
+
.result-hero{text-align:center;padding:24px 10px;}
|
| 105 |
+
.result-num{font-size:52px;font-weight:800;font-family:"SF Mono",monospace;}
|
| 106 |
+
.result-label{font-size:14px;color:var(--muted);}
|
| 107 |
+
.result-best{margin-top:8px;font-size:13px;color:var(--amber);font-weight:700;}
|
| 108 |
+
|
| 109 |
+
/* ===== Entrenamiento curado ===== */
|
| 110 |
+
.ex-card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:14px;}
|
| 111 |
+
.ex-head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:8px;}
|
| 112 |
+
.ex-num{font-size:11px;font-weight:800;letter-spacing:.06em;color:var(--muted);text-transform:uppercase;}
|
| 113 |
+
.level-tag{font-size:10px;font-weight:800;padding:3px 8px;border-radius:20px;text-transform:uppercase;}
|
| 114 |
+
.level-medio{background:#FBF3E4;color:var(--amber);}
|
| 115 |
+
.level-avanzado{background:var(--neg-bg);color:var(--neg);}
|
| 116 |
+
.level-extremo{background:var(--purple-bg);color:var(--purple);}
|
| 117 |
+
.ex-desc{font-size:14px;line-height:1.5;margin:6px 0 12px;}
|
| 118 |
+
.ex-question{font-size:14px;font-weight:700;margin:10px 0 12px;padding:9px 12px;
|
| 119 |
+
background:#F2F0EA;border-left:3px solid var(--red);border-radius:4px;}
|
| 120 |
+
.ex-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px;align-items:center;}
|
| 121 |
+
button.btn{border:none;border-radius:6px;padding:9px 14px;font-size:13px;font-weight:700;cursor:pointer;}
|
| 122 |
+
.btn-primary{background:var(--ink);color:#fff;}
|
| 123 |
+
.btn-timer{background:#fff;color:var(--ink);border:1px solid var(--line);}
|
| 124 |
+
.btn-timer.running{background:var(--red);color:#fff;border-color:var(--red);}
|
| 125 |
+
.btn-master{background:#fff;color:var(--muted);border:1px solid var(--line);}
|
| 126 |
+
.btn-master.done{background:var(--pos-bg);color:var(--pos);border-color:var(--pos);}
|
| 127 |
+
.timer-display{font-family:"SF Mono",monospace;font-weight:800;font-size:14px;color:var(--muted);}
|
| 128 |
+
.timer-display.urgent{color:var(--red);}
|
| 129 |
+
.solution{margin-top:12px;padding:14px;background:#F4F8F6;border:1px solid #CFE4D8;
|
| 130 |
+
border-radius:8px;font-size:14px;line-height:1.6;display:none;}
|
| 131 |
+
.solution.open{display:block;}
|
| 132 |
+
.solution b{color:var(--pos);}
|
| 133 |
+
.solution .step{margin:0 0 6px;padding-left:12px;border-left:2px solid var(--pos-bg);}
|
| 134 |
+
|
| 135 |
+
/* ===== Progreso ===== */
|
| 136 |
+
.progress-hero{text-align:center;padding:20px 10px 8px;}
|
| 137 |
+
.progress-num{font-size:46px;font-weight:800;font-family:"SF Mono",monospace;}
|
| 138 |
+
.progress-label{font-size:13px;color:var(--muted);margin-top:2px;}
|
| 139 |
+
.bar-track{height:8px;background:var(--line);border-radius:6px;overflow:hidden;margin:16px 0 22px;}
|
| 140 |
+
.bar-fill{height:100%;background:var(--red);transition:width .3s ease;}
|
| 141 |
+
.stat-row{display:flex;justify-content:space-between;align-items:center;padding:11px 4px;border-bottom:1px solid var(--line);font-size:14px;}
|
| 142 |
+
.stat-row:last-child{border-bottom:none;}
|
| 143 |
+
.stat-acc{font-family:"SF Mono",monospace;font-weight:800;}
|
| 144 |
+
.acc-good{color:var(--pos);} .acc-mid{color:var(--amber);} .acc-bad{color:var(--neg);} .acc-none{color:var(--muted);}
|
| 145 |
+
.reset-link{display:block;text-align:center;margin-top:20px;font-size:13px;color:var(--muted);
|
| 146 |
+
text-decoration:underline;cursor:pointer;background:none;border:none;}
|
| 147 |
+
</style>
|
| 148 |
+
</head>
|
| 149 |
+
<body>
|
| 150 |
+
|
| 151 |
+
<header>
|
| 152 |
+
<div class="eyebrow">ICT-Eignungstest · Schulwissen</div>
|
| 153 |
+
<h1>Mathematik</h1>
|
| 154 |
+
<p class="sub">Motor de ejercicios infinitos: cada pregunta se genera al azar y su respuesta se calcula por código — nunca se repiten, nunca tienen errores.</p>
|
| 155 |
+
<div class="rail"></div>
|
| 156 |
+
</header>
|
| 157 |
+
|
| 158 |
+
<nav class="tabs">
|
| 159 |
+
<button class="tab-btn active" data-tab="teoria">Teoría</button>
|
| 160 |
+
<button class="tab-btn" data-tab="sprint">⚡ Sprint</button>
|
| 161 |
+
<button class="tab-btn" data-tab="entreno">Entrenamiento</button>
|
| 162 |
+
<button class="tab-btn" data-tab="progreso">Progreso</button>
|
| 163 |
+
</nav>
|
| 164 |
+
|
| 165 |
+
<main>
|
| 166 |
+
|
| 167 |
+
<!-- ================= TEORÍA ================= -->
|
| 168 |
+
<section id="teoria" class="active">
|
| 169 |
+
<h2 class="section-title">¿Qué evalúa exactamente?</h2>
|
| 170 |
+
<div class="card">
|
| 171 |
+
<p>Tres bloques: <b>Kopfrechnen</b> (cálculo mental estricto, sin papel ni calculadora), <b>Proportionalität</b> (reglas de tres directas e inversas), y <b>Prozentrechnen</b> (descuentos encadenados, IVA suizo 8.1%, variaciones porcentuales). No mide matemática avanzada: mide <b>velocidad y precisión bajo presión</b>.</p>
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
+
<h2 class="section-title">Tabla de referencia: fracción ↔ decimal ↔ %</h2>
|
| 175 |
+
<div class="card">
|
| 176 |
+
<p>Memorízala hasta que sea automática — es la herramienta nº1 para comparar fracciones en segundos:</p>
|
| 177 |
+
<table class="ref">
|
| 178 |
+
<tr><th>Fracción</th><th>Decimal</th><th>%</th><th>Fracción</th><th>Decimal</th><th>%</th></tr>
|
| 179 |
+
<tr><td>1/2</td><td>0.5</td><td>50%</td><td>1/8</td><td>0.125</td><td>12.5%</td></tr>
|
| 180 |
+
<tr><td>1/3</td><td>0.333</td><td>33.3%</td><td>3/8</td><td>0.375</td><td>37.5%</td></tr>
|
| 181 |
+
<tr><td>2/3</td><td>0.667</td><td>66.7%</td><td>5/8</td><td>0.625</td><td>62.5%</td></tr>
|
| 182 |
+
<tr><td>1/4</td><td>0.25</td><td>25%</td><td>7/8</td><td>0.875</td><td>87.5%</td></tr>
|
| 183 |
+
<tr><td>3/4</td><td>0.75</td><td>75%</td><td>1/16</td><td>0.0625</td><td>6.25%</td></tr>
|
| 184 |
+
<tr><td>1/5</td><td>0.2</td><td>20%</td><td>11/16</td><td>0.6875</td><td>68.75%</td></tr>
|
| 185 |
+
<tr><td>1/6</td><td>0.167</td><td>16.7%</td><td>1/12</td><td>0.083</td><td>8.3%</td></tr>
|
| 186 |
+
</table>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<h2 class="section-title">Las 5 técnicas de examen</h2>
|
| 190 |
+
<div class="card">
|
| 191 |
+
<div class="rule-box"><p><b>1 · Descomposición de porcentajes (10%+5%):</b> 15% de 240 → 10%=24, 5%=12 → 36. Otros ladrillos: 25%=un cuarto, 50%=mitad, 1%=÷100. Construye cualquier porcentaje sumando ladrillos: 35% = 25% + 10%.</p></div>
|
| 192 |
+
<div class="rule-box"><p><b>2 · Estimación por opción más cercana:</b> cuando hay opciones en pantalla, la respuesta correcta es <b>la opción más cercana al valor exacto</b>, no la que produce tu redondeo. Redondea solo UNO de los factores para minimizar el error (49×19.95 → 49×20 = 980, casi exacto) y luego compara distancias contra las opciones. Redondear los dos factores a la vez acumula error en la misma dirección.</p></div>
|
| 193 |
+
<div class="rule-box"><p><b>3 · Simplificación cruzada:</b> en 4/7 × 14/8, cancela antes de multiplicar: 4 con 8 (→1/2), 14 con 7 (→2/1). Resultado 1, sin números grandes.</p></div>
|
| 194 |
+
<div class="rule-box"><p><b>4 · Proporcionalidad — decide el tipo ANTES de calcular:</b> pregúntate "si sube X, ¿el resultado sube o baja?". Más tiempo → más solicitudes procesadas = DIRECTA (multiplica). Menos días de plazo → más gente necesaria = INVERSA (usa días-hombre: personas × días = carga constante).</p></div>
|
| 195 |
+
<div class="rule-box"><p><b>5 · Redondeo suizo:</b> los precios finales en CHF se redondean a 0.05 (5 céntimos). 350.244 → 350.25. En código: round(x/0.05)×0.05.</p></div>
|
| 196 |
+
</div>
|
| 197 |
+
|
| 198 |
+
<h2 class="section-title">Reglas de divisibilidad (para simplificar fracciones al instante)</h2>
|
| 199 |
+
<div class="card">
|
| 200 |
+
<table class="ref">
|
| 201 |
+
<tr><th>÷</th><th>Regla</th><th>Ejemplo</th></tr>
|
| 202 |
+
<tr><td>2</td><td>termina en par</td><td>438 ✓</td></tr>
|
| 203 |
+
<tr><td>3</td><td>suma de dígitos ÷3</td><td>438 → 4+3+8=15 ✓</td></tr>
|
| 204 |
+
<tr><td>4</td><td>últimos 2 dígitos ÷4</td><td>316 → 16 ✓</td></tr>
|
| 205 |
+
<tr><td>5</td><td>termina en 0 o 5</td><td>485 ✓</td></tr>
|
| 206 |
+
<tr><td>6</td><td>cumple ÷2 y ÷3</td><td>438 ✓</td></tr>
|
| 207 |
+
<tr><td>9</td><td>suma de dígitos ÷9</td><td>738 → 18 ✓</td></tr>
|
| 208 |
+
</table>
|
| 209 |
+
</div>
|
| 210 |
+
|
| 211 |
+
<h2 class="section-title">Unidades de datos (la trampa más cara del examen)</h2>
|
| 212 |
+
<div class="card">
|
| 213 |
+
<p><b>Bits vs Bytes:</b> la velocidad de red se mide en <b>Mbps (megaBITS/s)</b>, el tamaño de archivos en <b>MB (megaBYTES)</b>. 1 Byte = 8 bits, así que <b>1 MB = 8 Mb</b>. Todo cálculo de descarga/subida requiere primero convertir MB→Mb multiplicando por 8. Convención del examen: 1 GB = 1000 MB.</p>
|
| 214 |
+
<p><b>Secuencia fija de 3 pasos:</b> ① tamaño → MB, ② MB × 8 = Mb, ③ Mb ÷ Mbps = segundos → convierte a min:seg.</p>
|
| 215 |
+
</div>
|
| 216 |
+
</section>
|
| 217 |
+
|
| 218 |
+
<!-- ================= SPRINT ================= -->
|
| 219 |
+
<section id="sprint">
|
| 220 |
+
<div id="sprint-area"></div>
|
| 221 |
+
</section>
|
| 222 |
+
|
| 223 |
+
<!-- ================= ENTRENAMIENTO ================= -->
|
| 224 |
+
<section id="entreno">
|
| 225 |
+
<div class="card" style="background:#F2F0EA;border-style:dashed;">
|
| 226 |
+
<p style="font-size:13px;color:var(--muted);">Ejercicios curados de referencia con solución explicada paso a paso. Para volumen y velocidad, usa el <b>Sprint</b> — genera preguntas infinitas.</p>
|
| 227 |
+
</div>
|
| 228 |
+
<h3 class="sub-title">A · Kopfrechnen</h3>
|
| 229 |
+
<div id="mental-exercises"></div>
|
| 230 |
+
<h3 class="sub-title">B · Proportionalität</h3>
|
| 231 |
+
<div id="proportion-exercises"></div>
|
| 232 |
+
<h3 class="sub-title">C · Prozentrechnen</h3>
|
| 233 |
+
<div id="percent-exercises"></div>
|
| 234 |
+
<h3 class="sub-title">D · Datenmengen</h3>
|
| 235 |
+
<div id="data-exercises"></div>
|
| 236 |
+
</section>
|
| 237 |
+
|
| 238 |
+
<!-- ================= PROGRESO ================= -->
|
| 239 |
+
<section id="progreso">
|
| 240 |
+
<div class="progress-hero">
|
| 241 |
+
<div class="progress-num" id="prog-count">—</div>
|
| 242 |
+
<div class="progress-label">mejor puntuación en Sprint (de 10)</div>
|
| 243 |
+
</div>
|
| 244 |
+
<h2 class="section-title">Precisión por tipo de pregunta (Sprint)</h2>
|
| 245 |
+
<div class="card" id="stats-list"></div>
|
| 246 |
+
<h2 class="section-title">Entrenamiento curado</h2>
|
| 247 |
+
<div class="card" id="prog-list"></div>
|
| 248 |
+
<button class="reset-link" id="reset-btn">Reiniciar todo mi progreso</button>
|
| 249 |
+
<div style="font-size:12px;color:var(--muted);text-align:center;padding:10px;" id="storage-note"></div>
|
| 250 |
+
</section>
|
| 251 |
+
|
| 252 |
+
</main>
|
| 253 |
+
|
| 254 |
+
<script>
|
| 255 |
+
/* ============================================================
|
| 256 |
+
GENERADORES — cada pregunta se construye al azar y su
|
| 257 |
+
respuesta correcta se CALCULA. Cero respuestas escritas a mano.
|
| 258 |
+
============================================================ */
|
| 259 |
+
const ri = (a,b) => a + Math.floor(Math.random()*(b-a+1));
|
| 260 |
+
const pick = arr => arr[ri(0,arr.length-1)];
|
| 261 |
+
const shuffle = arr => { const a=[...arr]; for(let i=a.length-1;i>0;i--){const j=ri(0,i);[a[i],a[j]]=[a[j],a[i]];} return a; };
|
| 262 |
+
const chf = v => (Math.round(v/0.05)*0.05).toFixed(2);
|
| 263 |
+
const fmtMinSec = s => `${Math.floor(s/60)} min ${s%60} s`;
|
| 264 |
+
|
| 265 |
+
/* --- 1. Porcentaje mental --- */
|
| 266 |
+
function genPercent(){
|
| 267 |
+
const p = pick([5,10,15,20,25,30,35,40,45,50,60,75]);
|
| 268 |
+
const N = ri(3,48)*20;
|
| 269 |
+
const ans = N*p/100;
|
| 270 |
+
let d1 = N*(p+5)/100, d2 = p>5 ? N*(p-5)/100 : N*(p+10)/100;
|
| 271 |
+
return {
|
| 272 |
+
type:"percent", typeLabel:"Porcentajes",
|
| 273 |
+
text:`¿Cuánto es el ${p}% de ${N}?`,
|
| 274 |
+
context:"Sin calculadora — usa la descomposición 10% / 5% / 25%.",
|
| 275 |
+
options:[ans,d1,d2].map(String), correct:String(ans),
|
| 276 |
+
explain:`10% de ${N} = ${N/10}. Construye: ${p}% = ${ans}.`
|
| 277 |
+
};
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
/* --- 2. Comparación de fracciones --- */
|
| 281 |
+
const FRACS = [[1,2],[3,5],[5,8],[2,3],[7,10],[3,4],[4,5],[5,6],[7,8],[11,16],[9,16],[13,20],[17,20],[5,12],[7,12],[11,12],[2,5],[3,8]];
|
| 282 |
+
function genFraction(){
|
| 283 |
+
let sel;
|
| 284 |
+
do { sel = shuffle(FRACS).slice(0,3); }
|
| 285 |
+
while (new Set(sel.map(f=>f[0]/f[1])).size < 3);
|
| 286 |
+
const wantMax = Math.random() < 0.5;
|
| 287 |
+
const vals = sel.map(f=>f[0]/f[1]);
|
| 288 |
+
const target = wantMax ? Math.max(...vals) : Math.min(...vals);
|
| 289 |
+
const correct = sel[vals.indexOf(target)];
|
| 290 |
+
return {
|
| 291 |
+
type:"fraction", typeLabel:"Fracciones",
|
| 292 |
+
text:`¿Cuál de estas fracciones es la ${wantMax ? "MAYOR" : "MENOR"}?`,
|
| 293 |
+
context:"Convierte a decimal con la tabla de referencia de la Teoría.",
|
| 294 |
+
options: sel.map(f=>`${f[0]}/${f[1]}`), correct:`${correct[0]}/${correct[1]}`,
|
| 295 |
+
explain:`En decimal: ${sel.map(f=>`${f[0]}/${f[1]} = ${(f[0]/f[1]).toFixed(4).replace(/0+$/,"")}`).join(" · ")}.`
|
| 296 |
+
};
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
/* --- 3. Estimación por opción más cercana --- */
|
| 300 |
+
function genEstimate(){
|
| 301 |
+
for(let tries=0; tries<50; tries++){
|
| 302 |
+
const a = ri(17,59);
|
| 303 |
+
const b = pick([3.1,4.95,5.9,7.9,9.9,12.1,19.95,24.9,29.9]);
|
| 304 |
+
const exact = a*b;
|
| 305 |
+
const base = Math.round(exact/10)*10;
|
| 306 |
+
const opts = [base-20, base, base+20].filter(o=>o>0);
|
| 307 |
+
if(opts.length<3) continue;
|
| 308 |
+
const dists = opts.map(o=>Math.abs(o-exact));
|
| 309 |
+
const minD = Math.min(...dists);
|
| 310 |
+
if(dists.filter(d=>Math.abs(d-minD)<0.6).length > 1) continue; // evitar empates
|
| 311 |
+
const correct = opts[dists.indexOf(minD)];
|
| 312 |
+
return {
|
| 313 |
+
type:"estimate", typeLabel:"Estimación",
|
| 314 |
+
text:`Estima: ${a} × ${b} ≈ ? Elige la opción MÁS CERCANA.`,
|
| 315 |
+
context:"Redondea solo UNO de los factores para minimizar el error.",
|
| 316 |
+
options: opts.map(String), correct:String(correct),
|
| 317 |
+
explain:`Valor exacto: ${a} × ${b} = ${exact.toFixed(2)}. La opción más cercana es ${correct} (diferencia ${Math.abs(correct-exact).toFixed(2)}).`
|
| 318 |
+
};
|
| 319 |
+
}
|
| 320 |
+
return genPercent();
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
/* --- 4. Proporcionalidad inversa (días-hombre) --- */
|
| 324 |
+
function genInverse(){
|
| 325 |
+
for(let tries=0; tries<80; tries++){
|
| 326 |
+
const w = ri(2,6), d = pick([6,8,9,10,12]);
|
| 327 |
+
const total = w*d;
|
| 328 |
+
const divs = [];
|
| 329 |
+
for(let x=2;x<d;x++) if(total%x===0) divs.push(x);
|
| 330 |
+
if(!divs.length) continue;
|
| 331 |
+
const d2 = pick(divs);
|
| 332 |
+
const need = total/d2, add = need-w;
|
| 333 |
+
if(add<1) continue;
|
| 334 |
+
const opts = new Set([add, need, add+1]);
|
| 335 |
+
if(add>1) opts.add(add-1);
|
| 336 |
+
return {
|
| 337 |
+
type:"inverse", typeLabel:"Prop. inversa",
|
| 338 |
+
text:`¿Cuántas personas ADICIONALES hay que incorporar?`,
|
| 339 |
+
context:`${w} desarrolladores tardan ${d} días en completar un módulo a ritmo constante. El nuevo plazo es de ${d2} días.`,
|
| 340 |
+
options: shuffle([...opts]).slice(0,4).map(String), correct:String(add),
|
| 341 |
+
explain:`Carga: ${w}×${d} = ${total} días-persona. Necesarios: ${total}÷${d2} = ${need}. Adicionales: ${need}−${w} = ${add}. (La trampa clásica es responder ${need}.)`
|
| 342 |
+
};
|
| 343 |
+
}
|
| 344 |
+
return genPercent();
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
/* --- 5. Proporcionalidad directa (tasas) --- */
|
| 348 |
+
function genDirect(){
|
| 349 |
+
const t = pick([4,5,6,8]), k = ri(4,12)*10;
|
| 350 |
+
const N = t*k;
|
| 351 |
+
let t2; do { t2 = pick([10,12,15,20]); } while(t2===t);
|
| 352 |
+
const ans = k*t2;
|
| 353 |
+
const opts = new Set([ans, k*t, ans+k, Math.max(k, ans-k)]);
|
| 354 |
+
return {
|
| 355 |
+
type:"direct", typeLabel:"Prop. directa",
|
| 356 |
+
text:`¿Cuántas solicitudes procesa en ${t2} segundos?`,
|
| 357 |
+
context:`Un servidor procesa ${N} solicitudes cada ${t} segundos, a ritmo constante.`,
|
| 358 |
+
options: shuffle([...opts]).slice(0,4).map(String), correct:String(ans),
|
| 359 |
+
explain:`Tasa: ${N}÷${t} = ${k}/s. En ${t2}s: ${k}×${t2} = ${ans}.`
|
| 360 |
+
};
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
/* --- 6. Descuentos encadenados + IVA suizo --- */
|
| 364 |
+
function genChained(){
|
| 365 |
+
for(let tries=0; tries<50; tries++){
|
| 366 |
+
const base = pick([200,250,300,350,400,450,500,600]);
|
| 367 |
+
const d1 = pick([10,15,20,25]), d2 = pick([5,10,15]);
|
| 368 |
+
const correct = chf(base*(1-d1/100)*(1-d2/100)*1.081);
|
| 369 |
+
const trapSum = chf(base*(1-(d1+d2)/100)*1.081); // sumar descuentos (error clásico)
|
| 370 |
+
const noVat = chf(base*(1-d1/100)*(1-d2/100)); // olvidar IVA
|
| 371 |
+
if(correct===trapSum || correct===noVat || trapSum===noVat) continue;
|
| 372 |
+
return {
|
| 373 |
+
type:"chained", typeLabel:"Descuentos+IVA",
|
| 374 |
+
text:`¿Precio final con IVA incluido? (CHF)`,
|
| 375 |
+
context:`Precio base CHF ${base}. Descuento del ${d1}%, luego ${d2}% adicional sobre el precio ya rebajado. Al final se suma el 8.1% de IVA (MWST). Redondeo suizo a 0.05.`,
|
| 376 |
+
options: shuffle([correct,trapSum,noVat]), correct,
|
| 377 |
+
explain:`${base}×${(1-d1/100).toFixed(2)} = ${(base*(1-d1/100)).toFixed(2)} → ×${(1-d2/100).toFixed(2)} = ${(base*(1-d1/100)*(1-d2/100)).toFixed(2)} → ×1.081 = ${correct}. Trampa: sumar ${d1}+${d2}=${d1+d2}% de golpe da ${trapSum} (incorrecto).`
|
| 378 |
+
};
|
| 379 |
+
}
|
| 380 |
+
return genPercent();
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
/* --- 7. Transferencia de datos --- */
|
| 384 |
+
function genData(){
|
| 385 |
+
for(let tries=0; tries<80; tries++){
|
| 386 |
+
const mbps = pick([10,20,25,40,50,80]);
|
| 387 |
+
const s = ri(9,60)*10; // 90..600 s
|
| 388 |
+
const mbits = mbps*s;
|
| 389 |
+
if(mbits % 8 !== 0) continue;
|
| 390 |
+
const MB = mbits/8;
|
| 391 |
+
if(MB < 100 || MB > 6000) continue;
|
| 392 |
+
const wrongNoX8 = Math.round(MB/mbps); // olvidó ×8
|
| 393 |
+
if(wrongNoX8===s) continue;
|
| 394 |
+
const opts = new Set([fmtMinSec(s), fmtMinSec(wrongNoX8), fmtMinSec(s+30), fmtMinSec(Math.max(30,s-30))]);
|
| 395 |
+
const sizeTxt = MB%1000===0 ? `${MB/1000} GB (=${MB} MB)` : `${MB} MB`;
|
| 396 |
+
return {
|
| 397 |
+
type:"data", typeLabel:"Datos/Red",
|
| 398 |
+
text:`¿Cuánto tarda la descarga?`,
|
| 399 |
+
context:`Archivo de ${sizeTxt}. Velocidad: ${mbps} Mbps. (1 MB = 8 Mb)`,
|
| 400 |
+
options: shuffle([...opts]).slice(0,4), correct:fmtMinSec(s),
|
| 401 |
+
explain:`${MB} MB × 8 = ${mbits} Mb → ${mbits} ÷ ${mbps} = ${s} s = ${fmtMinSec(s)}. Trampa: sin ×8 sale ${fmtMinSec(wrongNoX8)} (incorrecto).`
|
| 402 |
+
};
|
| 403 |
+
}
|
| 404 |
+
return genPercent();
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
const GENERATORS = {
|
| 408 |
+
percent: {fn:genPercent, label:"Porcentajes"},
|
| 409 |
+
fraction: {fn:genFraction, label:"Fracciones"},
|
| 410 |
+
estimate: {fn:genEstimate, label:"Estimación"},
|
| 411 |
+
inverse: {fn:genInverse, label:"Prop. inversa"},
|
| 412 |
+
direct: {fn:genDirect, label:"Prop. directa"},
|
| 413 |
+
chained: {fn:genChained, label:"Descuentos+IVA"},
|
| 414 |
+
data: {fn:genData, label:"Datos/Red"}
|
| 415 |
+
};
|
| 416 |
+
|
| 417 |
+
/* ============================================================
|
| 418 |
+
SPRINT — 10 preguntas, 4 minutos, puntuación y estadísticas
|
| 419 |
+
============================================================ */
|
| 420 |
+
const SPRINT_N = 10, SPRINT_SECONDS = 240;
|
| 421 |
+
let sprint = null, sprintTimer = null, selectedCats = new Set(Object.keys(GENERATORS));
|
| 422 |
+
|
| 423 |
+
function renderSprintSetup(){
|
| 424 |
+
clearInterval(sprintTimer);
|
| 425 |
+
const area = document.getElementById("sprint-area");
|
| 426 |
+
area.innerHTML = `
|
| 427 |
+
<div class="card sprint-setup">
|
| 428 |
+
<h2 class="section-title" style="margin-top:0;">Sprint: ${SPRINT_N} preguntas · ${SPRINT_SECONDS/60} minutos</h2>
|
| 429 |
+
<p>Cada pregunta se genera al azar con respuesta calculada por código. Los distractores son los errores clásicos reales (sumar descuentos, olvidar ×8, confundir directa/inversa). Elige qué tipos entrenar:</p>
|
| 430 |
+
<div class="cat-grid">
|
| 431 |
+
${Object.entries(GENERATORS).map(([k,g]) => `
|
| 432 |
+
<button class="cat-btn ${selectedCats.has(k)?'sel':''}" data-cat="${k}">${g.label}</button>
|
| 433 |
+
`).join("")}
|
| 434 |
+
</div>
|
| 435 |
+
<button class="start-btn" id="start-sprint">▶ Empezar Sprint</button>
|
| 436 |
+
</div>`;
|
| 437 |
+
area.querySelectorAll(".cat-btn").forEach(b => b.addEventListener("click", () => {
|
| 438 |
+
const k = b.dataset.cat;
|
| 439 |
+
if(selectedCats.has(k)){ if(selectedCats.size>1) selectedCats.delete(k); }
|
| 440 |
+
else selectedCats.add(k);
|
| 441 |
+
renderSprintSetup();
|
| 442 |
+
}));
|
| 443 |
+
area.querySelector("#start-sprint").addEventListener("click", startSprint);
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
function startSprint(){
|
| 447 |
+
const cats = [...selectedCats];
|
| 448 |
+
const qs = [];
|
| 449 |
+
for(let i=0;i<SPRINT_N;i++) qs.push(GENERATORS[cats[i%cats.length]].fn());
|
| 450 |
+
sprint = { qs: shuffle(qs), idx:0, score:0, remaining:SPRINT_SECONDS };
|
| 451 |
+
sprintTimer = setInterval(() => {
|
| 452 |
+
sprint.remaining--;
|
| 453 |
+
const el = document.getElementById("hud-timer");
|
| 454 |
+
if(el){
|
| 455 |
+
el.textContent = `${Math.floor(sprint.remaining/60)}:${(sprint.remaining%60).toString().padStart(2,"0")}`;
|
| 456 |
+
el.classList.toggle("urgent", sprint.remaining<=30);
|
| 457 |
+
}
|
| 458 |
+
if(sprint.remaining<=0) endSprint(true);
|
| 459 |
+
}, 1000);
|
| 460 |
+
renderQuestion();
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
function renderQuestion(){
|
| 464 |
+
const q = sprint.qs[sprint.idx];
|
| 465 |
+
const area = document.getElementById("sprint-area");
|
| 466 |
+
area.innerHTML = `
|
| 467 |
+
<div class="card">
|
| 468 |
+
<div class="sprint-hud">
|
| 469 |
+
<span>${sprint.idx+1} / ${SPRINT_N}</span>
|
| 470 |
+
<span class="hud-timer" id="hud-timer">${Math.floor(sprint.remaining/60)}:${(sprint.remaining%60).toString().padStart(2,"0")}</span>
|
| 471 |
+
<span class="hud-score">✓ ${sprint.score}</span>
|
| 472 |
+
</div>
|
| 473 |
+
${q.context ? `<p class="q-context">${q.context}</p>` : ""}
|
| 474 |
+
<p class="q-text">${q.text}</p>
|
| 475 |
+
<div class="opt-list">
|
| 476 |
+
${shuffle(q.options).map(o => `<button class="opt-btn" data-val="${o}">${o}</button>`).join("")}
|
| 477 |
+
</div>
|
| 478 |
+
<div class="q-feedback" id="q-feedback"></div>
|
| 479 |
+
<button class="next-btn" id="next-btn">Siguiente →</button>
|
| 480 |
+
</div>`;
|
| 481 |
+
area.querySelectorAll(".opt-btn").forEach(btn => btn.addEventListener("click", () => answer(btn, q)));
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
function answer(btn, q){
|
| 485 |
+
const area = document.getElementById("sprint-area");
|
| 486 |
+
const isRight = btn.dataset.val === q.correct;
|
| 487 |
+
area.querySelectorAll(".opt-btn").forEach(b => {
|
| 488 |
+
b.disabled = true;
|
| 489 |
+
if(b.dataset.val === q.correct) b.classList.add("right");
|
| 490 |
+
else if(b===btn && !isRight) b.classList.add("wrong");
|
| 491 |
+
});
|
| 492 |
+
if(isRight) sprint.score++;
|
| 493 |
+
if(!stats[q.type]) stats[q.type] = {ok:0,total:0};
|
| 494 |
+
stats[q.type].total++; if(isRight) stats[q.type].ok++;
|
| 495 |
+
saveState();
|
| 496 |
+
const fb = document.getElementById("q-feedback");
|
| 497 |
+
fb.className = `q-feedback show ${isRight?'ok':'ko'}`;
|
| 498 |
+
fb.innerHTML = `${isRight ? "✓ Correcto. " : "✗ Incorrecto. "}${q.explain}`;
|
| 499 |
+
document.querySelector(".hud-score").textContent = `✓ ${sprint.score}`;
|
| 500 |
+
const nb = document.getElementById("next-btn");
|
| 501 |
+
nb.classList.add("show");
|
| 502 |
+
nb.textContent = sprint.idx+1 < SPRINT_N ? "Siguiente →" : "Ver resultado";
|
| 503 |
+
nb.addEventListener("click", () => {
|
| 504 |
+
sprint.idx++;
|
| 505 |
+
if(sprint.idx < SPRINT_N) renderQuestion(); else endSprint(false);
|
| 506 |
+
});
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
function endSprint(timedOut){
|
| 510 |
+
clearInterval(sprintTimer);
|
| 511 |
+
const prev = best;
|
| 512 |
+
if(sprint.score > best){ best = sprint.score; saveState(); }
|
| 513 |
+
const area = document.getElementById("sprint-area");
|
| 514 |
+
area.innerHTML = `
|
| 515 |
+
<div class="card result-hero">
|
| 516 |
+
<div class="result-num">${sprint.score} / ${SPRINT_N}</div>
|
| 517 |
+
<div class="result-label">${timedOut ? "⏱ Se acabó el tiempo" : "Sprint completado"}</div>
|
| 518 |
+
${sprint.score > prev ? `<div class="result-best">★ ¡Nuevo récord personal!</div>` : `<div class="result-best" style="color:var(--muted)">Mejor marca: ${best}/${SPRINT_N}</div>`}
|
| 519 |
+
<button class="start-btn" id="again">▶ Otro Sprint</button>
|
| 520 |
+
<button class="reset-link" id="back-setup">Cambiar tipos de pregunta</button>
|
| 521 |
+
</div>`;
|
| 522 |
+
area.querySelector("#again").addEventListener("click", startSprint);
|
| 523 |
+
area.querySelector("#back-setup").addEventListener("click", renderSprintSetup);
|
| 524 |
+
renderProgress();
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
/* ============================================================
|
| 528 |
+
ENTRENAMIENTO CURADO (soluciones verificadas paso a paso)
|
| 529 |
+
============================================================ */
|
| 530 |
+
const mentalExercises = [
|
| 531 |
+
{ id:"m1", level:"medio", seconds:20,
|
| 532 |
+
question:"Ordena de menor a mayor: Servidor A = 5/8, Servidor B = 11/16, Servidor C = 3/4",
|
| 533 |
+
solution:"<div class='step'>A=5/8=0.625 · B=11/16=0.6875 · C=3/4=0.75</div><b>A (5/8) < B (11/16) < C (3/4)</b>" },
|
| 534 |
+
{ id:"m2", level:"medio", seconds:10,
|
| 535 |
+
question:"Estima sin calculadora: 49 licencias a CHF 19.95/mes. Si las opciones en pantalla son 970, 990, 1000 — ¿cuál es la MÁS CERCANA?",
|
| 536 |
+
solution:"<div class='step'>Valor exacto: 49 × 19.95 = 977.55 CHF.</div><div class='step'>970 → dif. 7.55 · 990 → dif. 12.45 · 1000 → dif. 22.45.</div><b>Respuesta: 970</b>. Técnica: redondea solo UNO de los números (49×20 = 980, casi exacto)." },
|
| 537 |
+
{ id:"m3", level:"medio", seconds:8,
|
| 538 |
+
question:"Estima sin calculadora: 498 × 3.1 ≈ ?",
|
| 539 |
+
solution:"<div class='step'>Redondea: 498→500, 3.1→3 → 1500.</div><b>≈1500</b> (exacto: 1543.8)." },
|
| 540 |
+
{ id:"m4", level:"avanzado", seconds:15,
|
| 541 |
+
question:"Truco 10%+5%: ¿cuánto es el 15% de 340?",
|
| 542 |
+
solution:"<div class='step'>10% = 34 · 5% = 17.</div><b>34 + 17 = 51</b>" },
|
| 543 |
+
{ id:"m5", level:"avanzado", seconds:15,
|
| 544 |
+
question:"Truco 25%+10%: ¿cuánto es el 35% de 480?",
|
| 545 |
+
solution:"<div class='step'>25% = 120 · 10% = 48.</div><b>120 + 48 = 168</b>" }
|
| 546 |
+
];
|
| 547 |
+
const proportionExercises = [
|
| 548 |
+
{ id:"p1", level:"avanzado",
|
| 549 |
+
desc:"3 ingenieros tardan 12 días en desarrollar un módulo. El cliente exige que esté listo en 4 días.",
|
| 550 |
+
question:"¿Cuántos ingenieros adicionales hay que incorporar?",
|
| 551 |
+
solution:"<div class='step'>Carga: 3 × 12 = 36 días-persona.</div><div class='step'>Necesarios: 36 ÷ 4 = 9.</div><b>Adicionales: 9 − 3 = 6</b>",
|
| 552 |
+
note:"INVERSA: menos días → más gente. Trampa clásica: responder 9 en vez de los adicionales." },
|
| 553 |
+
{ id:"p2", level:"avanzado",
|
| 554 |
+
desc:"5 técnicos tardan 8 días en migrar una base de datos. Nuevo plazo: 2 días.",
|
| 555 |
+
question:"¿Cuántos técnicos adicionales?",
|
| 556 |
+
solution:"<div class='step'>Carga: 5 × 8 = 40. Necesarios: 40 ÷ 2 = 20.</div><b>Adicionales: 20 − 5 = 15</b>" },
|
| 557 |
+
{ id:"p3", level:"medio",
|
| 558 |
+
desc:"Un servidor procesa 480 solicitudes cada 6 segundos.",
|
| 559 |
+
question:"¿Cuántas procesa en 15 segundos?",
|
| 560 |
+
solution:"<div class='step'>Tasa: 480 ÷ 6 = 80/s.</div><b>80 × 15 = 1200</b>",
|
| 561 |
+
note:"DIRECTA: más tiempo → más solicitudes. Decide el tipo antes de calcular." }
|
| 562 |
+
];
|
| 563 |
+
const percentExercises = [
|
| 564 |
+
{ id:"c1", level:"extremo",
|
| 565 |
+
desc:"Servidor cloud: CHF 450/mes. Descuento 20%, luego 10% adicional sobre el rebajado, más 8.1% IVA.",
|
| 566 |
+
question:"¿Coste final con IVA?",
|
| 567 |
+
solution:"<div class='step'>450 × 0.80 = 360 → × 0.90 = 324 → × 1.081 = 350.244.</div><b>CHF 350.25</b> (redondeo suizo a 0.05)" },
|
| 568 |
+
{ id:"c2", level:"extremo",
|
| 569 |
+
desc:"Licencia: CHF 300. Descuento 25%, luego 15% adicional, más 8.1% IVA.",
|
| 570 |
+
question:"¿Coste final con IVA?",
|
| 571 |
+
solution:"<div class='step'>300 × 0.75 = 225 → × 0.85 = 191.25 → × 1.081 ≈ 206.74.</div><b>CHF 206.75</b>" }
|
| 572 |
+
];
|
| 573 |
+
const dataExercises = [
|
| 574 |
+
{ id:"d1", level:"extremo",
|
| 575 |
+
desc:"Backup de 1.5 GB. Velocidad: 40 Mbps. (1 GB = 1000 MB, 1 MB = 8 Mb)",
|
| 576 |
+
question:"¿Tiempo exacto en min y seg?",
|
| 577 |
+
solution:"<div class='step'>1.5 GB = 1500 MB × 8 = 12000 Mb ÷ 40 = 300 s.</div><b>5 min 0 s</b>" },
|
| 578 |
+
{ id:"d2", level:"extremo",
|
| 579 |
+
desc:"Subida de 800 MB a 20 Mbps.",
|
| 580 |
+
question:"¿Tiempo exacto en min y seg?",
|
| 581 |
+
solution:"<div class='step'>800 × 8 = 6400 Mb ÷ 20 = 320 s.</div><b>5 min 20 s</b>" }
|
| 582 |
+
];
|
| 583 |
+
|
| 584 |
+
let mastered = {}, stats = {}, best = 0, timers = {};
|
| 585 |
+
const allIds = [...mentalExercises, ...proportionExercises, ...percentExercises, ...dataExercises].map(e=>e.id);
|
| 586 |
+
|
| 587 |
+
function renderTimedGroup(containerId, list, hasTimer){
|
| 588 |
+
const container = document.getElementById(containerId);
|
| 589 |
+
container.innerHTML = list.map((ex,i) => `
|
| 590 |
+
<div class="ex-card">
|
| 591 |
+
<div class="ex-head">
|
| 592 |
+
<span class="ex-num">${i+1} / ${list.length}</span>
|
| 593 |
+
<span class="level-tag level-${ex.level}">${ex.level}</span>
|
| 594 |
+
</div>
|
| 595 |
+
${ex.desc ? `<p class="ex-desc">${ex.desc}</p>` : ""}
|
| 596 |
+
<p class="ex-question">${ex.question}</p>
|
| 597 |
+
<div class="ex-actions">
|
| 598 |
+
<button class="btn btn-primary reveal-btn" data-id="${ex.id}">Revelar solución</button>
|
| 599 |
+
${hasTimer ? `<button class="btn btn-timer timer-btn" data-id="${ex.id}" data-seconds="${ex.seconds}">⏱ ${ex.seconds}s</button><span class="timer-display" id="timer-${ex.id}"></span>` : ""}
|
| 600 |
+
<button class="btn btn-master ${mastered[ex.id]?'done':''}" data-id="${ex.id}">${mastered[ex.id]?'✓ Dominado':'Marcar dominado'}</button>
|
| 601 |
+
</div>
|
| 602 |
+
<div class="solution" id="sol-${ex.id}">${ex.solution}${ex.note ? `<p style="margin-top:8px;color:var(--muted);font-size:13px">${ex.note}</p>` : ""}</div>
|
| 603 |
+
</div>`).join("");
|
| 604 |
+
container.querySelectorAll(".reveal-btn").forEach(btn => btn.addEventListener("click", () => {
|
| 605 |
+
const sol = document.getElementById(`sol-${btn.dataset.id}`);
|
| 606 |
+
sol.classList.toggle("open");
|
| 607 |
+
btn.textContent = sol.classList.contains("open") ? "Ocultar solución" : "Revelar solución";
|
| 608 |
+
}));
|
| 609 |
+
container.querySelectorAll(".timer-btn").forEach(btn =>
|
| 610 |
+
btn.addEventListener("click", () => startCuratedTimer(btn.dataset.id, parseInt(btn.dataset.seconds))));
|
| 611 |
+
container.querySelectorAll(".btn-master").forEach(btn =>
|
| 612 |
+
btn.addEventListener("click", async () => { mastered[btn.dataset.id]=!mastered[btn.dataset.id]; await saveState(); renderCurated(); renderProgress(); }));
|
| 613 |
+
}
|
| 614 |
+
function startCuratedTimer(id, seconds){
|
| 615 |
+
if(timers[id]){ clearInterval(timers[id]); timers[id]=null; }
|
| 616 |
+
let remaining = seconds;
|
| 617 |
+
const display = document.getElementById(`timer-${id}`);
|
| 618 |
+
const btn = document.querySelector(`.timer-btn[data-id="${id}"]`);
|
| 619 |
+
btn.classList.add("running"); btn.textContent = "Detener";
|
| 620 |
+
display.textContent = `0:${seconds.toString().padStart(2,"0")}`;
|
| 621 |
+
timers[id] = setInterval(() => {
|
| 622 |
+
remaining--;
|
| 623 |
+
display.textContent = `0:${Math.max(remaining,0).toString().padStart(2,"0")}`;
|
| 624 |
+
display.classList.toggle("urgent", remaining<=3);
|
| 625 |
+
if(remaining<=0){ clearInterval(timers[id]); timers[id]=null; display.textContent="¡Tiempo!"; btn.classList.remove("running"); btn.textContent=`⏱ ${seconds}s`; }
|
| 626 |
+
},1000);
|
| 627 |
+
}
|
| 628 |
+
function renderCurated(){
|
| 629 |
+
renderTimedGroup("mental-exercises", mentalExercises, true);
|
| 630 |
+
renderTimedGroup("proportion-exercises", proportionExercises, false);
|
| 631 |
+
renderTimedGroup("percent-exercises", percentExercises, false);
|
| 632 |
+
renderTimedGroup("data-exercises", dataExercises, false);
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
/* ============================================================
|
| 636 |
+
PROGRESO + STORAGE
|
| 637 |
+
============================================================ */
|
| 638 |
+
function accClass(pct){ return pct>=80?"acc-good":pct>=50?"acc-mid":"acc-bad"; }
|
| 639 |
+
function renderProgress(){
|
| 640 |
+
document.getElementById("prog-count").textContent = best>0 ? `${best} / ${SPRINT_N}` : "—";
|
| 641 |
+
document.getElementById("stats-list").innerHTML = Object.entries(GENERATORS).map(([k,g]) => {
|
| 642 |
+
const s = stats[k];
|
| 643 |
+
const inner = s && s.total>0
|
| 644 |
+
? `<span class="stat-acc ${accClass(Math.round(s.ok/s.total*100))}">${Math.round(s.ok/s.total*100)}% (${s.ok}/${s.total})</span>`
|
| 645 |
+
: `<span class="stat-acc acc-none">sin datos</span>`;
|
| 646 |
+
return `<div class="stat-row"><span>${g.label}</span>${inner}</div>`;
|
| 647 |
+
}).join("");
|
| 648 |
+
const all = [...mentalExercises, ...proportionExercises, ...percentExercises, ...dataExercises];
|
| 649 |
+
document.getElementById("prog-list").innerHTML = all.map(ex => `
|
| 650 |
+
<div class="stat-row"><span>${ex.question.slice(0,48)}${ex.question.length>48?"…":""}</span>
|
| 651 |
+
<span class="stat-acc ${mastered[ex.id]?'acc-good':'acc-none'}">${mastered[ex.id]?"✓":"—"}</span></div>`).join("");
|
| 652 |
+
}
|
| 653 |
+
async function loadState(){
|
| 654 |
+
try{
|
| 655 |
+
const res = await window.storage.get("mathematik-progress");
|
| 656 |
+
if(res && res.value){
|
| 657 |
+
const d = JSON.parse(res.value);
|
| 658 |
+
mastered = d.mastered||{}; stats = d.stats||{}; best = d.best||0;
|
| 659 |
+
}
|
| 660 |
+
}catch(e){}
|
| 661 |
+
}
|
| 662 |
+
async function saveState(){
|
| 663 |
+
try{ await window.storage.set("mathematik-progress", JSON.stringify({mastered, stats, best})); }
|
| 664 |
+
catch(e){ const n=document.getElementById("storage-note"); if(n) n.textContent="No se pudo guardar el progreso."; }
|
| 665 |
+
}
|
| 666 |
+
document.getElementById("reset-btn").addEventListener("click", async () => {
|
| 667 |
+
if(!confirm("¿Reiniciar todo tu progreso (récord, estadísticas y dominados)?")) return;
|
| 668 |
+
mastered={}; stats={}; best=0;
|
| 669 |
+
await saveState(); renderCurated(); renderProgress();
|
| 670 |
+
});
|
| 671 |
+
document.querySelectorAll(".tab-btn").forEach(btn => btn.addEventListener("click", () => {
|
| 672 |
+
document.querySelectorAll(".tab-btn").forEach(b=>b.classList.remove("active"));
|
| 673 |
+
document.querySelectorAll("main > section").forEach(s=>s.classList.remove("active"));
|
| 674 |
+
btn.classList.add("active");
|
| 675 |
+
document.getElementById(btn.dataset.tab).classList.add("active");
|
| 676 |
+
}));
|
| 677 |
+
|
| 678 |
+
(async function init(){
|
| 679 |
+
await loadState();
|
| 680 |
+
renderSprintSetup();
|
| 681 |
+
renderCurated();
|
| 682 |
+
renderProgress();
|
| 683 |
+
})();
|
| 684 |
+
</script>
|
| 685 |
+
</body>
|
| 686 |
+
</html>
|
apps/vernetztes-denken-app.html
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="es">
|
| 3 |
+
<head>
|
| 4 |
+
<script>
|
| 5 |
+
/* BrainBit storage shim: sustituye window.storage (entorno artifacts de Claude.ai)
|
| 6 |
+
por localStorage con la misma interfaz async {value}. Notifica el progreso al parent. */
|
| 7 |
+
window.storage = {
|
| 8 |
+
async get(key){ const v = localStorage.getItem(key); return v === null ? null : { key, value: v }; },
|
| 9 |
+
async set(key, value){
|
| 10 |
+
localStorage.setItem(key, String(value));
|
| 11 |
+
try { if (window.parent !== window) window.parent.postMessage({ source: "brainbit-app", type: "progress", key: key, value: String(value) }, "*"); } catch(e){}
|
| 12 |
+
}
|
| 13 |
+
};
|
| 14 |
+
window.addEventListener("load", () => {
|
| 15 |
+
try {
|
| 16 |
+
if (window.parent === window) return;
|
| 17 |
+
for (let i = 0; i < localStorage.length; i++) {
|
| 18 |
+
const key = localStorage.key(i);
|
| 19 |
+
if (key && key.endsWith("-progress")) {
|
| 20 |
+
window.parent.postMessage({ source: "brainbit-app", type: "progress", key: key, value: localStorage.getItem(key) }, "*");
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
} catch(e){}
|
| 24 |
+
});
|
| 25 |
+
</script>
|
| 26 |
+
|
| 27 |
+
<meta charset="UTF-8">
|
| 28 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 29 |
+
<title>Vernetztes Denken — ICT-Eignungstest</title>
|
| 30 |
+
<style>
|
| 31 |
+
:root{
|
| 32 |
+
--bg:#F7F6F3;
|
| 33 |
+
--panel:#FFFFFF;
|
| 34 |
+
--ink:#1C1C1C;
|
| 35 |
+
--muted:#6B6864;
|
| 36 |
+
--line:#D8D5CC;
|
| 37 |
+
--red:#D8232A;
|
| 38 |
+
--red-ink:#A3181D;
|
| 39 |
+
--pos:#1F7A4C;
|
| 40 |
+
--pos-bg:#E6F1EA;
|
| 41 |
+
--neg:#A63A22;
|
| 42 |
+
--neg-bg:#F6E9E3;
|
| 43 |
+
--amber:#B8860B;
|
| 44 |
+
}
|
| 45 |
+
*{box-sizing:border-box;}
|
| 46 |
+
html,body{margin:0;padding:0;background:var(--bg);color:var(--ink);
|
| 47 |
+
font-family:"Helvetica Neue",Arial,sans-serif;-webkit-font-smoothing:antialiased;}
|
| 48 |
+
.mono{font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;}
|
| 49 |
+
|
| 50 |
+
/* ---------- HEADER ---------- */
|
| 51 |
+
header{background:var(--ink);color:#fff;padding:28px 20px 22px;position:relative;overflow:hidden;}
|
| 52 |
+
header .rail{position:absolute;left:0;right:0;bottom:0;height:4px;background:repeating-linear-gradient(90deg,var(--red) 0 18px, transparent 18px 30px);opacity:.9;}
|
| 53 |
+
.eyebrow{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:#C9C6BD;font-weight:700;margin-bottom:8px;}
|
| 54 |
+
h1{font-size:26px;margin:0 0 4px;font-weight:800;letter-spacing:-0.01em;}
|
| 55 |
+
.sub{font-size:14px;color:#C9C6BD;margin:0;max-width:560px;line-height:1.4;}
|
| 56 |
+
|
| 57 |
+
/* ---------- TABS ---------- */
|
| 58 |
+
nav.tabs{display:flex;background:var(--panel);border-bottom:1px solid var(--line);
|
| 59 |
+
position:sticky;top:0;z-index:20;}
|
| 60 |
+
nav.tabs button{flex:1;padding:14px 8px;font-size:13px;font-weight:700;letter-spacing:.02em;
|
| 61 |
+
background:none;border:none;color:var(--muted);cursor:pointer;border-bottom:3px solid transparent;}
|
| 62 |
+
nav.tabs button.active{color:var(--ink);border-bottom-color:var(--red);}
|
| 63 |
+
|
| 64 |
+
main{max-width:720px;margin:0 auto;padding:20px 16px 60px;}
|
| 65 |
+
section{display:none;}
|
| 66 |
+
section.active{display:block;}
|
| 67 |
+
|
| 68 |
+
h2.section-title{font-size:19px;font-weight:800;margin:26px 0 10px;padding-top:4px;}
|
| 69 |
+
h2.section-title:first-child{margin-top:4px;}
|
| 70 |
+
|
| 71 |
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;margin-bottom:14px;}
|
| 72 |
+
.card p{line-height:1.55;font-size:14.5px;margin:0 0 10px;}
|
| 73 |
+
.card p:last-child{margin-bottom:0;}
|
| 74 |
+
.rule-box{background:#FBF3E4;border:1px solid #E9D9B4;border-radius:8px;padding:14px 16px;margin:10px 0;}
|
| 75 |
+
.rule-box b{color:var(--amber);}
|
| 76 |
+
|
| 77 |
+
/* ---------- Signal line (signature element) ---------- */
|
| 78 |
+
.line-wrap{display:flex;align-items:center;overflow-x:auto;padding:18px 4px 10px;gap:0;}
|
| 79 |
+
.station{display:flex;flex-direction:column;align-items:center;min-width:52px;flex-shrink:0;}
|
| 80 |
+
.station .dot{width:14px;height:14px;border-radius:50%;background:var(--ink);border:2px solid var(--ink);}
|
| 81 |
+
.station .lbl{margin-top:6px;font-size:12px;font-weight:800;font-family:"SF Mono",ui-monospace,monospace;}
|
| 82 |
+
.track{flex:1;min-width:36px;height:2px;background:var(--line);position:relative;}
|
| 83 |
+
.signal{position:absolute;top:-11px;left:50%;transform:translateX(-50%);
|
| 84 |
+
font-family:"SF Mono",ui-monospace,monospace;font-size:12px;font-weight:800;
|
| 85 |
+
padding:1px 6px;border-radius:4px;white-space:nowrap;}
|
| 86 |
+
.signal.pos{background:var(--pos-bg);color:var(--pos);}
|
| 87 |
+
.signal.neg{background:var(--neg-bg);color:var(--neg);}
|
| 88 |
+
|
| 89 |
+
/* ---------- Exercise card ---------- */
|
| 90 |
+
.ex-card{background:var(--panel);border:1px solid var(--line);border-radius:10px;
|
| 91 |
+
padding:16px;margin-bottom:14px;}
|
| 92 |
+
.ex-head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:8px;}
|
| 93 |
+
.ex-num{font-size:11px;font-weight:800;letter-spacing:.06em;color:var(--muted);text-transform:uppercase;}
|
| 94 |
+
.level-tag{font-size:10px;font-weight:800;padding:3px 8px;border-radius:20px;letter-spacing:.03em;text-transform:uppercase;}
|
| 95 |
+
.level-basico{background:#E6F1EA;color:var(--pos);}
|
| 96 |
+
.level-intermedio{background:#FBF3E4;color:var(--amber);}
|
| 97 |
+
.level-avanzado{background:var(--neg-bg);color:var(--neg);}
|
| 98 |
+
.level-extremo{background:#F0E3EE;color:#7A2A63;}
|
| 99 |
+
.subchain-label{font-size:12px;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.03em;margin:14px 0 -6px;}
|
| 100 |
+
.indeterminate-box{background:#F0E3EE;border:1px solid #DCC0D6;border-radius:8px;padding:10px 12px;margin-top:10px;font-size:13px;}
|
| 101 |
+
.indeterminate-box b{color:#7A2A63;}
|
| 102 |
+
.ex-context{font-size:14px;line-height:1.55;margin:6px 0 10px;}
|
| 103 |
+
.ex-question{font-size:14.5px;font-weight:700;margin:8px 0 12px;padding:10px 12px;
|
| 104 |
+
background:#F2F0EA;border-left:3px solid var(--red);border-radius:4px;}
|
| 105 |
+
.ex-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px;}
|
| 106 |
+
button.btn{border:none;border-radius:6px;padding:9px 14px;font-size:13px;font-weight:700;cursor:pointer;}
|
| 107 |
+
.btn-primary{background:var(--ink);color:#fff;}
|
| 108 |
+
.btn-primary:hover{background:#000;}
|
| 109 |
+
.btn-timer{background:#fff;color:var(--ink);border:1px solid var(--line);}
|
| 110 |
+
.btn-timer.running{background:var(--red);color:#fff;border-color:var(--red);}
|
| 111 |
+
.btn-master{background:#fff;color:var(--muted);border:1px solid var(--line);}
|
| 112 |
+
.btn-master.done{background:var(--pos-bg);color:var(--pos);border-color:var(--pos);}
|
| 113 |
+
.timer-display{font-family:"SF Mono",ui-monospace,monospace;font-weight:800;font-size:14px;
|
| 114 |
+
align-self:center;color:var(--muted);}
|
| 115 |
+
.timer-display.urgent{color:var(--red);}
|
| 116 |
+
|
| 117 |
+
.solution{margin-top:12px;padding:14px;background:#F4F8F6;border:1px solid #CFE4D8;
|
| 118 |
+
border-radius:8px;font-size:14px;line-height:1.6;display:none;}
|
| 119 |
+
.solution.open{display:block;}
|
| 120 |
+
.solution b{color:var(--pos);}
|
| 121 |
+
|
| 122 |
+
/* ---------- Progress tab ---------- */
|
| 123 |
+
.progress-hero{text-align:center;padding:20px 10px 8px;}
|
| 124 |
+
.progress-num{font-size:46px;font-weight:800;font-family:"SF Mono",ui-monospace,monospace;}
|
| 125 |
+
.progress-label{font-size:13px;color:var(--muted);margin-top:2px;}
|
| 126 |
+
.bar-track{height:8px;background:var(--line);border-radius:6px;overflow:hidden;margin:16px 0 22px;}
|
| 127 |
+
.bar-fill{height:100%;background:var(--red);transition:width .3s ease;}
|
| 128 |
+
.prog-row{display:flex;justify-content:space-between;align-items:center;padding:11px 4px;
|
| 129 |
+
border-bottom:1px solid var(--line);font-size:14px;}
|
| 130 |
+
.prog-row:last-child{border-bottom:none;}
|
| 131 |
+
.prog-check{width:18px;height:18px;border-radius:50%;flex-shrink:0;}
|
| 132 |
+
.prog-check.on{background:var(--pos);}
|
| 133 |
+
.prog-check.off{background:var(--line);}
|
| 134 |
+
.reset-link{display:block;text-align:center;margin-top:20px;font-size:13px;color:var(--muted);
|
| 135 |
+
text-decoration:underline;cursor:pointer;background:none;border:none;}
|
| 136 |
+
|
| 137 |
+
.checklist-item{display:flex;gap:10px;align-items:flex-start;padding:9px 0;font-size:14px;line-height:1.5;}
|
| 138 |
+
.checklist-item input{margin-top:3px;}
|
| 139 |
+
|
| 140 |
+
.loading-note{font-size:12px;color:var(--muted);text-align:center;padding:10px;}
|
| 141 |
+
</style>
|
| 142 |
+
</head>
|
| 143 |
+
<body>
|
| 144 |
+
|
| 145 |
+
<header>
|
| 146 |
+
<div class="eyebrow">ICT-Eignungstest · ICT-Grundwissen</div>
|
| 147 |
+
<h1>Vernetztes Denken</h1>
|
| 148 |
+
<p class="sub">Pensamiento interconectado — teoría suiza de sistemas aplicada a desarrollo de software. Diagramas tipo línea de tren: cada estación es una variable, cada tramo un efecto (+/-).</p>
|
| 149 |
+
<div class="rail"></div>
|
| 150 |
+
</header>
|
| 151 |
+
|
| 152 |
+
<nav class="tabs">
|
| 153 |
+
<button class="tab-btn active" data-tab="teoria">Teoría</button>
|
| 154 |
+
<button class="tab-btn" data-tab="practicar">Practicar</button>
|
| 155 |
+
<button class="tab-btn" data-tab="progreso">Progreso</button>
|
| 156 |
+
</nav>
|
| 157 |
+
|
| 158 |
+
<main>
|
| 159 |
+
|
| 160 |
+
<!-- ================= TEORÍA ================= -->
|
| 161 |
+
<section id="teoria" class="active">
|
| 162 |
+
|
| 163 |
+
<h2 class="section-title">¿Qué evalúa esta categoría?</h2>
|
| 164 |
+
<div class="card">
|
| 165 |
+
<p>En Suiza este concepto viene del libro <i>«Vernetztes Denken im Management»</i> de Gomez & Probst (1987). El examen lo usa porque en informática todo es un sistema: una base de datos, un servidor, un equipo, un usuario.</p>
|
| 166 |
+
<p>La pregunta real detrás de cada ejercicio es siempre la misma: <b>¿puedes predecir qué pasa en el punto E de una cadena cuando modificas el punto B?</b></p>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<h2 class="section-title">La línea de señales: + y −</h2>
|
| 170 |
+
<div class="card">
|
| 171 |
+
<p>Toda cadena se construye con solo dos tipos de tramo:</p>
|
| 172 |
+
<div class="line-wrap">
|
| 173 |
+
<div class="station"><div class="dot"></div><div class="lbl">A</div></div>
|
| 174 |
+
<div class="track"><div class="signal pos">+</div></div>
|
| 175 |
+
<div class="station"><div class="dot"></div><div class="lbl">B</div></div>
|
| 176 |
+
</div>
|
| 177 |
+
<p style="margin-top:-4px">Si A sube, B sube (misma dirección). Ej: más horas de estudio (+) → más conocimiento (+).</p>
|
| 178 |
+
<div class="line-wrap">
|
| 179 |
+
<div class="station"><div class="dot"></div><div class="lbl">C</div></div>
|
| 180 |
+
<div class="track"><div class="signal neg">−</div></div>
|
| 181 |
+
<div class="station"><div class="dot"></div><div class="lbl">D</div></div>
|
| 182 |
+
</div>
|
| 183 |
+
<p style="margin-top:-4px">Si C sube, D baja (dirección contraria). Ej: más errores de código (+) → menos satisfacción del cliente (−).</p>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<h2 class="section-title">La regla de oro (lo más importante)</h2>
|
| 187 |
+
<div class="card">
|
| 188 |
+
<div class="rule-box">
|
| 189 |
+
<p><b>Cuenta cuántas señales negativas (−) hay en toda la cadena.</b></p>
|
| 190 |
+
<p style="margin:0">Par (0, 2, 4...) → el efecto final es <b style="color:var(--pos)">POSITIVO</b><br>
|
| 191 |
+
Impar (1, 3, 5...) → el efecto final es <b style="color:var(--neg)">NEGATIVO</b></p>
|
| 192 |
+
</div>
|
| 193 |
+
<p>Funciona igual que multiplicar signos en matemáticas. Es la forma más rápida de resolver esto bajo presión de tiempo, sin "sentir" la lógica paso a paso.</p>
|
| 194 |
+
<p><b>Ejemplo — 2 negativos (par) → efecto positivo:</b></p>
|
| 195 |
+
<div class="line-wrap">
|
| 196 |
+
<div class="station"><div class="dot"></div><div class="lbl">A</div></div>
|
| 197 |
+
<div class="track"><div class="signal pos">+</div></div>
|
| 198 |
+
<div class="station"><div class="dot"></div><div class="lbl">B</div></div>
|
| 199 |
+
<div class="track"><div class="signal neg">−</div></div>
|
| 200 |
+
<div class="station"><div class="dot"></div><div class="lbl">C</div></div>
|
| 201 |
+
<div class="track"><div class="signal pos">+</div></div>
|
| 202 |
+
<div class="station"><div class="dot"></div><div class="lbl">D</div></div>
|
| 203 |
+
<div class="track"><div class="signal neg">−</div></div>
|
| 204 |
+
<div class="station"><div class="dot"></div><div class="lbl">E</div></div>
|
| 205 |
+
</div>
|
| 206 |
+
<p style="margin-top:2px">2 tramos negativos → par → si A sube, E también sube.</p>
|
| 207 |
+
</div>
|
| 208 |
+
|
| 209 |
+
<h2 class="section-title">Bucles de refuerzo vs. de balance</h2>
|
| 210 |
+
<div class="card">
|
| 211 |
+
<p><b>Bucle de refuerzo (reinforcing):</b> el sistema se acelera a sí mismo, sin freno natural. Ejemplo clásico: nenúfares que se duplican cada día — si tardan 1 mes en cubrir la mitad de un estanque, cubren la otra mitad en <b>un solo día más</b>, no en otro mes. Crecimiento exponencial.</p>
|
| 212 |
+
<p><b>Bucle de balance (balancing):</b> el sistema se autorregula hacia un equilibrio. Ejemplo: un termostato, o un servidor con auto-escalado que deja de añadir máquinas cuando la carga por servidor ya bajó lo suficiente.</p>
|
| 213 |
+
</div>
|
| 214 |
+
|
| 215 |
+
<h2 class="section-title">El error más común</h2>
|
| 216 |
+
<div class="card">
|
| 217 |
+
<p>Pensamos de forma lineal y a corto plazo por naturaleza. El examen busca justo eso: si caes en "causa → efecto inmediato" sin pensar en la segunda y tercera consecuencia. Al resolver, pregúntate siempre: <b>«¿y luego qué pasa? ¿y después de eso?»</b></p>
|
| 218 |
+
</div>
|
| 219 |
+
|
| 220 |
+
</section>
|
| 221 |
+
|
| 222 |
+
<!-- ================= PRACTICAR ================= -->
|
| 223 |
+
<section id="practicar">
|
| 224 |
+
<h2 class="section-title">12 ejercicios — de básico a examen real</h2>
|
| 225 |
+
<p style="font-size:13px;color:var(--muted);margin-top:-6px;margin-bottom:16px;">Toca «Revelar solución» cuando quieras comprobar tu respuesta. Usa el temporizador para simular presión de examen (60s).</p>
|
| 226 |
+
<div id="exercise-list"></div>
|
| 227 |
+
</section>
|
| 228 |
+
|
| 229 |
+
<!-- ================= PROGRESO ================= -->
|
| 230 |
+
<section id="progreso">
|
| 231 |
+
<div class="progress-hero">
|
| 232 |
+
<div class="progress-num" id="prog-count">0 / 12</div>
|
| 233 |
+
<div class="progress-label">ejercicios dominados</div>
|
| 234 |
+
<div class="bar-track"><div class="bar-fill" id="prog-bar" style="width:0%"></div></div>
|
| 235 |
+
</div>
|
| 236 |
+
|
| 237 |
+
<h2 class="section-title">Checklist de dominio</h2>
|
| 238 |
+
<div class="card" id="checklist-card"></div>
|
| 239 |
+
|
| 240 |
+
<h2 class="section-title">Detalle por ejercicio</h2>
|
| 241 |
+
<div class="card" id="prog-list"></div>
|
| 242 |
+
|
| 243 |
+
<button class="reset-link" id="reset-btn">Reiniciar todo mi progreso</button>
|
| 244 |
+
<div class="loading-note" id="storage-note"></div>
|
| 245 |
+
</section>
|
| 246 |
+
|
| 247 |
+
</main>
|
| 248 |
+
|
| 249 |
+
<script>
|
| 250 |
+
/* ---------------- DATA ---------------- */
|
| 251 |
+
const exercises = [
|
| 252 |
+
{
|
| 253 |
+
level:"basico",
|
| 254 |
+
context:"Aumenta la temperatura del servidor (A) → aumenta el riesgo de fallo del hardware (B) → disminuye la disponibilidad del sistema (C).",
|
| 255 |
+
question:"Si la temperatura del servidor (A) DISMINUYE, ¿qué pasa con la disponibilidad del sistema (C)?",
|
| 256 |
+
chain:[["A","+","B"],["B","-","C"]],
|
| 257 |
+
solution:"A→B es (+), B→C es (−): 1 negativo (impar) → efecto contrario al cambio. Como A disminuye (no aumenta), invertimos: B baja, y como B→C es negativa, C sube. <b>La disponibilidad (C) AUMENTA.</b>"
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
level:"basico",
|
| 261 |
+
context:"Aumenta el número de usuarios activos (A) → aumenta la carga del servidor (B) → aumenta el tiempo de respuesta (C) → disminuye la satisfacción del usuario (D).",
|
| 262 |
+
question:"Si A aumenta, ¿qué pasa con la satisfacción del usuario (D)?",
|
| 263 |
+
chain:[["A","+","B"],["B","+","C"],["C","-","D"]],
|
| 264 |
+
solution:"Negativos en la cadena: 1 (impar) → efecto final negativo. <b>La satisfacción (D) DISMINUYE.</b>"
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
level:"basico",
|
| 268 |
+
context:"Disminuye el presupuesto de marketing (A) → disminuye el número de nuevos clientes (B) → disminuye el ingreso mensual (C) → aumenta la presión financiera (D).",
|
| 269 |
+
question:"Si el presupuesto de marketing (A) AUMENTA (contrario al enunciado), ¿qué pasa con la presión financiera (D)?",
|
| 270 |
+
chain:[["A","+","B"],["B","+","C"],["C","-","D"]],
|
| 271 |
+
solution:"Con la dirección original (A baja): 1 negativo (impar) → D sube. Como ahora A sube (dirección contraria), el resultado se invierte. <b>La presión financiera (D) DISMINUYE.</b>"
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
level:"intermedio",
|
| 275 |
+
context:"Empresa de software: más presión por entregar (A) → menos tiempo de revisión de código (B) → más errores en la app (C) → más tiempo de soporte resolviendo quejas (D) → menos satisfacción del cliente (E).",
|
| 276 |
+
question:"Si la empresa DUPLICA directamente el tiempo de revisión (B), sin que A cambie, ¿qué pasa con la satisfacción del cliente (E)?",
|
| 277 |
+
chain:[["B","-","C"],["C","+","D"],["D","-","E"]],
|
| 278 |
+
solution:"Ignora A→B (no aplica, el cambio empieza en B). Desde B: negativos = 2 (par) → efecto positivo. Si B aumenta: C baja, D baja, E sube. <b>La satisfacción (E) AUMENTA.</b>"
|
| 279 |
+
},
|
| 280 |
+
{
|
| 281 |
+
level:"intermedio",
|
| 282 |
+
context:"Mismo sistema anterior. Ahora la empresa contrata soporte externo que reduce directamente la carga de D, sin que C haya cambiado.",
|
| 283 |
+
question:"¿Qué le pasa a la satisfacción del cliente (E)?",
|
| 284 |
+
chain:[["D","-","E"]],
|
| 285 |
+
solution:"Solo importa D→E, que es negativa. D baja → E sube. <b>La satisfacción (E) AUMENTA.</b> Esto muestra que puedes intervenir en un nodo intermedio sin tocar el origen de la cadena.",
|
| 286 |
+
note:"En este ejercicio no hace falta ver toda la cadena — solo el último tramo. Truco válido para el examen real: enfócate solo en el tramo relevante a la pregunta."
|
| 287 |
+
},
|
| 288 |
+
{
|
| 289 |
+
level:"intermedio",
|
| 290 |
+
context:"Un bug crítico se publica en redes (A) → más usuarios ven la publicación (B) → más usuarios comparten el post (C) → esto vuelve a aumentar directamente a B (retroalimentación).",
|
| 291 |
+
question:"¿Es un bucle de refuerzo o de balance? ¿Qué pasará con el número de usuarios que ven el post con el tiempo, sin intervención?",
|
| 292 |
+
chain:[["B","+","C"],["C","+","B"]],
|
| 293 |
+
solution:"Es un <b>bucle de refuerzo</b>: B aumenta C, y C vuelve a aumentar B. Sin intervención, crecerá de forma <b>exponencial</b> (como los nenúfares), no lineal — cada hora de retraso es exponencialmente peor, no «un poco peor»."
|
| 294 |
+
},
|
| 295 |
+
{
|
| 296 |
+
level:"intermedio",
|
| 297 |
+
context:"Servidor con auto-escalado: más carga de usuarios (A) → el sistema añade más servidores (B) → la carga POR SERVIDOR disminuye (C) → esto detiene el añadido de más servidores.",
|
| 298 |
+
question:"¿Refuerzo o balance? ¿Hacia dónde tiende el sistema?",
|
| 299 |
+
chain:[["A","+","B"],["B","-","C"]],
|
| 300 |
+
solution:"Es un <b>bucle de balance</b>: cuando C baja lo suficiente, el sistema deja de reaccionar — busca un punto de equilibrio en vez de crecer sin control. Patrón típico de infraestructura bien diseñada."
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
level:"avanzado",
|
| 304 |
+
context:"3 caminos hacia el mismo resultado (X = errores en producción): Automatización de pruebas (A, sube) → X baja. Capacitación del equipo (B, igual) → X baja. Presión de tiempo (C, sube) → X sube.",
|
| 305 |
+
question:"Si A sube, B se mantiene y C también sube al mismo tiempo, ¿se puede saber con certeza qué pasa con X?",
|
| 306 |
+
chain:[["A","-","X"],["C","+","X"]],
|
| 307 |
+
solution:"<b>No, no se puede saber con certeza</b> sin conocer la MAGNITUD de cada efecto. Cuando dos fuerzas empujan en direcciones opuestas sobre la misma variable, el resultado depende de cuál es más fuerte — no solo de la dirección. Esta es la diferencia entre pensamiento sistémico básico (direcciones) y avanzado (magnitudes relativas)."
|
| 308 |
+
},
|
| 309 |
+
{
|
| 310 |
+
level:"avanzado",
|
| 311 |
+
context:"Deuda técnica (A) → código difícil de entender (B) → más tiempo para implementar funciones (C) → más presión de la empresa (D) → más atajos de código (E) → esto vuelve a aumentar la deuda técnica (A).",
|
| 312 |
+
question:"¿Refuerzo o balance? Si quieres romper el ciclo, ¿qué tramo conviene atacar y por qué?",
|
| 313 |
+
chain:[["A","+","B"],["B","+","C"],["C","+","D"],["D","+","E"],["E","+","A"]],
|
| 314 |
+
solution:"<b>Bucle de refuerzo (círculo vicioso)</b> — todas las relaciones son positivas, el sistema se degrada progresivamente sin límite. Conviene romper en D→E (reducir presión) o en A (tiempo dedicado a refactorizar). Atacar solo C (pedir «trabajar más rápido») NO rompe el ciclo — lo empeora, porque solo alimenta D. No todos los puntos de un ciclo son igual de eficaces para intervenir."
|
| 315 |
+
},
|
| 316 |
+
{
|
| 317 |
+
level:"avanzado",
|
| 318 |
+
context:"Mes 1: la empresa reduce el equipo de QA para ahorrar (A). Los errores no suben de inmediato porque el código ya probado sigue funcionando. Mes 4: empiezan a acumularse errores no detectados (B). Mes 6: la satisfacción del cliente cae fuerte (C).",
|
| 319 |
+
question:"¿Por qué es peligroso este tipo de sistema con retraso (delay) para quien toma la decisión?",
|
| 320 |
+
chain:[["A","+","B"],["B","-","C"]],
|
| 321 |
+
solution:"Porque quien decide no ve las consecuencias de inmediato — falsa sensación de que «la decisión fue correcta» durante meses. Cuando el problema aparece, ya es más grande y difícil de revertir, y es difícil conectar causa (Mes 1) y efecto (Mes 6) por el tiempo transcurrido. Los sistemas con retraso engañan a quien decide, aparentando que todo va bien justo antes de una crisis."
|
| 322 |
+
},
|
| 323 |
+
{
|
| 324 |
+
level:"avanzado",
|
| 325 |
+
context:"Tienda online: más publicidad (A) → más visitantes (B) → más carga del servidor (C) → más lentitud (D) → menos conversión de ventas (E).",
|
| 326 |
+
question:"3 opciones con el mismo costo: 1) más publicidad, 2) mejorar el servidor (atacar C directo), 3) reducir publicidad a la mitad. ¿Cuál tiene más sentido sistémico y por qué NO la opción 1?",
|
| 327 |
+
chain:[["A","+","B"],["B","+","C"],["C","+","D"],["D","-","E"]],
|
| 328 |
+
solution:"<b>La Opción 2 es la más inteligente.</b> Más publicidad (Opción 1) parece obvia pensando linealmente, pero solo empeora el cuello de botella (más carga → más lentitud → menos conversión), pudiendo anular el beneficio. Atacar el cuello de botella real (servidor) sin generar más carga es la jugada sistémica correcta: atacar el punto de entrada en vez de la causa raíz es el error más común en gestión."
|
| 329 |
+
},
|
| 330 |
+
{
|
| 331 |
+
level:"avanzado",
|
| 332 |
+
context:"Banca móvil lanza función sin suficientes pruebas (A). 2 semanas después (retraso) aparecen errores en un grupo pequeño de usuarios (B, sube levemente). Quejas en redes (C) — si supera un umbral crítico de visibilidad, provoca sobrecarga masiva del sistema (D) con efecto exponencial.",
|
| 333 |
+
question:"¿Por qué es crucial actuar ANTES de superar el umbral crítico de visibilidad, en vez de esperar a ver «qué tan grave es» el problema inicial?",
|
| 334 |
+
chain:[["A","+","B"],["B","+","C"],["C","+","D"]],
|
| 335 |
+
solution:"Una vez que el sistema cruza el umbral crítico y entra en crecimiento exponencial (bucle de refuerzo), la velocidad de reacción necesaria aumenta exponencialmente también. Lo que era una corrección simple en fase B se convierte en crisis en fase D. Esperar mientras el sistema está en su fase lenta es el error clásico: estos sistemas parecen manejables hasta el momento exacto en que dejan de serlo."
|
| 336 |
+
}
|
| 337 |
+
];
|
| 338 |
+
|
| 339 |
+
const checklistItems = [
|
| 340 |
+
"Puedo contar signos negativos en una cadena de 5+ elementos y determinar el efecto final en menos de 30 segundos",
|
| 341 |
+
"Puedo distinguir un bucle de refuerzo de uno de balance a partir de un texto descriptivo",
|
| 342 |
+
"Entiendo por qué «atacar el síntoma en el punto de entrada» suele ser un error sistémico",
|
| 343 |
+
"Puedo identificar cuándo NO hay suficiente información para responder con certeza",
|
| 344 |
+
"Entiendo el concepto de retraso temporal (delay) y por qué engaña a quien decide",
|
| 345 |
+
"Puedo construir yo mismo un diagrama de causalidad a partir de un párrafo narrativo"
|
| 346 |
+
];
|
| 347 |
+
|
| 348 |
+
/* ---------------- STATE (persisted via window.storage) ---------------- */
|
| 349 |
+
let mastered = {}; // { exIndex: true }
|
| 350 |
+
let checklist = {}; // { itemIndex: true }
|
| 351 |
+
let timers = {}; // { exIndex: intervalId }
|
| 352 |
+
|
| 353 |
+
const levelLabel = { basico:"Básico", intermedio:"Intermedio", avanzado:"Avanzado", extremo:"Extremo" };
|
| 354 |
+
|
| 355 |
+
exercises.push({
|
| 356 |
+
level:"extremo",
|
| 357 |
+
context:"Sistema bancario con 5 variables: A=Automatización de procesos, B=Vulnerabilidades de seguridad, C=Frecuencia de auditorías, D=Confianza del cliente, E=Costes operativos. Reglas: A↑→B↑ y A↑→E↓ · B↑→D↓ · D↓→C↑ (relación inversa) · C↑→B↓ y C↑→E↑.",
|
| 358 |
+
question:"Si el banco eleva al máximo la Automatización (A), rastrea el impacto en cadena. 1) ¿Qué pasa con la Confianza del cliente (D) tras la reacción de las auditorías? 2) ¿Qué pasa con los Costes (E), que reciben dos fuerzas por caminos distintos?",
|
| 359 |
+
extraChains:[
|
| 360 |
+
{label:"Vía 1 — impacto inicial en D, con bucle de corrección vía C", chain:[["A","+","B"],["B","-","D"],["D","-","C"],["C","-","B"]]},
|
| 361 |
+
{label:"Vía 2 — las dos fuerzas que llegan a E", chain:[["A","-","E"]]},
|
| 362 |
+
{label:"Vía 2b — el camino indirecto que también llega a E", chain:[["A","+","B"],["B","-","D"],["D","-","C"],["C","+","E"]]}
|
| 363 |
+
],
|
| 364 |
+
solution:`<b>1) Confianza del cliente (D):</b> El primer golpe es directo: A↑→B↑→D↓ (cae). Pero el bucle B→D→C→B tiene 3 signos negativos (impar) → es un <b>bucle Estabilizador (balancing)</b>: las auditorías reaccionan y frenan la caída de D. Aquí hay que ser más cuidadosos que con una cadena simple: como A empuja a B de forma <i>sostenida</i> (no es un golpe único), no podemos afirmar con certeza absoluta que D "se recupere por encima del nivel original" — solo podemos afirmar con certeza que <b>el bucle es estabilizador y amortigua la caída inicial</b>. Afirmar un rebote neto positivo exacto necesitaría conocer las magnitudes.
|
| 365 |
+
<br><br><b>2) Costes operativos (E):</b> Hay dos fuerzas simultáneas y opuestas: la Vía 2 (A reduce E directamente, ahorro por automatización) y la Vía 2b (A→B→D→C→E, la crisis de seguridad obliga a más auditorías, que cuestan más). Con signos: Vía 2 = 1 negativo (impar) = efecto negativo directo sobre E. Vía 2b = 3 negativos (impar) = efecto negativo... pero ojo, ese último tramo C→E es POSITIVO explícitamente (más auditorías = más costes), así que recuenta: A(+)B(-)D(-)C(+)E → 2 negativos (par) → efecto positivo de esa vía. Con una vía empujando E hacia abajo y la otra empujando E hacia arriba al mismo tiempo, <b>el impacto neto es indeterminado sin conocer las magnitudes relativas</b> — igual que en el Ejercicio 7 de este mismo documento.`,
|
| 366 |
+
note:"Ejercicio basado en un caso originalmente propuesto con una clasificación distinta del bucle de D. Aquí se recalculó contando los signos del bucle B→D→C→B: son 3 negativos (impar) → Estabilizador. Verifica tú mismo contando las flechas — es la mejor forma de fijar la regla de conteo de negativos."
|
| 367 |
+
});
|
| 368 |
+
|
| 369 |
+
/* ---- Ejercicios 14-18: lógica de signos verificada por código antes de escribirse ---- */
|
| 370 |
+
exercises.push(
|
| 371 |
+
{
|
| 372 |
+
level:"intermedio",
|
| 373 |
+
context:"Pipeline de despliegue: más revisiones automáticas (A) → más errores detectados antes de producción (B) → menos incidentes en producción (C) → menos interrupciones del equipo (D). Ojo al modelar: «menos incidentes → menos interrupciones» es una relación POSITIVA (se mueven en el mismo sentido).",
|
| 374 |
+
question:"Si las revisiones automáticas (A) DISMINUYEN (se desactivan por ahorrar tiempo de CI), ¿qué pasa con las interrupciones del equipo (D)?",
|
| 375 |
+
chain:[["A","+","B"],["B","-","C"],["C","+","D"]],
|
| 376 |
+
solution:"Negativos: 1 (impar) → A y D se mueven en sentidos OPUESTOS. Como A disminuye, D aumenta. Verifícalo tramo a tramo: A↓ → B↓ (menos errores detectados) → C↑ (más incidentes llegan a producción) → D↑ (más interrupciones). <b>Las interrupciones (D) AUMENTAN.</b>",
|
| 377 |
+
note:"La trampa de este ejercicio está en el MODELADO, no en el conteo: la frase «menos incidentes → menos interrupciones» describe dos variables bajando juntas — eso es una relación positiva (+), no negativa. El signo captura si se mueven juntas u opuestas, no si la frase contiene la palabra «menos». Es el error de traducción texto→diagrama más común del examen."
|
| 378 |
+
},
|
| 379 |
+
{
|
| 380 |
+
level:"avanzado",
|
| 381 |
+
context:"Se lanza una función muy compleja (A = complejidad del lanzamiento). Camino 1: A↑ → más tickets de usuarios confundidos (B) → más carga del equipo de soporte (X). Camino 2: A↑ → menos tiempo para documentar, calidad de documentación (C) baja → y menos documentación significa más carga de soporte (X).",
|
| 382 |
+
question:"Ambos caminos llegan a X. ¿Se puede determinar con certeza qué pasa con la carga de soporte (X) cuando A sube, o es indeterminado como en el Ejercicio 8?",
|
| 383 |
+
extraChains:[
|
| 384 |
+
{label:"Camino 1", chain:[["A","+","B"],["B","+","X"]]},
|
| 385 |
+
{label:"Camino 2", chain:[["A","-","C"],["C","-","X"]]}
|
| 386 |
+
],
|
| 387 |
+
solution:"Camino 1: 0 negativos (par) → empuja X hacia ARRIBA. Camino 2: 2 negativos (par) → también empuja X hacia ARRIBA (A↑→C↓→X↑). <b>Las dos fuerzas empujan en el MISMO sentido → X AUMENTA con certeza total, sin necesidad de conocer magnitudes.</b> Esta es la pareja conceptual del Ejercicio 8: la convergencia de caminos solo es indeterminada cuando los caminos se OPONEN. Si coinciden, la certeza es absoluta. En el examen, calcula el signo neto de CADA camino antes de declarar «indeterminado»."
|
| 388 |
+
},
|
| 389 |
+
{
|
| 390 |
+
level:"avanzado",
|
| 391 |
+
context:"Equipo de operaciones: más incidentes en producción (P) → menos tiempo para trabajo planificado (T) → menos mantenimiento preventivo (M) → esto vuelve a aumentar los incidentes (P).",
|
| 392 |
+
question:"El bucle P→T→M→P contiene signos negativos. ¿Es un bucle de balance entonces? Clasifícalo contando y explica qué pasará con el tiempo.",
|
| 393 |
+
chain:[["P","-","T"],["T","+","M"],["M","-","P"]],
|
| 394 |
+
solution:"Negativos en el bucle: P→T (−) y M→P (−) = <b>2 negativos (PAR) → producto positivo → bucle de REFUERZO</b>, no de balance. Es el clásico círculo vicioso operativo: más incidentes → menos preventivo → aún más incidentes, espiral descendente. <b>La lección clave: contener signos negativos NO hace a un bucle estabilizador — solo el número IMPAR de negativos lo hace.</b> Es el error de clasificación más común (y el mismo tipo de error que corregimos en el material de Gemini del Ejercicio 13)."
|
| 395 |
+
},
|
| 396 |
+
{
|
| 397 |
+
level:"extremo",
|
| 398 |
+
context:"Mismo círculo vicioso del ejercicio anterior (P→T→M→P, refuerzo), pero ahora con retraso: el efecto de reducir mantenimiento (M) tarda ~3 meses en convertirse en incidentes nuevos. La dirección propone: «contratemos bomberos de incidentes» (más gente dedicada a apagar fuegos, reduce P directamente).",
|
| 399 |
+
question:"¿Por qué esa intervención NO rompe el bucle, y cuál sí lo haría? ¿Qué papel juega el retraso de 3 meses en que la dirección crea que su plan funciona?",
|
| 400 |
+
chain:[["P","-","T"],["T","+","M"],["M","-","P"]],
|
| 401 |
+
solution:"Contratar «bomberos» reduce P temporalmente pero <b>no toca ninguna flecha del bucle</b> — en cuanto el refuerzo dé otra vuelta, P vuelve a subir y ahora con más nómina. Romper el bucle exige cortar una flecha: <b>proteger tiempo de mantenimiento preventivo (M) de forma innegociable</b> (rompe T→M) es la intervención estructural. El retraso de 3 meses es el veneno: tras contratar bomberos, P baja de inmediato y M sigue cayendo silenciosamente — la dirección ve 3 meses de «mejora» justo mientras el sistema se degrada, y cuando la ola de incidentes llega, la atribuye a otra causa. <b>Retraso + refuerzo = la combinación que más decisiones erróneas produce.</b>"
|
| 402 |
+
},
|
| 403 |
+
{
|
| 404 |
+
level:"extremo",
|
| 405 |
+
context:"El Ejercicio 8 quedó indeterminado por falta de magnitudes. Ahora las tienes: cada punto de automatización (A) reduce los errores (X) en 2 unidades por la vía directa, y los aumenta en 1 unidad por la vía de la presión de tiempo. A sube 4 puntos.",
|
| 406 |
+
question:"¿Ahora sí se puede determinar el efecto sobre X? Calcula el cambio neto.",
|
| 407 |
+
extraChains:[
|
| 408 |
+
{label:"Vía directa (−2 por punto)", chain:[["A","-","X"]]},
|
| 409 |
+
{label:"Vía presión (+1 por punto)", chain:[["A","+","X"]]}
|
| 410 |
+
],
|
| 411 |
+
solution:"Sí — <b>con magnitudes, lo indeterminado se vuelve determinable</b>. Neto por punto de A: −2 + 1 = −1. Con A subiendo 4 puntos: 4 × (−1) = <b>−4 → los errores (X) BAJAN 4 unidades</b>. Cierre conceptual de toda la categoría: dirección de signos (nivel 1) → paridad en cadenas y bucles (nivel 2) → reconocer indeterminación (nivel 3) → resolverla con magnitudes (nivel 4). El examen puede preguntarte en cualquiera de los 4 niveles."
|
| 412 |
+
}
|
| 413 |
+
);
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
/* ---------------- RENDER: chain diagram ---------------- */
|
| 417 |
+
function renderChain(chain){
|
| 418 |
+
if(!chain || !chain.length) return "";
|
| 419 |
+
let html = '<div class="line-wrap">';
|
| 420 |
+
chain.forEach((tramo, i) => {
|
| 421 |
+
const [from, sign, to] = tramo;
|
| 422 |
+
if(i === 0){
|
| 423 |
+
html += `<div class="station"><div class="dot"></div><div class="lbl">${from}</div></div>`;
|
| 424 |
+
}
|
| 425 |
+
const cls = sign === "+" ? "pos" : "neg";
|
| 426 |
+
html += `<div class="track"><div class="signal ${cls}">${sign}</div></div>`;
|
| 427 |
+
html += `<div class="station"><div class="dot"></div><div class="lbl">${to}</div></div>`;
|
| 428 |
+
});
|
| 429 |
+
html += '</div>';
|
| 430 |
+
return html;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
/* ---------------- RENDER: exercises ---------------- */
|
| 434 |
+
function renderExercises(){
|
| 435 |
+
const list = document.getElementById("exercise-list");
|
| 436 |
+
list.innerHTML = exercises.map((ex, i) => {
|
| 437 |
+
const isMastered = !!mastered[i];
|
| 438 |
+
const chainsHtml = ex.extraChains
|
| 439 |
+
? ex.extraChains.map(c => `<div class="subchain-label">${c.label}</div>${renderChain(c.chain)}`).join("")
|
| 440 |
+
: renderChain(ex.chain);
|
| 441 |
+
return `
|
| 442 |
+
<div class="ex-card" data-idx="${i}">
|
| 443 |
+
<div class="ex-head">
|
| 444 |
+
<span class="ex-num">Ejercicio ${i+1} / ${exercises.length}</span>
|
| 445 |
+
<span class="level-tag level-${ex.level}">${levelLabel[ex.level]}</span>
|
| 446 |
+
</div>
|
| 447 |
+
<p class="ex-context">${ex.context}</p>
|
| 448 |
+
${chainsHtml}
|
| 449 |
+
<p class="ex-question">${ex.question}</p>
|
| 450 |
+
<div class="ex-actions">
|
| 451 |
+
<button class="btn btn-primary reveal-btn" data-idx="${i}">Revelar solución</button>
|
| 452 |
+
<button class="btn btn-timer timer-btn" data-idx="${i}">⏱ 60s</button>
|
| 453 |
+
<span class="timer-display" id="timer-${i}"></span>
|
| 454 |
+
<button class="btn btn-master ${isMastered ? 'done' : ''}" data-idx="${i}">
|
| 455 |
+
${isMastered ? '✓ Dominado' : 'Marcar dominado'}
|
| 456 |
+
</button>
|
| 457 |
+
</div>
|
| 458 |
+
<div class="solution" id="sol-${i}">${ex.solution}${ex.note ? `<p style="margin-top:8px;color:var(--muted);font-size:13px">${ex.note}</p>` : ""}</div>
|
| 459 |
+
</div>`;
|
| 460 |
+
}).join("");
|
| 461 |
+
|
| 462 |
+
list.querySelectorAll(".reveal-btn").forEach(btn => {
|
| 463 |
+
btn.addEventListener("click", () => {
|
| 464 |
+
const idx = btn.dataset.idx;
|
| 465 |
+
document.getElementById(`sol-${idx}`).classList.toggle("open");
|
| 466 |
+
btn.textContent = document.getElementById(`sol-${idx}`).classList.contains("open") ? "Ocultar solución" : "Revelar solución";
|
| 467 |
+
});
|
| 468 |
+
});
|
| 469 |
+
|
| 470 |
+
list.querySelectorAll(".timer-btn").forEach(btn => {
|
| 471 |
+
btn.addEventListener("click", () => startTimer(parseInt(btn.dataset.idx)));
|
| 472 |
+
});
|
| 473 |
+
|
| 474 |
+
list.querySelectorAll(".btn-master").forEach(btn => {
|
| 475 |
+
btn.addEventListener("click", () => toggleMastered(parseInt(btn.dataset.idx)));
|
| 476 |
+
});
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
function startTimer(idx){
|
| 480 |
+
if(timers[idx]) { clearInterval(timers[idx]); timers[idx]=null; }
|
| 481 |
+
let remaining = 60;
|
| 482 |
+
const display = document.getElementById(`timer-${idx}`);
|
| 483 |
+
const btn = document.querySelector(`.timer-btn[data-idx="${idx}"]`);
|
| 484 |
+
btn.classList.add("running");
|
| 485 |
+
btn.textContent = "Detener";
|
| 486 |
+
display.textContent = "0:60";
|
| 487 |
+
timers[idx] = setInterval(() => {
|
| 488 |
+
remaining--;
|
| 489 |
+
const m = Math.floor(remaining/60), s = remaining%60;
|
| 490 |
+
display.textContent = `0:${s.toString().padStart(2,"0")}`;
|
| 491 |
+
display.classList.toggle("urgent", remaining <= 10);
|
| 492 |
+
if(remaining <= 0){
|
| 493 |
+
clearInterval(timers[idx]);
|
| 494 |
+
timers[idx]=null;
|
| 495 |
+
display.textContent = "¡Tiempo!";
|
| 496 |
+
btn.classList.remove("running");
|
| 497 |
+
btn.textContent = "⏱ 60s";
|
| 498 |
+
}
|
| 499 |
+
}, 1000);
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
async function toggleMastered(idx){
|
| 503 |
+
mastered[idx] = !mastered[idx];
|
| 504 |
+
await saveState();
|
| 505 |
+
renderExercises();
|
| 506 |
+
renderProgress();
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
/* ---------------- RENDER: progress tab ---------------- */
|
| 510 |
+
function renderChecklist(){
|
| 511 |
+
const card = document.getElementById("checklist-card");
|
| 512 |
+
card.innerHTML = checklistItems.map((txt, i) => `
|
| 513 |
+
<label class="checklist-item">
|
| 514 |
+
<input type="checkbox" data-cidx="${i}" ${checklist[i] ? "checked" : ""}>
|
| 515 |
+
<span>${txt}</span>
|
| 516 |
+
</label>
|
| 517 |
+
`).join("");
|
| 518 |
+
card.querySelectorAll("input[type=checkbox]").forEach(cb => {
|
| 519 |
+
cb.addEventListener("change", async (e) => {
|
| 520 |
+
checklist[e.target.dataset.cidx] = e.target.checked;
|
| 521 |
+
await saveState();
|
| 522 |
+
});
|
| 523 |
+
});
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
function renderProgress(){
|
| 527 |
+
const count = Object.values(mastered).filter(Boolean).length;
|
| 528 |
+
document.getElementById("prog-count").textContent = `${count} / ${exercises.length}`;
|
| 529 |
+
document.getElementById("prog-bar").style.width = `${(count/exercises.length)*100}%`;
|
| 530 |
+
|
| 531 |
+
const list = document.getElementById("prog-list");
|
| 532 |
+
list.innerHTML = exercises.map((ex, i) => `
|
| 533 |
+
<div class="prog-row">
|
| 534 |
+
<span>Ejercicio ${i+1} · ${levelLabel[ex.level]}</span>
|
| 535 |
+
<span class="prog-check ${mastered[i] ? 'on' : 'off'}"></span>
|
| 536 |
+
</div>
|
| 537 |
+
`).join("");
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
/* ---------------- STORAGE ---------------- */
|
| 541 |
+
async function loadState(){
|
| 542 |
+
const note = document.getElementById("storage-note");
|
| 543 |
+
try{
|
| 544 |
+
const res = await window.storage.get("vernetztes-denken-progress");
|
| 545 |
+
if(res && res.value){
|
| 546 |
+
const data = JSON.parse(res.value);
|
| 547 |
+
mastered = data.mastered || {};
|
| 548 |
+
checklist = data.checklist || {};
|
| 549 |
+
}
|
| 550 |
+
}catch(e){
|
| 551 |
+
// no data yet, that's fine
|
| 552 |
+
}
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
async function saveState(){
|
| 556 |
+
try{
|
| 557 |
+
await window.storage.set("vernetztes-denken-progress", JSON.stringify({ mastered, checklist }));
|
| 558 |
+
}catch(e){
|
| 559 |
+
const note = document.getElementById("storage-note");
|
| 560 |
+
if(note) note.textContent = "No se pudo guardar el progreso (modo sin conexión).";
|
| 561 |
+
}
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
document.getElementById("reset-btn").addEventListener("click", async () => {
|
| 565 |
+
if(!confirm("¿Reiniciar todo tu progreso? Esta acción no se puede deshacer.")) return;
|
| 566 |
+
mastered = {}; checklist = {};
|
| 567 |
+
await saveState();
|
| 568 |
+
renderExercises(); renderChecklist(); renderProgress();
|
| 569 |
+
});
|
| 570 |
+
|
| 571 |
+
/* ---------------- TABS ---------------- */
|
| 572 |
+
document.querySelectorAll(".tab-btn").forEach(btn => {
|
| 573 |
+
btn.addEventListener("click", () => {
|
| 574 |
+
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
| 575 |
+
document.querySelectorAll("main > section").forEach(s => s.classList.remove("active"));
|
| 576 |
+
btn.classList.add("active");
|
| 577 |
+
document.getElementById(btn.dataset.tab).classList.add("active");
|
| 578 |
+
});
|
| 579 |
+
});
|
| 580 |
+
|
| 581 |
+
/* ---------------- INIT ---------------- */
|
| 582 |
+
(async function init(){
|
| 583 |
+
await loadState();
|
| 584 |
+
renderExercises();
|
| 585 |
+
renderChecklist();
|
| 586 |
+
renderProgress();
|
| 587 |
+
})();
|
| 588 |
+
</script>
|
| 589 |
+
</body>
|
| 590 |
+
</html>
|
apps/vorstellungsvermoegen-app.html
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="es">
|
| 3 |
+
<head>
|
| 4 |
+
<script>
|
| 5 |
+
/* BrainBit storage shim: sustituye window.storage (entorno artifacts de Claude.ai)
|
| 6 |
+
por localStorage con la misma interfaz async {value}. Notifica el progreso al parent. */
|
| 7 |
+
window.storage = {
|
| 8 |
+
async get(key){ const v = localStorage.getItem(key); return v === null ? null : { key, value: v }; },
|
| 9 |
+
async set(key, value){
|
| 10 |
+
localStorage.setItem(key, String(value));
|
| 11 |
+
try { if (window.parent !== window) window.parent.postMessage({ source: "brainbit-app", type: "progress", key: key, value: String(value) }, "*"); } catch(e){}
|
| 12 |
+
}
|
| 13 |
+
};
|
| 14 |
+
window.addEventListener("load", () => {
|
| 15 |
+
try {
|
| 16 |
+
if (window.parent === window) return;
|
| 17 |
+
for (let i = 0; i < localStorage.length; i++) {
|
| 18 |
+
const key = localStorage.key(i);
|
| 19 |
+
if (key && key.endsWith("-progress")) {
|
| 20 |
+
window.parent.postMessage({ source: "brainbit-app", type: "progress", key: key, value: localStorage.getItem(key) }, "*");
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
} catch(e){}
|
| 24 |
+
});
|
| 25 |
+
</script>
|
| 26 |
+
|
| 27 |
+
<meta charset="UTF-8">
|
| 28 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 29 |
+
<title>Vorstellungsvermögen — ICT-Eignungstest</title>
|
| 30 |
+
<style>
|
| 31 |
+
:root{
|
| 32 |
+
--bg:#F7F6F3; --panel:#FFFFFF; --ink:#1C1C1C; --muted:#6B6864; --line:#D8D5CC;
|
| 33 |
+
--red:#D8232A; --pos:#1F7A4C; --pos-bg:#E6F1EA; --neg:#A63A22; --neg-bg:#F6E9E3;
|
| 34 |
+
--amber:#B8860B; --purple:#7A2A63; --purple-bg:#F0E3EE;
|
| 35 |
+
}
|
| 36 |
+
*{box-sizing:border-box;}
|
| 37 |
+
html,body{margin:0;padding:0;background:var(--bg);color:var(--ink);
|
| 38 |
+
font-family:"Helvetica Neue",Arial,sans-serif;-webkit-font-smoothing:antialiased;}
|
| 39 |
+
.mono{font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;}
|
| 40 |
+
|
| 41 |
+
header{background:var(--ink);color:#fff;padding:28px 20px 22px;position:relative;overflow:hidden;}
|
| 42 |
+
header .rail{position:absolute;left:0;right:0;bottom:0;height:4px;
|
| 43 |
+
background:repeating-linear-gradient(90deg,var(--red) 0 18px, transparent 18px 30px);opacity:.9;}
|
| 44 |
+
.eyebrow{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:#C9C6BD;font-weight:700;margin-bottom:8px;}
|
| 45 |
+
h1{font-size:26px;margin:0 0 4px;font-weight:800;letter-spacing:-0.01em;}
|
| 46 |
+
.sub{font-size:14px;color:#C9C6BD;margin:0;max-width:560px;line-height:1.4;}
|
| 47 |
+
|
| 48 |
+
nav.tabs{display:flex;background:var(--panel);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20;}
|
| 49 |
+
nav.tabs button{flex:1;padding:14px 8px;font-size:13px;font-weight:700;background:none;border:none;
|
| 50 |
+
color:var(--muted);cursor:pointer;border-bottom:3px solid transparent;}
|
| 51 |
+
nav.tabs button.active{color:var(--ink);border-bottom-color:var(--red);}
|
| 52 |
+
|
| 53 |
+
main{max-width:760px;margin:0 auto;padding:20px 16px 60px;}
|
| 54 |
+
section{display:none;}
|
| 55 |
+
section.active{display:block;}
|
| 56 |
+
|
| 57 |
+
h2.section-title{font-size:19px;font-weight:800;margin:26px 0 10px;}
|
| 58 |
+
h2.section-title:first-child{margin-top:4px;}
|
| 59 |
+
|
| 60 |
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;margin-bottom:14px;}
|
| 61 |
+
.card p{line-height:1.55;font-size:14.5px;margin:0 0 10px;}
|
| 62 |
+
.card p:last-child{margin-bottom:0;}
|
| 63 |
+
.rule-box{background:#FBF3E4;border:1px solid #E9D9B4;border-radius:8px;padding:14px 16px;margin:10px 0;}
|
| 64 |
+
.rule-box b{color:var(--amber);}
|
| 65 |
+
.fix-box{background:var(--purple-bg);border:1px solid #DCC0D6;border-radius:8px;padding:14px 16px;margin:10px 0;}
|
| 66 |
+
.fix-box b{color:var(--purple);}
|
| 67 |
+
|
| 68 |
+
.ex-card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:14px;}
|
| 69 |
+
.ex-head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:8px;}
|
| 70 |
+
.ex-num{font-size:11px;font-weight:800;letter-spacing:.06em;color:var(--muted);text-transform:uppercase;}
|
| 71 |
+
.level-tag{font-size:10px;font-weight:800;padding:3px 8px;border-radius:20px;text-transform:uppercase;}
|
| 72 |
+
.level-medio{background:#FBF3E4;color:var(--amber);}
|
| 73 |
+
.level-avanzado{background:var(--neg-bg);color:var(--neg);}
|
| 74 |
+
.level-extremo{background:var(--purple-bg);color:var(--purple);}
|
| 75 |
+
.ex-desc{font-size:14px;line-height:1.5;margin:6px 0 12px;}
|
| 76 |
+
.ex-question{font-size:14px;font-weight:700;margin:10px 0 12px;padding:9px 12px;
|
| 77 |
+
background:#F2F0EA;border-left:3px solid var(--red);border-radius:4px;}
|
| 78 |
+
|
| 79 |
+
/* ---------- 3D net / cube (signature element) ---------- */
|
| 80 |
+
.net-scene{position:relative;height:230px;margin:10px 0 6px;perspective:900px;
|
| 81 |
+
display:flex;align-items:center;justify-content:center;}
|
| 82 |
+
.net-inner{position:relative;transform-style:preserve-3d;transition:transform .8s;}
|
| 83 |
+
.net-face{position:absolute;transform-style:preserve-3d;transition:transform .9s;
|
| 84 |
+
display:flex;align-items:center;justify-content:center;font-family:"SF Mono",monospace;
|
| 85 |
+
font-weight:800;font-size:18px;border:2px solid #fff;color:#fff;border-radius:3px;}
|
| 86 |
+
.net-inner.folded .edge-top{transform:rotateX(90deg);}
|
| 87 |
+
.net-inner.folded .edge-bottom{transform:rotateX(-90deg);}
|
| 88 |
+
.net-inner.folded .edge-left{transform:rotateY(-90deg);}
|
| 89 |
+
.net-inner.folded .edge-right{transform:rotateY(90deg);}
|
| 90 |
+
|
| 91 |
+
.fold-controls{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin:10px 0;}
|
| 92 |
+
button.btn{border:none;border-radius:6px;padding:9px 14px;font-size:13px;font-weight:700;cursor:pointer;}
|
| 93 |
+
.btn-primary{background:var(--ink);color:#fff;}
|
| 94 |
+
.btn-secondary{background:#fff;color:var(--ink);border:1px solid var(--line);}
|
| 95 |
+
.btn-master{background:#fff;color:var(--muted);border:1px solid var(--line);}
|
| 96 |
+
.btn-master.done{background:var(--pos-bg);color:var(--pos);border-color:var(--pos);}
|
| 97 |
+
.rotate-row{display:flex;gap:6px;justify-content:center;margin-top:6px;}
|
| 98 |
+
.rotate-row button{padding:6px 10px;font-size:12px;}
|
| 99 |
+
|
| 100 |
+
/* ---------- Cube for rotation exercise ---------- */
|
| 101 |
+
.cube-scene{perspective:900px;height:220px;display:flex;align-items:center;justify-content:center;margin:10px 0;}
|
| 102 |
+
.cube-view{transform-style:preserve-3d;transform:rotateX(-18deg) rotateY(-28deg);}
|
| 103 |
+
.cube{position:relative;width:90px;height:90px;transform-style:preserve-3d;transition:transform 1s;margin:auto;}
|
| 104 |
+
.cube-face{position:absolute;width:90px;height:90px;display:flex;align-items:center;justify-content:center;
|
| 105 |
+
font-family:"SF Mono",monospace;font-weight:800;font-size:22px;border:2px solid #fff;border-radius:4px;color:#fff;}
|
| 106 |
+
.cf-front{background:#1F7A4C;transform:translateZ(45px);}
|
| 107 |
+
.cf-back{background:#6B6864;transform:rotateY(180deg) translateZ(45px);}
|
| 108 |
+
.cf-right{background:#D8232A;transform:rotateY(90deg) translateZ(45px);}
|
| 109 |
+
.cf-left{background:#6B6864;transform:rotateY(-90deg) translateZ(45px);}
|
| 110 |
+
.cf-top{background:#B8860B;transform:rotateX(90deg) translateZ(45px);}
|
| 111 |
+
.cf-bottom{background:#6B6864;transform:rotateX(-90deg) translateZ(45px);}
|
| 112 |
+
|
| 113 |
+
.front-readout{text-align:center;font-size:14px;font-weight:700;margin-top:8px;padding:8px;
|
| 114 |
+
background:#F4F8F6;border:1px solid #CFE4D8;border-radius:8px;color:var(--pos);}
|
| 115 |
+
.step-progress{text-align:center;font-size:12px;color:var(--muted);margin-top:6px;font-family:"SF Mono",monospace;}
|
| 116 |
+
|
| 117 |
+
.quiz-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0;
|
| 118 |
+
border-bottom:1px solid var(--line);font-size:14px;}
|
| 119 |
+
.quiz-row:last-child{border-bottom:none;}
|
| 120 |
+
.quiz-tag{font-size:11px;font-weight:800;padding:3px 8px;border-radius:20px;}
|
| 121 |
+
.tag-imposible{background:var(--neg-bg);color:var(--neg);}
|
| 122 |
+
.tag-posible{background:var(--pos-bg);color:var(--pos);}
|
| 123 |
+
|
| 124 |
+
.ex-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px;}
|
| 125 |
+
|
| 126 |
+
.progress-hero{text-align:center;padding:20px 10px 8px;}
|
| 127 |
+
.progress-num{font-size:46px;font-weight:800;font-family:"SF Mono",ui-monospace,monospace;}
|
| 128 |
+
.progress-label{font-size:13px;color:var(--muted);margin-top:2px;}
|
| 129 |
+
.bar-track{height:8px;background:var(--line);border-radius:6px;overflow:hidden;margin:16px 0 22px;}
|
| 130 |
+
.bar-fill{height:100%;background:var(--red);transition:width .3s ease;}
|
| 131 |
+
.prog-row{display:flex;justify-content:space-between;align-items:center;padding:11px 4px;border-bottom:1px solid var(--line);font-size:14px;}
|
| 132 |
+
.prog-row:last-child{border-bottom:none;}
|
| 133 |
+
.prog-check{width:18px;height:18px;border-radius:50%;flex-shrink:0;}
|
| 134 |
+
.prog-check.on{background:var(--pos);}
|
| 135 |
+
.prog-check.off{background:var(--line);}
|
| 136 |
+
.reset-link{display:block;text-align:center;margin-top:20px;font-size:13px;color:var(--muted);
|
| 137 |
+
text-decoration:underline;cursor:pointer;background:none;border:none;}
|
| 138 |
+
.loading-note{font-size:12px;color:var(--muted);text-align:center;padding:10px;}
|
| 139 |
+
</style>
|
| 140 |
+
</head>
|
| 141 |
+
<body>
|
| 142 |
+
|
| 143 |
+
<header>
|
| 144 |
+
<div class="eyebrow">ICT-Eignungstest · Kognitive Fähigkeiten</div>
|
| 145 |
+
<h1>Vorstellungsvermögen</h1>
|
| 146 |
+
<p class="sub">Capacidad espacial — cubos que se pliegan y giran de verdad frente a ti, no en tu imaginación. Mira, pliega, rota, verifica.</p>
|
| 147 |
+
<div class="rail"></div>
|
| 148 |
+
</header>
|
| 149 |
+
|
| 150 |
+
<nav class="tabs">
|
| 151 |
+
<button class="tab-btn active" data-tab="teoria">Teoría</button>
|
| 152 |
+
<button class="tab-btn" data-tab="practicar">Practicar</button>
|
| 153 |
+
<button class="tab-btn" data-tab="progreso">Progreso</button>
|
| 154 |
+
</nav>
|
| 155 |
+
|
| 156 |
+
<main>
|
| 157 |
+
|
| 158 |
+
<section id="teoria" class="active">
|
| 159 |
+
|
| 160 |
+
<h2 class="section-title">¿Qué evalúa exactamente?</h2>
|
| 161 |
+
<div class="card">
|
| 162 |
+
<p>Tu <b>memoria de trabajo espacial</b> — no geometría con fórmulas, sino manipulación mental de objetos. Dos tipos de problema: <b>Abwicklungen</b> (despliegues: qué cubo 3D corresponde a una red plana) y <b>Würfel-Rotationen</b> (identificar el mismo cubo tras varias rotaciones).</p>
|
| 163 |
+
</div>
|
| 164 |
+
|
| 165 |
+
<h2 class="section-title">Las 3 reglas de descarte rápido</h2>
|
| 166 |
+
<div class="card">
|
| 167 |
+
<div class="rule-box">
|
| 168 |
+
<p><b>1. Regla de las caras opuestas:</b> en una red, las caras separadas por exactamente un cuadrado intermedio nunca se ven juntas en el cubo armado. Si una opción muestra dos caras opuestas visibles a la vez, queda eliminada de inmediato.</p>
|
| 169 |
+
</div>
|
| 170 |
+
<div class="rule-box">
|
| 171 |
+
<p><b>2. Orientación de bordes:</b> los símbolos tienen dirección. Si algo apunta hacia una arista en el plano, debe seguir apuntando hacia esa misma arista al doblar.</p>
|
| 172 |
+
</div>
|
| 173 |
+
<div class="rule-box">
|
| 174 |
+
<p><b>3. Regla del sentido horario:</b> toma tres caras que compartan una esquina. El orden de los símbolos en sentido de las agujas del reloj debe mantenerse igual al plegar.</p>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<h2 class="section-title">⚠️ La trampa del bloque 2×2 (un error real que corregimos aquí)</h2>
|
| 179 |
+
<div class="card">
|
| 180 |
+
<div class="fix-box">
|
| 181 |
+
<p><b>Dato importante:</b> si en una red plana ves 4 cuadrados formando un bloque 2×2 completo (2 filas × 2 columnas, todos conectados entre sí), esa red es <b>geométricamente imposible</b> — no puede formar un cubo real. La razón: un cubo solo tiene 3 caras encontrándose en cada esquina, nunca 4. Un bloque 2×2 obligaría a 4 caras a encontrarse en un mismo punto, lo cual viola la estructura del cubo.</p>
|
| 182 |
+
<p style="margin:0">Si te encuentras con una red así en un ejercicio de práctica (de cualquier fuente), descártala — es un error de diseño, no un ejercicio válido. Usamos esta corrección en el Ejercicio 3 de abajo.</p>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
</section>
|
| 187 |
+
|
| 188 |
+
<section id="practicar">
|
| 189 |
+
<h2 class="section-title">3 ejercicios — pliega y gira cubos reales</h2>
|
| 190 |
+
<p style="font-size:13px;color:var(--muted);margin-top:-6px;margin-bottom:16px;">
|
| 191 |
+
Usa «Doblar cubo» para ver la red convertirse en un cubo 3D real, y los botones de rotación para explorarlo desde todos los ángulos.
|
| 192 |
+
</p>
|
| 193 |
+
<div id="exercise-list"></div>
|
| 194 |
+
</section>
|
| 195 |
+
|
| 196 |
+
<section id="progreso">
|
| 197 |
+
<div class="progress-hero">
|
| 198 |
+
<div class="progress-num" id="prog-count">0 / 6</div>
|
| 199 |
+
<div class="progress-label">ejercicios dominados</div>
|
| 200 |
+
<div class="bar-track"><div class="bar-fill" id="prog-bar" style="width:0%"></div></div>
|
| 201 |
+
</div>
|
| 202 |
+
<h2 class="section-title">Detalle por ejercicio</h2>
|
| 203 |
+
<div class="card" id="prog-list"></div>
|
| 204 |
+
<button class="reset-link" id="reset-btn">Reiniciar todo mi progreso</button>
|
| 205 |
+
<div class="loading-note" id="storage-note"></div>
|
| 206 |
+
</section>
|
| 207 |
+
|
| 208 |
+
</main>
|
| 209 |
+
|
| 210 |
+
<script>
|
| 211 |
+
/* ================= NET DEFINITIONS ================= */
|
| 212 |
+
// Ejercicio 1: cruz clásica (verificada como red válida real)
|
| 213 |
+
const netCross = [
|
| 214 |
+
{id:"F2", label:"●", parent:null, edge:null, color:"#1F7A4C"},
|
| 215 |
+
{id:"F1", label:"↑", parent:"F2", edge:"top", color:"#D8232A"},
|
| 216 |
+
{id:"F3", label:"□", parent:"F2", edge:"bottom", color:"#B8860B"},
|
| 217 |
+
{id:"F5", label:"▲", parent:"F2", edge:"left", color:"#7A2A63"},
|
| 218 |
+
{id:"F6", label:"//", parent:"F2", edge:"right", color:"#2E6DA4"},
|
| 219 |
+
{id:"F4", label:"+", parent:"F3", edge:"bottom", color:"#A63A22"}
|
| 220 |
+
];
|
| 221 |
+
|
| 222 |
+
// Ejercicio 3: versión CORREGIDA (red válida, un solo cuadrado de solape, no bloque 2x2)
|
| 223 |
+
const netStaircase = [
|
| 224 |
+
{id:"C", label:"X", parent:null, edge:null, color:"#7A2A63"},
|
| 225 |
+
{id:"B", label:"★", parent:"C", edge:"left", color:"#D8232A"},
|
| 226 |
+
{id:"A", label:"╱", parent:"B", edge:"left", color:"#B8860B"},
|
| 227 |
+
{id:"D", label:"□", parent:"C", edge:"bottom", color:"#1F7A4C"},
|
| 228 |
+
{id:"E", label:"7", parent:"D", edge:"right", color:"#2E6DA4"},
|
| 229 |
+
{id:"F", label:"~", parent:"E", edge:"right", color:"#6B6864"}
|
| 230 |
+
];
|
| 231 |
+
|
| 232 |
+
// Red en T ancha — pares verificados por simulador de plegado: A↔C, B↔D, E↔F
|
| 233 |
+
const netT = [
|
| 234 |
+
{id:"B", label:"●", parent:null, edge:null, color:"#1F7A4C"},
|
| 235 |
+
{id:"A", label:"▲", parent:"B", edge:"left", color:"#D8232A"},
|
| 236 |
+
{id:"C", label:"■", parent:"B", edge:"right", color:"#B8860B"},
|
| 237 |
+
{id:"D", label:"✚", parent:"C", edge:"right", color:"#7A2A63"},
|
| 238 |
+
{id:"E", label:"↑", parent:"B", edge:"top", color:"#2E6DA4"},
|
| 239 |
+
{id:"F", label:"◇", parent:"B", edge:"bottom", color:"#A63A22"}
|
| 240 |
+
];
|
| 241 |
+
|
| 242 |
+
// Red en Z — pares verificados por simulador de plegado: B↔E, A↔D, C↔F
|
| 243 |
+
const netZ = [
|
| 244 |
+
{id:"B", label:"5", parent:null, edge:null, color:"#2E6DA4"},
|
| 245 |
+
{id:"A", label:"K", parent:"B", edge:"left", color:"#D8232A"},
|
| 246 |
+
{id:"C", label:"Ω", parent:"B", edge:"bottom", color:"#1F7A4C"},
|
| 247 |
+
{id:"D", label:"✦", parent:"C", edge:"right", color:"#B8860B"},
|
| 248 |
+
{id:"E", label:"♪", parent:"D", edge:"bottom", color:"#7A2A63"},
|
| 249 |
+
{id:"F", label:"Z", parent:"E", edge:"right", color:"#6B6864"}
|
| 250 |
+
];
|
| 251 |
+
|
| 252 |
+
function buildNet(container, faces, size){
|
| 253 |
+
container.innerHTML = "";
|
| 254 |
+
const scene = document.createElement("div");
|
| 255 |
+
scene.className = "net-scene";
|
| 256 |
+
const inner = document.createElement("div");
|
| 257 |
+
inner.className = "net-inner";
|
| 258 |
+
scene.appendChild(inner);
|
| 259 |
+
container.appendChild(scene);
|
| 260 |
+
|
| 261 |
+
const root = faces.find(f => f.parent === null);
|
| 262 |
+
const rootEl = document.createElement("div");
|
| 263 |
+
rootEl.className = "net-face";
|
| 264 |
+
rootEl.style.width = size+"px"; rootEl.style.height = size+"px";
|
| 265 |
+
rootEl.style.top = "calc(50% - "+(size/2)+"px)";
|
| 266 |
+
rootEl.style.left = "calc(50% - "+(size/2)+"px)";
|
| 267 |
+
rootEl.style.background = root.color;
|
| 268 |
+
rootEl.textContent = root.label;
|
| 269 |
+
inner.appendChild(rootEl);
|
| 270 |
+
|
| 271 |
+
function attach(parentId, parentEl){
|
| 272 |
+
faces.filter(f => f.parent === parentId).forEach(f => {
|
| 273 |
+
const el = document.createElement("div");
|
| 274 |
+
el.className = "net-face edge-"+f.edge;
|
| 275 |
+
el.style.width = size+"px"; el.style.height = size+"px";
|
| 276 |
+
el.style.background = f.color;
|
| 277 |
+
el.textContent = f.label;
|
| 278 |
+
if(f.edge === "top"){ el.style.top = -size+"px"; el.style.left = "0"; el.style.transformOrigin = "50% 100%"; }
|
| 279 |
+
if(f.edge === "bottom"){ el.style.top = size+"px"; el.style.left = "0"; el.style.transformOrigin = "50% 0%"; }
|
| 280 |
+
if(f.edge === "left"){ el.style.top = "0"; el.style.left = -size+"px"; el.style.transformOrigin = "100% 50%"; }
|
| 281 |
+
if(f.edge === "right"){ el.style.top = "0"; el.style.left = size+"px"; el.style.transformOrigin = "0% 50%"; }
|
| 282 |
+
parentEl.appendChild(el);
|
| 283 |
+
attach(f.id, el);
|
| 284 |
+
});
|
| 285 |
+
}
|
| 286 |
+
attach(root.id, rootEl);
|
| 287 |
+
return inner;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
function wireFoldControls(container, inner){
|
| 291 |
+
const controls = document.createElement("div");
|
| 292 |
+
controls.className = "fold-controls";
|
| 293 |
+
controls.innerHTML = `
|
| 294 |
+
<button class="btn btn-primary fold-toggle">Doblar cubo</button>
|
| 295 |
+
<div class="rotate-row">
|
| 296 |
+
<button class="btn btn-secondary rot" data-ax="x" data-amt="-15">⟲ arriba/abajo</button>
|
| 297 |
+
<button class="btn btn-secondary rot" data-ax="x" data-amt="15">⟳ arriba/abajo</button>
|
| 298 |
+
<button class="btn btn-secondary rot" data-ax="y" data-amt="-15">⟲ izq/der</button>
|
| 299 |
+
<button class="btn btn-secondary rot" data-ax="y" data-amt="15">⟳ izq/der</button>
|
| 300 |
+
</div>
|
| 301 |
+
`;
|
| 302 |
+
container.appendChild(controls);
|
| 303 |
+
|
| 304 |
+
let folded = false, vx = -15, vy = 20;
|
| 305 |
+
const applyView = () => { inner.style.transform = `rotateX(${vx}deg) rotateY(${vy}deg)`; };
|
| 306 |
+
applyView();
|
| 307 |
+
|
| 308 |
+
controls.querySelector(".fold-toggle").addEventListener("click", (e) => {
|
| 309 |
+
folded = !folded;
|
| 310 |
+
inner.classList.toggle("folded", folded);
|
| 311 |
+
e.target.textContent = folded ? "Desplegar" : "Doblar cubo";
|
| 312 |
+
});
|
| 313 |
+
controls.querySelectorAll(".rot").forEach(btn => {
|
| 314 |
+
btn.addEventListener("click", () => {
|
| 315 |
+
const amt = parseInt(btn.dataset.amt);
|
| 316 |
+
if(btn.dataset.ax === "x") vx += amt; else vy += amt;
|
| 317 |
+
applyView();
|
| 318 |
+
});
|
| 319 |
+
});
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
/* ================= ROTATION EXERCISE (Ejercicio 2) ================= */
|
| 323 |
+
// Verificado con álgebra de vectores: Paso1=rotateY(90) Paso2=rotateX(90) Paso3=rotateZ(-90)
|
| 324 |
+
// Front resultante por paso: 0:F(frontal original) 1:cara izq.(sin etiqueta) 2:"3"(techo) 3:"3"(sin cambio de cara, solo gira en el plano)
|
| 325 |
+
const rotationSteps = [
|
| 326 |
+
{ transform:"rotateY(0deg)", front:"F (posición inicial)" },
|
| 327 |
+
{ transform:"rotateY(90deg)", front:"cara Izquierda (sin símbolo) — «la izquierda pasa al frente»" },
|
| 328 |
+
{ transform:"rotateX(90deg) rotateY(90deg)", front:"3 (el Techo cae al frente)" },
|
| 329 |
+
{ transform:"rotateZ(-90deg) rotateX(90deg) rotateY(90deg)", front:"3 — sigue al frente, pero mira cómo giró en el dibujo" }
|
| 330 |
+
];
|
| 331 |
+
|
| 332 |
+
function buildRotationCube(container){
|
| 333 |
+
container.innerHTML = "";
|
| 334 |
+
const scene = document.createElement("div");
|
| 335 |
+
scene.className = "cube-scene";
|
| 336 |
+
const view = document.createElement("div");
|
| 337 |
+
view.className = "cube-view";
|
| 338 |
+
const cube = document.createElement("div");
|
| 339 |
+
cube.className = "cube";
|
| 340 |
+
cube.id = "rot-cube";
|
| 341 |
+
cube.innerHTML = `
|
| 342 |
+
<div class="cube-face cf-front">F</div>
|
| 343 |
+
<div class="cube-face cf-back">?</div>
|
| 344 |
+
<div class="cube-face cf-right">•</div>
|
| 345 |
+
<div class="cube-face cf-left">?</div>
|
| 346 |
+
<div class="cube-face cf-top">3</div>
|
| 347 |
+
<div class="cube-face cf-bottom">?</div>
|
| 348 |
+
`;
|
| 349 |
+
view.appendChild(cube);
|
| 350 |
+
scene.appendChild(view);
|
| 351 |
+
container.appendChild(scene);
|
| 352 |
+
|
| 353 |
+
const readout = document.createElement("div");
|
| 354 |
+
readout.className = "front-readout";
|
| 355 |
+
readout.id = "rot-readout";
|
| 356 |
+
const stepProg = document.createElement("div");
|
| 357 |
+
stepProg.className = "step-progress";
|
| 358 |
+
stepProg.id = "rot-step-progress";
|
| 359 |
+
container.appendChild(readout);
|
| 360 |
+
container.appendChild(stepProg);
|
| 361 |
+
|
| 362 |
+
const controls = document.createElement("div");
|
| 363 |
+
controls.className = "fold-controls";
|
| 364 |
+
controls.innerHTML = `
|
| 365 |
+
<button class="btn btn-secondary" id="rot-prev">◀ Paso anterior</button>
|
| 366 |
+
<button class="btn btn-primary" id="rot-next">Aplicar siguiente rotación ▶</button>
|
| 367 |
+
<button class="btn btn-secondary" id="rot-reset">↺ Reiniciar</button>
|
| 368 |
+
`;
|
| 369 |
+
container.appendChild(controls);
|
| 370 |
+
|
| 371 |
+
let step = 0;
|
| 372 |
+
function render(){
|
| 373 |
+
cube.style.transform = rotationSteps[step].transform;
|
| 374 |
+
readout.textContent = `Cara frontal actual: ${rotationSteps[step].front}`;
|
| 375 |
+
stepProg.textContent = `Paso ${step} / 3`;
|
| 376 |
+
container.querySelector("#rot-next").disabled = step >= 3;
|
| 377 |
+
container.querySelector("#rot-prev").disabled = step <= 0;
|
| 378 |
+
}
|
| 379 |
+
container.querySelector("#rot-next").addEventListener("click", () => { if(step<3){step++; render();} });
|
| 380 |
+
container.querySelector("#rot-prev").addEventListener("click", () => { if(step>0){step--; render();} });
|
| 381 |
+
container.querySelector("#rot-reset").addEventListener("click", () => { step=0; render(); });
|
| 382 |
+
render();
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
/* ================= EXERCISES META ================= */
|
| 386 |
+
const exercisesMeta = [
|
| 387 |
+
{
|
| 388 |
+
level:"medio",
|
| 389 |
+
title:"El filtro de la cruz estándar",
|
| 390 |
+
desc:"Red en forma de cruz vertical: tira de 4 caras (↑, ●, □, +) con dos brazos laterales (▲ y //) unidos a la cara ●.",
|
| 391 |
+
question:"¿Cuáles combinaciones de 3 caras visibles simultáneamente son posibles?",
|
| 392 |
+
quiz:[
|
| 393 |
+
{label:"A) ↑, □ y ▲ al mismo tiempo", tag:"imposible", why:"↑ y □ son opuestas (2 apart en la tira de 4)."},
|
| 394 |
+
{label:"B) ●, + y // al mismo tiempo", tag:"imposible", why:"● y + son opuestas (2 apart en la tira de 4)."},
|
| 395 |
+
{label:"C) ▲, ● y // al mismo tiempo", tag:"imposible", why:"▲ y // son los dos brazos — siempre opuestos entre sí."}
|
| 396 |
+
],
|
| 397 |
+
netType:"cross"
|
| 398 |
+
},
|
| 399 |
+
{
|
| 400 |
+
level:"avanzado",
|
| 401 |
+
title:"El desafío de los tres ejes",
|
| 402 |
+
desc:"Cubo inicial: Frontal=F, Techo=3, Derecha=•. Aplica 3 rotaciones: (1) 90° a la derecha sobre eje vertical, (2) 90° hacia adelante (vuelco), (3) 90° en sentido horario sobre el eje frontal actual.",
|
| 403 |
+
question:"¿Qué cara termina al frente y cómo se orienta?",
|
| 404 |
+
netType:"rotation"
|
| 405 |
+
},
|
| 406 |
+
{
|
| 407 |
+
level:"extremo",
|
| 408 |
+
title:"La red escalonada (versión corregida)",
|
| 409 |
+
desc:"La red original de este ejercicio tenía un bloque 2×2 imposible. Esta es la versión válida: fila superior X-★-╱ desplazada respecto a fila inferior □-7-~, con un solo cuadrado de solape.",
|
| 410 |
+
question:"Pliega el cubo y gíralo para descubrir tú mismo qué caras terminan opuestas entre sí — esa exploración práctica es exactamente la habilidad que mide el examen.",
|
| 411 |
+
netType:"staircase"
|
| 412 |
+
},
|
| 413 |
+
{
|
| 414 |
+
level:"avanzado",
|
| 415 |
+
title:"La red en T",
|
| 416 |
+
desc:"Fila horizontal de 4 caras (▲-●-■-✚) con dos caras verticales (↑ arriba y ◇ abajo) unidas a la cara ●.",
|
| 417 |
+
question:"Antes de plegar, predice los 3 pares de caras opuestas. Luego pliega y verifica.",
|
| 418 |
+
quiz:[
|
| 419 |
+
{label:"A) ▲ y ■ son opuestas", tag:"posible", why:"Correcto: en la tira horizontal ▲-●-■-✚, las posiciones 1 y 3 (dos de separación) son opuestas. Verificado por simulador de plegado."},
|
| 420 |
+
{label:"B) ● y ✚ son opuestas", tag:"posible", why:"Correcto: posiciones 2 y 4 de la tira — también dos de separación."},
|
| 421 |
+
{label:"C) ↑ y ◇ son opuestas", tag:"posible", why:"Correcto: ↑-●-◇ forman una tira vertical de 3 con ● en medio → extremos opuestos."},
|
| 422 |
+
{label:"D) ▲ y ✚ son opuestas", tag:"imposible", why:"Falso: en la tira de 4, las posiciones 1 y 4 quedan ADYACENTES al cerrar el anillo del cubo, no opuestas. La regla es «2 de separación», no «los extremos»."}
|
| 423 |
+
],
|
| 424 |
+
netType:"t"
|
| 425 |
+
},
|
| 426 |
+
{
|
| 427 |
+
level:"extremo",
|
| 428 |
+
title:"La red en Z",
|
| 429 |
+
desc:"Red escalonada doble: K-5 arriba, Ω-✦ en medio, ♪-Z abajo — cada fila desplazada un cuadrado respecto a la anterior.",
|
| 430 |
+
question:"Aquí NO hay ninguna tira de 3 ni de 4 alineada: la regla «2 de separación» no se puede aplicar directamente. Predice los pares y verifica plegando.",
|
| 431 |
+
quiz:[
|
| 432 |
+
{label:"A) 5 y ♪ son opuestas", tag:"posible", why:"Correcto (verificado por simulador). En redes escalonadas los pares saltan en diagonal — por eso este tipo es nivel extremo."},
|
| 433 |
+
{label:"B) K y ✦ son opuestas", tag:"posible", why:"Correcto: K y ✦ están en diagonal a dos pasos por el zigzag."},
|
| 434 |
+
{label:"C) Ω y Z son opuestas", tag:"posible", why:"Correcto: el tercer par. Nota el patrón de la Z: cada cara es opuesta a la que está «a dos pasos de zigzag» de distancia."},
|
| 435 |
+
{label:"D) 5 y Ω son opuestas", tag:"imposible", why:"Falso: comparten arista en la red → siempre serán ADYACENTES en el cubo. Dos caras que se tocan en la red jamás pueden ser opuestas."}
|
| 436 |
+
],
|
| 437 |
+
netType:"z"
|
| 438 |
+
},
|
| 439 |
+
{
|
| 440 |
+
level:"extremo",
|
| 441 |
+
title:"Detectar la red imposible",
|
| 442 |
+
desc:"Un examen muestra esta red: dos filas de 3 caras formando un bloque rectangular 2×3 (A-B-C arriba, D-E-F abajo, todas pegadas).",
|
| 443 |
+
question:"¿Puede esta red plegarse en un cubo? Justifica sin plegar.",
|
| 444 |
+
quiz:[
|
| 445 |
+
{label:"El bloque 2×3 completo, ¿forma un cubo?", tag:"imposible", why:"Al plegar, las caras D, E y F terminan superpuestas en la misma posición (verificado por simulador: 3 caras colapsan en una sola normal). Regla rápida: una red válida de cubo nunca contiene un rectángulo 2×3 lleno, ni un cuadrado 2×2 lleno — ambos obligan a superposiciones. De las 35 formas de unir 6 cuadrados (hexominós), solo 11 son redes válidas de cubo."}
|
| 446 |
+
],
|
| 447 |
+
netType:"none"
|
| 448 |
+
}
|
| 449 |
+
];
|
| 450 |
+
|
| 451 |
+
let mastered = {};
|
| 452 |
+
|
| 453 |
+
function renderExercises(){
|
| 454 |
+
const list = document.getElementById("exercise-list");
|
| 455 |
+
list.innerHTML = exercisesMeta.map((ex,i) => {
|
| 456 |
+
const isMastered = !!mastered[i];
|
| 457 |
+
let quizHtml = "";
|
| 458 |
+
if(ex.quiz){
|
| 459 |
+
quizHtml = `<div class="card" style="margin-top:10px;padding:12px 14px;" id="quiz-${i}">` +
|
| 460 |
+
ex.quiz.map((q,qi) => `
|
| 461 |
+
<div class="quiz-row">
|
| 462 |
+
<span>${q.label}</span>
|
| 463 |
+
<span class="quiz-tag tag-${q.tag} quiz-answer" data-i="${i}" data-qi="${qi}" style="display:none">${q.tag === 'imposible' ? 'IMPOSIBLE' : 'POSIBLE'}</span>
|
| 464 |
+
</div>
|
| 465 |
+
<p class="quiz-why" data-i="${i}" data-qi="${qi}" style="display:none;font-size:12px;color:var(--muted);margin:-4px 0 6px;">${q.why}</p>
|
| 466 |
+
`).join("") +
|
| 467 |
+
`<button class="btn btn-secondary reveal-quiz" data-idx="${i}" style="margin-top:8px;">Revelar respuestas</button>
|
| 468 |
+
</div>`;
|
| 469 |
+
}
|
| 470 |
+
return `
|
| 471 |
+
<div class="ex-card" data-idx="${i}">
|
| 472 |
+
<div class="ex-head">
|
| 473 |
+
<span class="ex-num">Ejercicio ${i+1} / ${exercisesMeta.length} — ${ex.title}</span>
|
| 474 |
+
<span class="level-tag level-${ex.level}">${ex.level.charAt(0).toUpperCase()+ex.level.slice(1)}</span>
|
| 475 |
+
</div>
|
| 476 |
+
<p class="ex-desc">${ex.desc}</p>
|
| 477 |
+
<p class="ex-question">${ex.question}</p>
|
| 478 |
+
<div id="net-container-${i}"></div>
|
| 479 |
+
${quizHtml}
|
| 480 |
+
<div class="ex-actions">
|
| 481 |
+
<button class="btn btn-master ${isMastered ? 'done' : ''}" data-idx="${i}">
|
| 482 |
+
${isMastered ? '✓ Dominado' : 'Marcar dominado'}
|
| 483 |
+
</button>
|
| 484 |
+
</div>
|
| 485 |
+
</div>`;
|
| 486 |
+
}).join("");
|
| 487 |
+
|
| 488 |
+
exercisesMeta.forEach((ex,i) => {
|
| 489 |
+
const c = document.getElementById(`net-container-${i}`);
|
| 490 |
+
if(ex.netType === "cross"){
|
| 491 |
+
const inner = buildNet(c, netCross, 46);
|
| 492 |
+
wireFoldControls(c, inner);
|
| 493 |
+
} else if(ex.netType === "staircase"){
|
| 494 |
+
const inner = buildNet(c, netStaircase, 46);
|
| 495 |
+
wireFoldControls(c, inner);
|
| 496 |
+
} else if(ex.netType === "t"){
|
| 497 |
+
const inner = buildNet(c, netT, 46);
|
| 498 |
+
wireFoldControls(c, inner);
|
| 499 |
+
} else if(ex.netType === "z"){
|
| 500 |
+
const inner = buildNet(c, netZ, 46);
|
| 501 |
+
wireFoldControls(c, inner);
|
| 502 |
+
} else if(ex.netType === "rotation"){
|
| 503 |
+
buildRotationCube(c);
|
| 504 |
+
}
|
| 505 |
+
});
|
| 506 |
+
|
| 507 |
+
list.querySelectorAll(".reveal-quiz").forEach(btn => {
|
| 508 |
+
btn.addEventListener("click", () => {
|
| 509 |
+
const idx = btn.dataset.idx;
|
| 510 |
+
document.querySelectorAll(`.quiz-answer[data-i="${idx}"], .quiz-why[data-i="${idx}"]`).forEach(el => {
|
| 511 |
+
el.style.display = el.style.display === "none" ? (el.classList.contains("quiz-why") ? "block" : "inline-block") : "none";
|
| 512 |
+
});
|
| 513 |
+
});
|
| 514 |
+
});
|
| 515 |
+
|
| 516 |
+
list.querySelectorAll(".btn-master").forEach(btn => {
|
| 517 |
+
btn.addEventListener("click", () => toggleMastered(parseInt(btn.dataset.idx)));
|
| 518 |
+
});
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
async function toggleMastered(idx){
|
| 522 |
+
mastered[idx] = !mastered[idx];
|
| 523 |
+
await saveState();
|
| 524 |
+
renderExercises();
|
| 525 |
+
renderProgress();
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
function renderProgress(){
|
| 529 |
+
const count = Object.values(mastered).filter(Boolean).length;
|
| 530 |
+
document.getElementById("prog-count").textContent = `${count} / ${exercisesMeta.length}`;
|
| 531 |
+
document.getElementById("prog-bar").style.width = `${(count/exercisesMeta.length)*100}%`;
|
| 532 |
+
const list = document.getElementById("prog-list");
|
| 533 |
+
list.innerHTML = exercisesMeta.map((ex,i) => `
|
| 534 |
+
<div class="prog-row">
|
| 535 |
+
<span>Ejercicio ${i+1} · ${ex.title}</span>
|
| 536 |
+
<span class="prog-check ${mastered[i] ? 'on' : 'off'}"></span>
|
| 537 |
+
</div>
|
| 538 |
+
`).join("");
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
async function loadState(){
|
| 542 |
+
try{
|
| 543 |
+
const res = await window.storage.get("vorstellungsvermoegen-progress");
|
| 544 |
+
if(res && res.value){
|
| 545 |
+
const data = JSON.parse(res.value);
|
| 546 |
+
mastered = data.mastered || {};
|
| 547 |
+
}
|
| 548 |
+
}catch(e){ /* no data yet */ }
|
| 549 |
+
}
|
| 550 |
+
async function saveState(){
|
| 551 |
+
try{
|
| 552 |
+
await window.storage.set("vorstellungsvermoegen-progress", JSON.stringify({ mastered }));
|
| 553 |
+
}catch(e){
|
| 554 |
+
const note = document.getElementById("storage-note");
|
| 555 |
+
if(note) note.textContent = "No se pudo guardar el progreso (modo sin conexión).";
|
| 556 |
+
}
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
document.getElementById("reset-btn").addEventListener("click", async () => {
|
| 560 |
+
if(!confirm("¿Reiniciar todo tu progreso? Esta acción no se puede deshacer.")) return;
|
| 561 |
+
mastered = {};
|
| 562 |
+
await saveState();
|
| 563 |
+
renderExercises(); renderProgress();
|
| 564 |
+
});
|
| 565 |
+
|
| 566 |
+
document.querySelectorAll(".tab-btn").forEach(btn => {
|
| 567 |
+
btn.addEventListener("click", () => {
|
| 568 |
+
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
| 569 |
+
document.querySelectorAll("main > section").forEach(s => s.classList.remove("active"));
|
| 570 |
+
btn.classList.add("active");
|
| 571 |
+
document.getElementById(btn.dataset.tab).classList.add("active");
|
| 572 |
+
});
|
| 573 |
+
});
|
| 574 |
+
|
| 575 |
+
(async function init(){
|
| 576 |
+
await loadState();
|
| 577 |
+
renderExercises();
|
| 578 |
+
renderProgress();
|
| 579 |
+
})();
|
| 580 |
+
</script>
|
| 581 |
+
</body>
|
| 582 |
+
</html>
|
apps/zahlenreihen-app.html
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="es">
|
| 3 |
+
<head>
|
| 4 |
+
<script>
|
| 5 |
+
/* BrainBit storage shim: sustituye window.storage (entorno artifacts de Claude.ai)
|
| 6 |
+
por localStorage con la misma interfaz async {value}. Notifica el progreso al parent. */
|
| 7 |
+
window.storage = {
|
| 8 |
+
async get(key){ const v = localStorage.getItem(key); return v === null ? null : { key, value: v }; },
|
| 9 |
+
async set(key, value){
|
| 10 |
+
localStorage.setItem(key, String(value));
|
| 11 |
+
try { if (window.parent !== window) window.parent.postMessage({ source: "brainbit-app", type: "progress", key: key, value: String(value) }, "*"); } catch(e){}
|
| 12 |
+
}
|
| 13 |
+
};
|
| 14 |
+
window.addEventListener("load", () => {
|
| 15 |
+
try {
|
| 16 |
+
if (window.parent === window) return;
|
| 17 |
+
for (let i = 0; i < localStorage.length; i++) {
|
| 18 |
+
const key = localStorage.key(i);
|
| 19 |
+
if (key && key.endsWith("-progress")) {
|
| 20 |
+
window.parent.postMessage({ source: "brainbit-app", type: "progress", key: key, value: localStorage.getItem(key) }, "*");
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
} catch(e){}
|
| 24 |
+
});
|
| 25 |
+
</script>
|
| 26 |
+
|
| 27 |
+
<meta charset="UTF-8">
|
| 28 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 29 |
+
<title>Zahlenreihen — ICT-Eignungstest</title>
|
| 30 |
+
<style>
|
| 31 |
+
:root{
|
| 32 |
+
--bg:#F7F6F3; --panel:#FFFFFF; --ink:#1C1C1C; --muted:#6B6864; --line:#D8D5CC;
|
| 33 |
+
--red:#D8232A; --pos:#1F7A4C; --pos-bg:#E6F1EA; --neg:#A63A22; --neg-bg:#F6E9E3;
|
| 34 |
+
--amber:#B8860B; --purple:#7A2A63; --purple-bg:#F0E3EE; --blue:#2E6DA4;
|
| 35 |
+
}
|
| 36 |
+
*{box-sizing:border-box;}
|
| 37 |
+
html,body{margin:0;padding:0;background:var(--bg);color:var(--ink);
|
| 38 |
+
font-family:"Helvetica Neue",Arial,sans-serif;-webkit-font-smoothing:antialiased;}
|
| 39 |
+
.mono{font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace;}
|
| 40 |
+
|
| 41 |
+
header{background:var(--ink);color:#fff;padding:28px 20px 22px;position:relative;overflow:hidden;}
|
| 42 |
+
header .rail{position:absolute;left:0;right:0;bottom:0;height:4px;
|
| 43 |
+
background:repeating-linear-gradient(90deg,var(--red) 0 18px, transparent 18px 30px);opacity:.9;}
|
| 44 |
+
.eyebrow{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:#C9C6BD;font-weight:700;margin-bottom:8px;}
|
| 45 |
+
h1{font-size:26px;margin:0 0 4px;font-weight:800;letter-spacing:-0.01em;}
|
| 46 |
+
.sub{font-size:14px;color:#C9C6BD;margin:0;max-width:560px;line-height:1.4;}
|
| 47 |
+
|
| 48 |
+
nav.tabs{display:flex;background:var(--panel);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20;}
|
| 49 |
+
nav.tabs button{flex:1;padding:14px 4px;font-size:12.5px;font-weight:700;background:none;border:none;
|
| 50 |
+
color:var(--muted);cursor:pointer;border-bottom:3px solid transparent;}
|
| 51 |
+
nav.tabs button.active{color:var(--ink);border-bottom-color:var(--red);}
|
| 52 |
+
|
| 53 |
+
main{max-width:760px;margin:0 auto;padding:20px 16px 60px;}
|
| 54 |
+
section{display:none;}
|
| 55 |
+
section.active{display:block;}
|
| 56 |
+
|
| 57 |
+
h2.section-title{font-size:19px;font-weight:800;margin:26px 0 10px;}
|
| 58 |
+
h2.section-title:first-child{margin-top:4px;}
|
| 59 |
+
|
| 60 |
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;margin-bottom:14px;}
|
| 61 |
+
.card p{line-height:1.55;font-size:14.5px;margin:0 0 10px;}
|
| 62 |
+
.card p:last-child{margin-bottom:0;}
|
| 63 |
+
.rule-box{background:#FBF3E4;border:1px solid #E9D9B4;border-radius:8px;padding:14px 16px;margin:10px 0;}
|
| 64 |
+
.rule-box b{color:var(--amber);}
|
| 65 |
+
table.ref{width:100%;border-collapse:collapse;font-size:13px;margin:8px 0;}
|
| 66 |
+
table.ref th{text-align:left;padding:7px 8px;background:#F2F0EA;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);}
|
| 67 |
+
table.ref td{padding:7px 8px;border-bottom:1px solid var(--line);}
|
| 68 |
+
table.ref td.mono{font-family:"SF Mono",monospace;font-size:12.5px;white-space:nowrap;}
|
| 69 |
+
table.ref tr:last-child td{border-bottom:none;}
|
| 70 |
+
|
| 71 |
+
.sprint-setup{text-align:center;padding:10px 0 4px;}
|
| 72 |
+
.sprint-setup p{color:var(--muted);font-size:14px;line-height:1.5;}
|
| 73 |
+
.cat-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:14px 0;}
|
| 74 |
+
.cat-btn{padding:12px 6px;border:1.5px solid var(--line);border-radius:8px;background:#fff;
|
| 75 |
+
font-size:12.5px;font-weight:700;cursor:pointer;color:var(--ink);}
|
| 76 |
+
.cat-btn.sel{border-color:var(--red);background:#FDF1F1;}
|
| 77 |
+
.start-btn{width:100%;padding:15px;background:var(--red);color:#fff;border:none;border-radius:8px;
|
| 78 |
+
font-size:15px;font-weight:800;cursor:pointer;margin-top:6px;}
|
| 79 |
+
|
| 80 |
+
.sprint-hud{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;
|
| 81 |
+
font-family:"SF Mono",monospace;font-weight:800;font-size:14px;}
|
| 82 |
+
.hud-timer.urgent{color:var(--red);}
|
| 83 |
+
.hud-score{color:var(--pos);}
|
| 84 |
+
|
| 85 |
+
/* ---- Elemento distintivo: la serie como vagones + anotación de diferencias ---- */
|
| 86 |
+
.series-wrap{overflow-x:auto;padding:6px 0 2px;}
|
| 87 |
+
.series-row{display:flex;gap:0;align-items:center;min-width:min-content;}
|
| 88 |
+
.term{min-width:52px;padding:12px 8px;background:var(--ink);color:#E8E6DF;border-radius:8px;
|
| 89 |
+
font-family:"SF Mono",monospace;font-size:17px;font-weight:800;text-align:center;flex-shrink:0;}
|
| 90 |
+
.term.unknown{background:var(--red);color:#fff;}
|
| 91 |
+
.term.revealed{background:var(--pos);color:#fff;}
|
| 92 |
+
.link{width:34px;text-align:center;font-family:"SF Mono",monospace;font-size:11px;font-weight:800;
|
| 93 |
+
color:transparent;flex-shrink:0;transition:color .3s;}
|
| 94 |
+
.link.show{color:var(--amber);}
|
| 95 |
+
.q-text{font-size:15.5px;font-weight:700;line-height:1.5;margin:12px 0 14px;}
|
| 96 |
+
.opt-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px;}
|
| 97 |
+
.opt-btn{padding:14px;border:1.5px solid var(--line);border-radius:8px;background:#fff;text-align:center;
|
| 98 |
+
font-size:16px;font-weight:800;cursor:pointer;font-family:"SF Mono",monospace;}
|
| 99 |
+
.opt-btn:disabled{cursor:default;}
|
| 100 |
+
.opt-btn.right{border-color:var(--pos);background:var(--pos-bg);color:var(--pos);}
|
| 101 |
+
.opt-btn.wrong{border-color:var(--neg);background:var(--neg-bg);color:var(--neg);}
|
| 102 |
+
.q-feedback{margin-top:12px;font-size:13.5px;line-height:1.6;padding:12px;border-radius:8px;display:none;}
|
| 103 |
+
.q-feedback.show{display:block;}
|
| 104 |
+
.q-feedback.ok{background:var(--pos-bg);color:var(--pos);}
|
| 105 |
+
.q-feedback.ko{background:var(--neg-bg);color:var(--neg);}
|
| 106 |
+
.next-btn{margin-top:12px;width:100%;padding:13px;background:var(--ink);color:#fff;border:none;
|
| 107 |
+
border-radius:8px;font-size:14px;font-weight:800;cursor:pointer;display:none;}
|
| 108 |
+
.next-btn.show{display:block;}
|
| 109 |
+
|
| 110 |
+
.result-hero{text-align:center;padding:24px 10px;}
|
| 111 |
+
.result-num{font-size:52px;font-weight:800;font-family:"SF Mono",monospace;}
|
| 112 |
+
.result-label{font-size:14px;color:var(--muted);}
|
| 113 |
+
.result-best{margin-top:8px;font-size:13px;color:var(--amber);font-weight:700;}
|
| 114 |
+
|
| 115 |
+
.stat-row{display:flex;justify-content:space-between;align-items:center;padding:11px 4px;border-bottom:1px solid var(--line);font-size:14px;}
|
| 116 |
+
.stat-row:last-child{border-bottom:none;}
|
| 117 |
+
.stat-acc{font-family:"SF Mono",monospace;font-weight:800;}
|
| 118 |
+
.acc-good{color:var(--pos);} .acc-mid{color:var(--amber);} .acc-bad{color:var(--neg);} .acc-none{color:var(--muted);}
|
| 119 |
+
.progress-hero{text-align:center;padding:20px 10px 8px;}
|
| 120 |
+
.progress-num{font-size:46px;font-weight:800;font-family:"SF Mono",monospace;}
|
| 121 |
+
.progress-label{font-size:13px;color:var(--muted);margin-top:2px;}
|
| 122 |
+
.reset-link{display:block;text-align:center;margin-top:20px;font-size:13px;color:var(--muted);
|
| 123 |
+
text-decoration:underline;cursor:pointer;background:none;border:none;}
|
| 124 |
+
</style>
|
| 125 |
+
</head>
|
| 126 |
+
<body>
|
| 127 |
+
|
| 128 |
+
<header>
|
| 129 |
+
<div class="eyebrow">ICT-Eignungstest · Logisches Denken</div>
|
| 130 |
+
<h1>Zahlenreihen</h1>
|
| 131 |
+
<p class="sub">Series numéricas infinitas: 9 familias de patrones, generadas al azar, con la respuesta siempre calculada por código.</p>
|
| 132 |
+
<div class="rail"></div>
|
| 133 |
+
</header>
|
| 134 |
+
|
| 135 |
+
<nav class="tabs">
|
| 136 |
+
<button class="tab-btn active" data-tab="teoria">Teoría</button>
|
| 137 |
+
<button class="tab-btn" data-tab="sprint">⚡ Sprint</button>
|
| 138 |
+
<button class="tab-btn" data-tab="progreso">Progreso</button>
|
| 139 |
+
</nav>
|
| 140 |
+
|
| 141 |
+
<main>
|
| 142 |
+
|
| 143 |
+
<section id="teoria" class="active">
|
| 144 |
+
<h2 class="section-title">El algoritmo de diagnóstico en 4 pasos</h2>
|
| 145 |
+
<div class="card">
|
| 146 |
+
<div class="rule-box"><p><b>Paso 1 — Diferencias:</b> calcula las diferencias entre términos consecutivos. ¿Constantes? → serie aritmética (+d). Es el caso más común, resuélvelo en 3 segundos.</p></div>
|
| 147 |
+
<div class="rule-box"><p><b>Paso 2 — Diferencias de las diferencias:</b> si las diferencias no son constantes pero crecen regularmente (+2,+4,+6…), la serie tiene diferencia creciente. Los cuadrados (1,4,9,16…) son el caso especial: sus diferencias son 3,5,7,9 (impares consecutivos).</p></div>
|
| 148 |
+
<div class="rule-box"><p><b>Paso 3 — Razones:</b> si las diferencias explotan (se duplican o más), divide términos consecutivos. ¿Cociente constante? → geométrica (×2, ×3, ÷2). ¿Casi el doble pero no exacto? → prueba ×2+c.</p></div>
|
| 149 |
+
<div class="rule-box"><p><b>Paso 4 — Los disfraces:</b> si nada funciona, prueba los tres patrones camuflados: <b>zigzag</b> (sube-baja: +p,−q alternante), <b>entrelazada</b> (dos series independientes en posiciones alternas: mira los términos 1º,3º,5º por un lado y 2º,4º,6º por otro), y <b>Fibonacci</b> (cada término = suma de los dos anteriores).</p></div>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<h2 class="section-title">Las 9 familias del examen</h2>
|
| 153 |
+
<div class="card">
|
| 154 |
+
<table class="ref">
|
| 155 |
+
<tr><th>Familia</th><th>Ejemplo</th><th>Clave</th></tr>
|
| 156 |
+
<tr><td>Aritmética</td><td class="mono">7, 11, 15, 19 → 23</td><td>diferencia fija</td></tr>
|
| 157 |
+
<tr><td>Geométrica ×</td><td class="mono">3, 6, 12, 24 → 48</td><td>cociente fijo</td></tr>
|
| 158 |
+
<tr><td>Geométrica ÷</td><td class="mono">320, 160, 80 → 40</td><td>mitades</td></tr>
|
| 159 |
+
<tr><td>Dif. creciente</td><td class="mono">2, 5, 10, 17 → 26</td><td>+3,+5,+7,+9</td></tr>
|
| 160 |
+
<tr><td>Cuadrados</td><td class="mono">4, 9, 16, 25 → 36</td><td>n²</td></tr>
|
| 161 |
+
<tr><td>Zigzag</td><td class="mono">10, 17, 14, 21, 18 → 25</td><td>+7,−3 alterno</td></tr>
|
| 162 |
+
<tr><td>Fibonacci</td><td class="mono">2, 5, 7, 12, 19 → 31</td><td>suma de 2 previos</td></tr>
|
| 163 |
+
<tr><td>Entrelazada</td><td class="mono">3, 10, 8, 12, 13, 14 → 18</td><td>2 series alternas</td></tr>
|
| 164 |
+
<tr><td>×2+c</td><td class="mono">3, 7, 15, 31 → 63</td><td>doble más c</td></tr>
|
| 165 |
+
</table>
|
| 166 |
+
</div>
|
| 167 |
+
|
| 168 |
+
<h2 class="section-title">La trampa nº1: la entrelazada</h2>
|
| 169 |
+
<div class="card">
|
| 170 |
+
<p>Una serie entrelazada parece caótica porque las diferencias saltan sin patrón. La señal delatora: <b>las diferencias alternan entre dos valores que no encajan en un zigzag limpio</b>. Solución mecánica: tapa los términos pares y lee solo los impares (1º, 3º, 5º) — si forman una serie limpia, es entrelazada, y la respuesta continúa la sub-serie que toque según la posición.</p>
|
| 171 |
+
</div>
|
| 172 |
+
</section>
|
| 173 |
+
|
| 174 |
+
<section id="sprint">
|
| 175 |
+
<div id="sprint-area"></div>
|
| 176 |
+
</section>
|
| 177 |
+
|
| 178 |
+
<section id="progreso">
|
| 179 |
+
<div class="progress-hero">
|
| 180 |
+
<div class="progress-num" id="prog-sprint">—</div>
|
| 181 |
+
<div class="progress-label">mejor Sprint (de 10)</div>
|
| 182 |
+
</div>
|
| 183 |
+
<h2 class="section-title">Precisión por familia</h2>
|
| 184 |
+
<div class="card" id="stats-list"></div>
|
| 185 |
+
<button class="reset-link" id="reset-btn">Reiniciar todo mi progreso</button>
|
| 186 |
+
<div style="font-size:12px;color:var(--muted);text-align:center;padding:10px;" id="storage-note"></div>
|
| 187 |
+
</section>
|
| 188 |
+
|
| 189 |
+
</main>
|
| 190 |
+
|
| 191 |
+
<script>
|
| 192 |
+
/* ============================================================
|
| 193 |
+
Generadores verificados con 27.000 series (0 fallos).
|
| 194 |
+
Cada respuesta y cada distractor se CALCULA — nada manual.
|
| 195 |
+
============================================================ */
|
| 196 |
+
const ri = (a,b) => a + Math.floor(Math.random()*(b-a+1));
|
| 197 |
+
const pick = arr => arr[ri(0,arr.length-1)];
|
| 198 |
+
const shuffle = arr => { const a=[...arr]; for(let i=a.length-1;i>0;i--){const j=ri(0,i);[a[i],a[j]]=[a[j],a[i]];} return a; };
|
| 199 |
+
|
| 200 |
+
function buildOptions(correct, wrongs){
|
| 201 |
+
const opts = new Set([correct]);
|
| 202 |
+
for(const w of wrongs){ if(opts.size<4 && w!==correct && Number.isFinite(w)) opts.add(w); }
|
| 203 |
+
let guard=0;
|
| 204 |
+
while(opts.size<4 && guard++<50) opts.add(correct + pick([-3,-2,2,3,4])*pick([1,1,2]));
|
| 205 |
+
return shuffle([...opts].slice(0,4));
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
function genArith(){
|
| 209 |
+
const d = pick([2,3,4,5,6,7,8,9,11,12,-2,-3,-4,-5]);
|
| 210 |
+
const a = d<0 ? ri(30,60) : ri(2,30);
|
| 211 |
+
const t = [...Array(5)].map((_,k)=>a+k*d);
|
| 212 |
+
const ans = a+5*d;
|
| 213 |
+
return { type:"arith", terms:t, ans,
|
| 214 |
+
options: buildOptions(ans, [ans+d, ans-d, t[4]]),
|
| 215 |
+
diffs: t.slice(1).map((v,i)=>fmtD(v-t[i])),
|
| 216 |
+
explain:`Diferencia constante de ${fmtD(d)}. Siguiente: ${t[4]} ${fmtD(d)} = ${ans}.` };
|
| 217 |
+
}
|
| 218 |
+
function genGeom(){
|
| 219 |
+
const r = pick([2,3]), a = pick([1,2,3,4,5]);
|
| 220 |
+
const t = [...Array(5)].map((_,k)=>a*Math.pow(r,k));
|
| 221 |
+
const ans = a*Math.pow(r,5);
|
| 222 |
+
const arithTrap = t[4] + (t[4]-t[3]);
|
| 223 |
+
return { type:"geom", terms:t, ans,
|
| 224 |
+
options: buildOptions(ans, [arithTrap, ans-r, t[4]*r+r]),
|
| 225 |
+
diffs: t.slice(1).map(()=>"×"+r),
|
| 226 |
+
explain:`Cociente constante ×${r}. Siguiente: ${t[4]}×${r} = ${ans}. Trampa: tratarla como suma daría ${arithTrap}.` };
|
| 227 |
+
}
|
| 228 |
+
function genGeomDiv(){
|
| 229 |
+
const a = pick([320,384,448,512,640]);
|
| 230 |
+
const t = [...Array(5)].map((_,k)=>a/Math.pow(2,k));
|
| 231 |
+
const ans = a/32;
|
| 232 |
+
return { type:"geomdiv", terms:t, ans,
|
| 233 |
+
options: buildOptions(ans, [t[4]-(t[3]-t[4]), ans*2, ans-2]),
|
| 234 |
+
diffs: t.slice(1).map(()=>"÷2"),
|
| 235 |
+
explain:`Cada término es la mitad del anterior. Siguiente: ${t[4]}÷2 = ${ans}.` };
|
| 236 |
+
}
|
| 237 |
+
function genGrowDiff(){
|
| 238 |
+
const d0 = ri(1,5), inc = pick([1,2,3]), a = ri(1,15);
|
| 239 |
+
const t = [a]; for(let k=0;k<4;k++) t.push(t[t.length-1]+d0+k*inc);
|
| 240 |
+
const ans = t[4]+d0+4*inc;
|
| 241 |
+
const lazyTrap = t[4]+d0+3*inc; // repetir la última diferencia sin crecer
|
| 242 |
+
return { type:"growdiff", terms:t, ans,
|
| 243 |
+
options: buildOptions(ans, [lazyTrap, ans+inc, ans-1]),
|
| 244 |
+
diffs: t.slice(1).map((v,i)=>fmtD(v-t[i])),
|
| 245 |
+
explain:`Las diferencias crecen de ${inc} en ${inc}: ${t.slice(1).map((v,i)=>fmtD(v-t[i])).join(", ")} → la siguiente es ${fmtD(d0+4*inc)}. Trampa: repetir la última diferencia daría ${lazyTrap}.` };
|
| 246 |
+
}
|
| 247 |
+
function genAltern(){
|
| 248 |
+
const p = ri(5,12), q = ri(1,p-2), a = ri(5,20);
|
| 249 |
+
const t = [a]; for(let k=0;k<5;k++) t.push(t[t.length-1]+(k%2===0?p:-q));
|
| 250 |
+
const ans = t[5]-q; // el 7º término aplica −q (índice 5, impar)
|
| 251 |
+
const wrongOp = t[5]+p;
|
| 252 |
+
return { type:"altern", terms:t, ans,
|
| 253 |
+
options: buildOptions(ans, [wrongOp, t[5]-p, t[5]+q]),
|
| 254 |
+
diffs: t.slice(1).map((v,i)=>fmtD(v-t[i])),
|
| 255 |
+
explain:`Zigzag +${p}/−${q} alternante. El siguiente paso es −${q}: ${t[5]}−${q} = ${ans}. Trampa: aplicar +${p} daría ${wrongOp}.` };
|
| 256 |
+
}
|
| 257 |
+
function genFib(){
|
| 258 |
+
const a = ri(1,5), b = ri(a,8);
|
| 259 |
+
const t = [a,b]; for(let k=0;k<4;k++) t.push(t[t.length-1]+t[t.length-2]);
|
| 260 |
+
const ans = t[5]+t[4];
|
| 261 |
+
return { type:"fib", terms:t, ans,
|
| 262 |
+
options: buildOptions(ans, [t[5]*2, t[5]+t[3], ans+1]),
|
| 263 |
+
diffs: t.slice(1).map((v,i)=>fmtD(v-t[i])),
|
| 264 |
+
explain:`Cada término = suma de los dos anteriores. Siguiente: ${t[4]}+${t[5]} = ${ans}. Pista: las diferencias reproducen la propia serie.` };
|
| 265 |
+
}
|
| 266 |
+
function genInterleaved(){
|
| 267 |
+
const a1 = ri(2,20), dA = pick([3,4,5,6,7]);
|
| 268 |
+
let b1 = ri(2,20); if(b1===a1) b1++;
|
| 269 |
+
let dB = pick([2,3,4,5,8]); if(dB===dA) dB = dA+1;
|
| 270 |
+
const t = [a1, b1, a1+dA, b1+dB, a1+2*dA, b1+2*dB];
|
| 271 |
+
const ans = a1+3*dA;
|
| 272 |
+
const wrongSeries = b1+3*dB;
|
| 273 |
+
return { type:"interleaved", terms:t, ans,
|
| 274 |
+
options: buildOptions(ans, [wrongSeries, t[5]+(t[5]-t[4]), ans+dA]),
|
| 275 |
+
diffs: t.slice(1).map((v,i)=>fmtD(v-t[i])),
|
| 276 |
+
explain:`Dos series entrelazadas — impares: ${a1}, ${a1+dA}, ${a1+2*dA} (+${dA}) · pares: ${b1}, ${b1+dB}, ${b1+2*dB} (+${dB}). La posición 7 pertenece a la serie impar: ${a1+2*dA}+${dA} = ${ans}. Trampa: continuar la serie equivocada daría ${wrongSeries}.` };
|
| 277 |
+
}
|
| 278 |
+
function genSquare(){
|
| 279 |
+
const n0 = ri(1,5), k = pick([0,0,1,2,3]);
|
| 280 |
+
const t = [...Array(5)].map((_,j)=>(n0+j)*(n0+j)+k);
|
| 281 |
+
const ans = (n0+5)*(n0+5)+k;
|
| 282 |
+
const lazyTrap = t[4]+(t[4]-t[3]);
|
| 283 |
+
return { type:"square", terms:t, ans,
|
| 284 |
+
options: buildOptions(ans, [lazyTrap, ans-1, ans+2]),
|
| 285 |
+
diffs: t.slice(1).map((v,i)=>fmtD(v-t[i])),
|
| 286 |
+
explain:`Cuadrados${k>0?` +${k}`:""}: ${t.map((_,j)=>`${n0+j}²${k>0?`+${k}`:""}`).join(", ")}. Siguiente: ${n0+5}²${k>0?`+${k}`:""} = ${ans}. Las diferencias son impares consecutivos — la firma de los cuadrados.` };
|
| 287 |
+
}
|
| 288 |
+
function genMultAdd(){
|
| 289 |
+
const c = pick([1,2,3,-1]), a = ri(1,5);
|
| 290 |
+
const t = [a]; for(let k=0;k<4;k++) t.push(t[t.length-1]*2+c);
|
| 291 |
+
const ans = t[4]*2+c;
|
| 292 |
+
const noC = t[4]*2;
|
| 293 |
+
return { type:"multadd", terms:t, ans,
|
| 294 |
+
options: buildOptions(ans, [noC, t[4]+(t[4]-t[3]), ans+c]),
|
| 295 |
+
diffs: t.slice(1).map(()=> c>=0?`×2+${c}`:`×2−${-c}`),
|
| 296 |
+
explain:`Regla: doble ${c>=0?`más ${c}`:`menos ${-c}`}. Siguiente: ${t[4]}×2${c>=0?`+${c}`:`−${-c}`} = ${ans}. Trampa: olvidar la constante daría ${noC}.` };
|
| 297 |
+
}
|
| 298 |
+
function fmtD(d){ return d>=0?`+${d}`:`−${-d}`; }
|
| 299 |
+
|
| 300 |
+
const GENERATORS = {
|
| 301 |
+
arith: {fn:genArith, label:"Aritmética"},
|
| 302 |
+
geom: {fn:genGeom, label:"Geométrica ×"},
|
| 303 |
+
geomdiv: {fn:genGeomDiv, label:"Geométrica ÷"},
|
| 304 |
+
growdiff: {fn:genGrowDiff, label:"Dif. creciente"},
|
| 305 |
+
square: {fn:genSquare, label:"Cuadrados"},
|
| 306 |
+
altern: {fn:genAltern, label:"Zigzag"},
|
| 307 |
+
fib: {fn:genFib, label:"Fibonacci"},
|
| 308 |
+
multadd: {fn:genMultAdd, label:"×2 + c"},
|
| 309 |
+
interleaved:{fn:genInterleaved,label:"Entrelazada"}
|
| 310 |
+
};
|
| 311 |
+
|
| 312 |
+
/* ============================================================
|
| 313 |
+
SPRINT
|
| 314 |
+
============================================================ */
|
| 315 |
+
const SPRINT_N = 10, SPRINT_SECONDS = 240;
|
| 316 |
+
let sprint=null, sprintTimer=null, selectedCats = new Set(Object.keys(GENERATORS));
|
| 317 |
+
|
| 318 |
+
function renderSeries(q, revealed){
|
| 319 |
+
let html = `<div class="series-wrap"><div class="series-row">`;
|
| 320 |
+
q.terms.forEach((v,i)=>{
|
| 321 |
+
html += `<span class="term">${v}</span>`;
|
| 322 |
+
html += `<span class="link ${revealed?'show':''}">${q.diffs[i] !== undefined ? q.diffs[i] : ""}</span>`;
|
| 323 |
+
});
|
| 324 |
+
html += `<span class="term ${revealed?'revealed':'unknown'}">${revealed ? q.ans : "?"}</span>`;
|
| 325 |
+
html += `</div></div>`;
|
| 326 |
+
return html;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
function renderSprintSetup(){
|
| 330 |
+
clearInterval(sprintTimer);
|
| 331 |
+
const area = document.getElementById("sprint-area");
|
| 332 |
+
area.innerHTML = `
|
| 333 |
+
<div class="card sprint-setup">
|
| 334 |
+
<h2 class="section-title" style="margin-top:0;">Sprint: ${SPRINT_N} series · ${SPRINT_SECONDS/60} minutos</h2>
|
| 335 |
+
<p>Series generadas al azar de 9 familias. Los distractores son los errores reales: continuar la serie equivocada en una entrelazada, aplicar el signo incorrecto en un zigzag, no hacer crecer la diferencia. Al responder, la estructura oculta se revela visualmente sobre la serie.</p>
|
| 336 |
+
<div class="cat-grid">
|
| 337 |
+
${Object.entries(GENERATORS).map(([k,g]) => `<button class="cat-btn ${selectedCats.has(k)?'sel':''}" data-cat="${k}">${g.label}</button>`).join("")}
|
| 338 |
+
</div>
|
| 339 |
+
<button class="start-btn" id="start-sprint">▶ Empezar Sprint</button>
|
| 340 |
+
</div>`;
|
| 341 |
+
area.querySelectorAll(".cat-btn").forEach(b => b.addEventListener("click", () => {
|
| 342 |
+
const k=b.dataset.cat;
|
| 343 |
+
if(selectedCats.has(k)){ if(selectedCats.size>1) selectedCats.delete(k); } else selectedCats.add(k);
|
| 344 |
+
renderSprintSetup();
|
| 345 |
+
}));
|
| 346 |
+
area.querySelector("#start-sprint").addEventListener("click", startSprint);
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
function startSprint(){
|
| 350 |
+
const cats=[...selectedCats];
|
| 351 |
+
const qs=[];
|
| 352 |
+
for(let i=0;i<SPRINT_N;i++) qs.push(GENERATORS[cats[i%cats.length]].fn());
|
| 353 |
+
sprint = { qs:shuffle(qs), idx:0, score:0, remaining:SPRINT_SECONDS };
|
| 354 |
+
sprintTimer = setInterval(() => {
|
| 355 |
+
sprint.remaining--;
|
| 356 |
+
const el=document.getElementById("hud-timer");
|
| 357 |
+
if(el){
|
| 358 |
+
el.textContent = `${Math.floor(sprint.remaining/60)}:${(sprint.remaining%60).toString().padStart(2,"0")}`;
|
| 359 |
+
el.classList.toggle("urgent", sprint.remaining<=30);
|
| 360 |
+
}
|
| 361 |
+
if(sprint.remaining<=0) endSprint(true);
|
| 362 |
+
},1000);
|
| 363 |
+
renderQuestion();
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
function renderQuestion(){
|
| 367 |
+
const q = sprint.qs[sprint.idx];
|
| 368 |
+
const area = document.getElementById("sprint-area");
|
| 369 |
+
area.innerHTML = `
|
| 370 |
+
<div class="card">
|
| 371 |
+
<div class="sprint-hud">
|
| 372 |
+
<span>${sprint.idx+1} / ${SPRINT_N}</span>
|
| 373 |
+
<span class="hud-timer" id="hud-timer">${Math.floor(sprint.remaining/60)}:${(sprint.remaining%60).toString().padStart(2,"0")}</span>
|
| 374 |
+
<span class="hud-score">✓ ${sprint.score}</span>
|
| 375 |
+
</div>
|
| 376 |
+
<div id="series-holder">${renderSeries(q,false)}</div>
|
| 377 |
+
<p class="q-text">¿Qué número continúa la serie?</p>
|
| 378 |
+
<div class="opt-grid">
|
| 379 |
+
${q.options.map(o=>`<button class="opt-btn" data-val="${o}">${o}</button>`).join("")}
|
| 380 |
+
</div>
|
| 381 |
+
<div class="q-feedback" id="q-feedback"></div>
|
| 382 |
+
<button class="next-btn" id="next-btn">Siguiente →</button>
|
| 383 |
+
</div>`;
|
| 384 |
+
area.querySelectorAll(".opt-btn").forEach(btn => btn.addEventListener("click", () => answer(btn,q)));
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
function answer(btn,q){
|
| 388 |
+
const area=document.getElementById("sprint-area");
|
| 389 |
+
const isRight = parseInt(btn.dataset.val,10)===q.ans;
|
| 390 |
+
area.querySelectorAll(".opt-btn").forEach(b=>{
|
| 391 |
+
b.disabled=true;
|
| 392 |
+
if(parseInt(b.dataset.val,10)===q.ans) b.classList.add("right");
|
| 393 |
+
else if(b===btn && !isRight) b.classList.add("wrong");
|
| 394 |
+
});
|
| 395 |
+
if(isRight) sprint.score++;
|
| 396 |
+
if(!stats[q.type]) stats[q.type]={ok:0,total:0};
|
| 397 |
+
stats[q.type].total++; if(isRight) stats[q.type].ok++;
|
| 398 |
+
saveState();
|
| 399 |
+
// revelar la estructura: diferencias anotadas + término solución
|
| 400 |
+
document.getElementById("series-holder").innerHTML = renderSeries(q,true);
|
| 401 |
+
const fb=document.getElementById("q-feedback");
|
| 402 |
+
fb.className=`q-feedback show ${isRight?'ok':'ko'}`;
|
| 403 |
+
fb.innerHTML = `${isRight?"✓ Correcto. ":"✗ Incorrecto. "}${q.explain}`;
|
| 404 |
+
document.querySelector(".hud-score").textContent=`✓ ${sprint.score}`;
|
| 405 |
+
const nb=document.getElementById("next-btn");
|
| 406 |
+
nb.classList.add("show");
|
| 407 |
+
nb.textContent = sprint.idx+1<SPRINT_N ? "Siguiente →" : "Ver resultado";
|
| 408 |
+
nb.addEventListener("click", ()=>{ sprint.idx++; sprint.idx<SPRINT_N ? renderQuestion() : endSprint(false); });
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
function endSprint(timedOut){
|
| 412 |
+
clearInterval(sprintTimer);
|
| 413 |
+
const prev=best;
|
| 414 |
+
if(sprint.score>best){ best=sprint.score; saveState(); }
|
| 415 |
+
const area=document.getElementById("sprint-area");
|
| 416 |
+
area.innerHTML=`
|
| 417 |
+
<div class="card result-hero">
|
| 418 |
+
<div class="result-num">${sprint.score} / ${SPRINT_N}</div>
|
| 419 |
+
<div class="result-label">${timedOut?"⏱ Se acabó el tiempo":"Sprint completado"}</div>
|
| 420 |
+
${sprint.score>prev?`<div class="result-best">★ ¡Nuevo récord personal!</div>`:`<div class="result-best" style="color:var(--muted)">Mejor marca: ${best}/${SPRINT_N}</div>`}
|
| 421 |
+
<button class="start-btn" id="again">▶ Otro Sprint</button>
|
| 422 |
+
<button class="reset-link" id="back-setup">Cambiar familias de series</button>
|
| 423 |
+
</div>`;
|
| 424 |
+
area.querySelector("#again").addEventListener("click", startSprint);
|
| 425 |
+
area.querySelector("#back-setup").addEventListener("click", renderSprintSetup);
|
| 426 |
+
renderProgress();
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
/* ============================================================
|
| 430 |
+
PROGRESO + STORAGE
|
| 431 |
+
============================================================ */
|
| 432 |
+
let stats={}, best=0;
|
| 433 |
+
function accClass(p){ return p>=80?"acc-good":p>=50?"acc-mid":"acc-bad"; }
|
| 434 |
+
function renderProgress(){
|
| 435 |
+
document.getElementById("prog-sprint").textContent = best>0?`${best} / ${SPRINT_N}`:"—";
|
| 436 |
+
document.getElementById("stats-list").innerHTML = Object.entries(GENERATORS).map(([k,g])=>{
|
| 437 |
+
const s=stats[k];
|
| 438 |
+
const inner = s&&s.total>0
|
| 439 |
+
? `<span class="stat-acc ${accClass(Math.round(s.ok/s.total*100))}">${Math.round(s.ok/s.total*100)}% (${s.ok}/${s.total})</span>`
|
| 440 |
+
: `<span class="stat-acc acc-none">sin datos</span>`;
|
| 441 |
+
return `<div class="stat-row"><span>${g.label}</span>${inner}</div>`;
|
| 442 |
+
}).join("");
|
| 443 |
+
}
|
| 444 |
+
async function loadState(){
|
| 445 |
+
try{
|
| 446 |
+
const res=await window.storage.get("zahlenreihen-progress");
|
| 447 |
+
if(res&&res.value){
|
| 448 |
+
const d=JSON.parse(res.value);
|
| 449 |
+
stats=d.stats||{}; best=d.best||0;
|
| 450 |
+
}
|
| 451 |
+
}catch(e){}
|
| 452 |
+
}
|
| 453 |
+
async function saveState(){
|
| 454 |
+
try{ await window.storage.set("zahlenreihen-progress", JSON.stringify({stats,best})); }
|
| 455 |
+
catch(e){ const n=document.getElementById("storage-note"); if(n) n.textContent="No se pudo guardar el progreso."; }
|
| 456 |
+
}
|
| 457 |
+
document.getElementById("reset-btn").addEventListener("click", async ()=>{
|
| 458 |
+
if(!confirm("¿Reiniciar todo tu progreso?")) return;
|
| 459 |
+
stats={}; best=0;
|
| 460 |
+
await saveState(); renderProgress();
|
| 461 |
+
});
|
| 462 |
+
document.querySelectorAll(".tab-btn").forEach(btn=>btn.addEventListener("click",()=>{
|
| 463 |
+
document.querySelectorAll(".tab-btn").forEach(b=>b.classList.remove("active"));
|
| 464 |
+
document.querySelectorAll("main > section").forEach(s=>s.classList.remove("active"));
|
| 465 |
+
btn.classList.add("active");
|
| 466 |
+
document.getElementById(btn.dataset.tab).classList.add("active");
|
| 467 |
+
}));
|
| 468 |
+
|
| 469 |
+
(async function init(){
|
| 470 |
+
await loadState();
|
| 471 |
+
renderSprintSetup();
|
| 472 |
+
renderProgress();
|
| 473 |
+
})();
|
| 474 |
+
</script>
|
| 475 |
+
</body>
|
| 476 |
+
</html>
|
assets/index-CwQxyZjZ.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.sprint-page{display:flex;flex-direction:column;gap:1.25rem}.sprint-page .study-viewer-bar{margin:-3rem -2rem 0;border-bottom:1px solid var(--line)}.sprint-controls{display:flex;flex-direction:column;gap:.75rem}.sprint-engines{display:flex;flex-wrap:wrap;gap:.5rem}.sprint-engine-btn{background:var(--panel);border:1px solid var(--line);padding:.5rem .9rem;border-radius:999px;font-size:.9rem;font-weight:600;color:var(--ink);cursor:pointer}.sprint-engine-btn.active{background:var(--red);border-color:var(--red);color:#fff}.sprint-ai-toggle{display:flex;align-items:center;gap:.5rem;font-size:.9rem;font-weight:600;cursor:pointer}.sprint-ai-toggle.disabled{color:var(--muted);cursor:default}.sprint-ai-note{font-size:.85rem;color:var(--muted)}.sprint-stats{display:flex;flex-wrap:wrap;gap:.5rem}.sprint-chip{font-size:.8rem;font-weight:600;padding:.25rem .7rem;border-radius:999px;background:var(--panel);border:1px solid var(--line);color:var(--muted)}.sprint-chip.ai{background:var(--purple-bg);color:var(--purple);border-color:var(--purple)}.sprint-chip.rej{background:var(--neg-bg);color:var(--neg);border-color:var(--neg)}.sprint-notice{font-size:.85rem;color:var(--amber);font-weight:600}.sprint-loading{padding:2.5rem;text-align:center;color:var(--muted);background:var(--panel);border:1px dashed var(--line);border-radius:8px}.quiz{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:1.5rem;display:flex;flex-direction:column;gap:1rem}.quiz-type{font-size:.8rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);display:flex;align-items:center;gap:.6rem}.quiz-badge{background:var(--purple-bg);color:var(--purple);border:1px solid var(--purple);padding:.1rem .55rem;border-radius:999px;font-size:.75rem;letter-spacing:0;text-transform:none}.quiz-context{background:var(--bg);border-left:4px solid var(--blue);padding:.75rem 1rem;border-radius:4px;font-size:.95rem}.quiz-code{background:#1c1c1c;color:#e8e6e1;padding:1rem;border-radius:6px;font-size:.9rem;overflow-x:auto;line-height:1.5}.quiz-text{font-size:1.1rem;font-weight:600}.quiz-options{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:.6rem}.quiz-option{background:var(--bg);border:1px solid var(--line);padding:.75rem;border-radius:6px;font-size:1rem;font-weight:600;font-family:inherit;cursor:pointer}.quiz-option:hover{border-color:var(--red)}.quiz-option.correct{background:var(--pos-bg);border-color:var(--pos);color:var(--pos)}.quiz-option.wrong{background:var(--neg-bg);border-color:var(--neg);color:var(--neg)}.quiz-option.disabled{opacity:.55;cursor:default}.quiz-explain{padding:.85rem 1rem;border-radius:6px;font-size:.95rem;line-height:1.5}.quiz-explain.ok{background:var(--pos-bg);border-left:4px solid var(--pos)}.quiz-explain.ko{background:var(--neg-bg);border-left:4px solid var(--neg)}.quiz-next{align-self:flex-start;padding:.6rem 1.2rem}.quiz-visual{overflow-x:auto;padding:.5rem 0}.quiz-visual .compare-row{display:flex;align-items:center;gap:.35rem;padding:.4rem 0;font-family:SF Mono,Consolas,monospace;white-space:nowrap}.quiz-visual .rlabel{font-weight:700;color:var(--muted);margin-right:.5rem;font-family:inherit}.quiz-visual .blk{position:relative;background:var(--bg);border:1px solid var(--line);border-radius:4px;padding:.3rem .55rem;font-size:1rem}.quiz-visual .blk.sep{background:none;border:none;padding:0 .1rem;color:var(--muted)}.quiz-visual .blk.hl{border-color:var(--amber)}.quiz-visual .blk-idx{position:absolute;top:-.7rem;right:-.3rem;font-size:.65rem;color:var(--muted)}.clase-page{display:flex;flex-direction:column;gap:1.25rem}.clase-controls{display:flex;flex-wrap:wrap;align-items:center;gap:1rem;padding:1rem;background:var(--panel);border:1px solid var(--line);border-radius:8px}.clase-speed{display:flex;align-items:center;gap:.6rem;font-size:.85rem;color:var(--muted)}.clase-speed input[type=range]{width:120px;accent-color:var(--red)}.clase-voice-select{padding:.4rem .6rem;border:1px solid var(--line);border-radius:4px;font-size:.85rem;background:var(--bg);color:var(--ink);max-width:240px}.clase-board{display:flex;flex-direction:column;gap:1.25rem}.clase-exercise{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:1.5rem}.clase-steps{display:flex;flex-direction:column;gap:.75rem}.clase-step{display:flex;gap:.9rem;align-items:flex-start;padding:1rem 1.2rem;border-radius:8px;border:1px solid var(--line);background:var(--panel);transition:all .2s}.clase-step-num{flex-shrink:0;width:1.8rem;height:1.8rem;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.85rem;font-weight:700;background:var(--bg);color:var(--muted)}.clase-step-title{font-weight:700;margin-bottom:.25rem}.clase-step-narration{color:var(--muted);font-size:.95rem;line-height:1.5}.clase-step.active{border-color:var(--red);background:var(--pos-bg);box-shadow:0 0 0 2px #d8232a1f}.clase-step.active .clase-step-num{background:var(--red);color:#fff}.clase-step.active .clase-step-narration{color:var(--ink)}.clase-step.past{opacity:.55}.clase-step.future{opacity:.4}.clase-actions{display:flex;gap:.75rem;justify-content:flex-end}@media (max-width: 768px){.clase-controls{flex-direction:column;align-items:stretch}.clase-voice-select{max-width:none}.clase-actions{flex-direction:column-reverse}}.tutor-container{display:flex;height:100%;gap:1rem}.tutor-sidebar{width:200px;background:var(--panel);border-right:1px solid var(--line);padding:1.5rem;overflow-y:auto;flex-shrink:0}.tutor-sidebar h3{font-size:.9rem;font-weight:700;color:var(--muted);margin-bottom:1rem;text-transform:uppercase}.category-list{display:flex;flex-direction:column;gap:.5rem;margin-bottom:2rem}.category-btn{background:var(--bg);border:1px solid var(--line);padding:.6rem .8rem;border-radius:4px;font-size:.85rem;font-weight:600;color:var(--ink);cursor:pointer;text-align:left;transition:all .2s}.category-btn:hover{border-color:var(--red);background:var(--panel)}.category-btn.active{background:var(--red);color:#fff;border-color:var(--red)}.tutor-info{padding-top:1rem;border-top:1px solid var(--line);font-size:.8rem}.session-user{color:var(--muted);margin-bottom:.5rem;word-break:break-all}.session-status{color:var(--pos);font-weight:600}.tutor-main{flex:1;display:flex;flex-direction:column;min-width:0}.tutor-header{padding:1.5rem;border-bottom:1px solid var(--line);background:var(--panel)}.tutor-header h2{font-size:1.3rem;margin-bottom:.25rem}.tutor-subtitle{font-size:.9rem;color:var(--muted)}.tutor-note{padding:.8rem 1.5rem;background:var(--purple-bg);border-left:4px solid var(--purple);color:var(--ink);font-size:.9rem}.chat-messages{flex:1;overflow-y:auto;padding:1.5rem;display:flex;flex-direction:column;gap:1rem}.chat-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--muted);text-align:center}.chat-empty p{margin:.5rem 0}.chat-message{display:flex;gap:.75rem;align-items:flex-start}.chat-message.user{justify-content:flex-end}.message-badge{font-size:1.5rem;flex-shrink:0;width:2rem;text-align:center}.chat-message.user .message-badge{order:2}.message-content{background:var(--bg);padding:.8rem 1rem;border-radius:8px;line-height:1.5;max-width:70%;word-wrap:break-word;white-space:pre-wrap}.chat-message.user .message-content{background:var(--red);color:#fff}.chat-message.assistant .message-content{background:var(--panel);border:1px solid var(--line)}.typing-indicator{display:flex;gap:.4rem;align-items:center;height:1.5rem}.typing-indicator span{width:.6rem;height:.6rem;border-radius:50%;background:var(--muted);animation:pulse 1.4s infinite}.typing-indicator span:nth-child(1){animation-delay:0s}.typing-indicator span:nth-child(2){animation-delay:.2s}.typing-indicator span:nth-child(3){animation-delay:.4s}@keyframes pulse{0%,to{opacity:.3}50%{opacity:1}}.chat-error{padding:1rem;background:var(--neg-bg);border-left:4px solid var(--neg);color:var(--neg);border-radius:4px;font-size:.9rem}.chat-form{display:flex;gap:.75rem;padding:1.5rem;border-top:1px solid var(--line);background:var(--panel)}.chat-input{flex:1;padding:.75rem 1rem;border:1px solid var(--line);border-radius:4px;font-size:.95rem;font-family:inherit}.chat-input:focus{outline:none;border-color:var(--red);box-shadow:0 0 0 2px #d8232a1a}.chat-input:disabled{background:var(--bg);cursor:wait}.chat-send{background:var(--red);color:#fff;border:none;padding:.75rem 1.5rem;border-radius:4px;font-weight:700;font-size:.95rem;cursor:pointer;white-space:nowrap}.chat-send:hover:not(:disabled){background:#c0202e}.chat-send:disabled{opacity:.6;cursor:wait}@media (max-width: 768px){.tutor-container{flex-direction:column}.tutor-sidebar{width:100%;border-right:none;border-bottom:1px solid var(--line);max-height:120px;padding:1rem}.category-list{flex-direction:row;flex-wrap:wrap;gap:.5rem}.category-btn{flex:1;min-width:100px}.message-content{max-width:90%}.chat-form{flex-wrap:wrap}.chat-input{flex-basis:100%}}:root{--bg: #F7F6F3;--panel: #FFFFFF;--ink: #1C1C1C;--muted: #6B6864;--line: #D8D5CC;--red: #D8232A;--pos: #1F7A4C;--pos-bg: #E6F1EA;--neg: #A63A22;--neg-bg: #F6E9E3;--amber: #B8860B;--purple: #7A2A63;--purple-bg: #F0E3EE;--blue: #2E6DA4}*{margin:0;padding:0;box-sizing:border-box}body{font-family:Helvetica Neue,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:var(--bg);color:var(--ink);line-height:1.6}.app{min-height:100vh;display:flex;flex-direction:column}.header{background:linear-gradient(90deg,#000,#1a1a1a);background-image:repeating-linear-gradient(90deg,var(--red) 0px,var(--red) 4px,transparent 4px,transparent 8px),linear-gradient(90deg,#000,#1a1a1a);color:#fff;padding:2rem;text-align:center}.header h1{font-size:2.5rem;font-weight:700;margin-bottom:.5rem}.header p{font-size:1rem;opacity:.9}.tabs{position:sticky;top:0;z-index:10;display:flex;gap:0;background:var(--panel);border-bottom:2px solid var(--line);padding:0 1rem;overflow-x:auto}.tab-btn{background:none;border:none;border-bottom:3px solid transparent;padding:.9rem 1.2rem;font-size:.95rem;font-weight:600;color:var(--muted);cursor:pointer;white-space:nowrap}.tab-btn.active{color:var(--red);border-bottom-color:var(--red)}.main{flex:1;padding:3rem 2rem;max-width:1200px;margin:0 auto;width:100%}.main .tutor-container{max-width:100%;margin:-3rem -2rem 0 -3rem;padding:0;height:calc(100vh - 60px)}.hero{text-align:center;margin-bottom:3rem}.hero h2{font-size:2rem;margin-bottom:1rem;color:var(--ink)}.hero p{font-size:1.1rem;color:var(--muted);margin-bottom:2rem}.features{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:2rem;margin:2rem 0}.feature{background:var(--panel);padding:1.5rem;border-radius:8px;border:1px solid var(--line);text-align:center}.feature h3{font-size:1.3rem;margin-bottom:.5rem;color:var(--red)}.feature p{color:var(--muted);font-size:.95rem}.feature{font-family:inherit;font-size:inherit;cursor:pointer;transition:border-color .2s,transform .2s}button.feature:hover{border-color:var(--red);transform:translateY(-2px)}.placeholder{background:var(--purple-bg);padding:2.5rem;border-radius:8px;border-left:4px solid var(--purple);text-align:center}.placeholder h2{margin-bottom:.75rem}.placeholder p{color:var(--muted)}.account-panel{max-width:420px;margin:0 auto;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:2rem}.account-panel h2{margin-bottom:.75rem}.account-note{font-size:.9rem;color:var(--muted);margin-bottom:1.25rem}.account-email{margin-bottom:.5rem}.account-sync{font-size:.9rem;font-weight:600;color:var(--pos);margin-bottom:1rem}.account-form{display:flex;flex-direction:column;gap:1rem}.account-form label{display:flex;flex-direction:column;gap:.3rem;font-size:.85rem;font-weight:600;color:var(--muted)}.account-form input{padding:.6rem .8rem;border:1px solid var(--line);border-radius:4px;font-size:1rem;font-family:inherit}.account-form input:focus{outline:2px solid var(--red);border-color:transparent}.primary-btn{background:var(--red);color:#fff;border:none;padding:.75rem;border-radius:4px;font-weight:700;font-size:1rem;cursor:pointer}.primary-btn:disabled{opacity:.6;cursor:wait}.secondary-btn{background:var(--panel);color:var(--ink);border:1px solid var(--line);padding:.6rem 1rem;border-radius:4px;font-weight:600;cursor:pointer}.danger-btn{background:var(--neg-bg);color:var(--neg);border:1px solid var(--neg);padding:.6rem 1rem;border-radius:4px;font-weight:600;cursor:pointer}.link-btn{background:none;border:none;color:var(--blue);text-decoration:underline;cursor:pointer;margin-top:1rem;font-size:.9rem}.account-message{margin-top:1rem;font-size:.9rem;font-weight:600}.account-actions{display:flex;gap:.75rem}.study-heading{font-size:1.4rem;margin-bottom:1.5rem;text-align:center}.study-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1.5rem}.category-card{display:flex;flex-direction:column;align-items:flex-start;gap:.4rem;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:1.5rem;text-align:left;font-family:inherit;cursor:pointer;transition:border-color .2s,transform .2s}.category-card:hover{border-color:var(--red);transform:translateY(-2px)}.category-emoji{font-size:2rem}.category-title{font-size:1.15rem;font-weight:700;color:var(--ink)}.category-subtitle{font-size:.9rem;color:var(--muted);line-height:1.4}.category-progress{margin-top:.5rem;font-size:.8rem;font-weight:600;color:var(--pos);background:var(--pos-bg);padding:.25rem .6rem;border-radius:999px}.category-progress.empty{color:var(--muted);background:var(--bg)}.category-card.sprint-card{border-color:var(--purple);background:var(--purple-bg)}.category-card.sprint-card .category-progress{color:var(--purple);background:var(--panel)}.study-viewer{display:flex;flex-direction:column;height:calc(100vh - 60px);margin:-3rem -2rem}.study-viewer-bar{display:flex;align-items:center;gap:1rem;padding:.6rem 1rem;background:var(--panel);border-bottom:1px solid var(--line)}.back-btn{background:var(--red);color:#fff;border:none;padding:.5rem 1rem;border-radius:4px;font-weight:600;cursor:pointer}.back-btn:hover{background:#c0202e}.study-viewer-title{font-weight:700}.study-frame{flex:1;width:100%;border:none;background:var(--bg)}@media (max-width: 768px){.header h1{font-size:1.8rem}.hero h2{font-size:1.5rem}.main{padding:1.5rem 1rem}}
|
assets/index-Dpi-wtlo.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>BrainBit - ICT-Eignungstest Study Suite</title>
|
| 8 |
+
<script type="module" crossorigin src="/assets/index-Dpi-wtlo.js"></script>
|
| 9 |
+
<link rel="stylesheet" crossorigin href="/assets/index-CwQxyZjZ.css">
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<div id="root"></div>
|
| 13 |
+
</body>
|
| 14 |
+
</html>
|