Skip to content

automatiseren t/m 20 plus en min #24

@riannesteenbeek-cmd

Description

@riannesteenbeek-cmd

import React, { useEffect, useMemo, useRef, useState } from "react";

function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

function makeProblem() {
const isPlus = Math.random() < 0.5;
if (isPlus) {
const a = randomInt(0, 20);
const b = randomInt(0, 20 - a);
return { a, b, op: "+", ans: a + b };
} else {
const a = randomInt(0, 20);
const b = randomInt(0, a);
return { a, b, op: "-", ans: a - b };
}
}

function makeProblemSet(n = 20) {
const arr = [];
for (let i = 0; i < n; i++) arr.push(makeProblem());
return arr;
}

function formatTime(ms) {
const s = Math.floor(ms / 1000);
const mm = String(Math.floor(s / 60)).padStart(2, "0");
const ss = String(s % 60).padStart(2, "0");
return ${mm}:${ss};
}

const SCORES_KEY = "rekenrace_scores_v1"; // nieuw: lijst met scores
const LEGACY_KEY = "rekenrace_v1"; // oud: mogelijk object met bestTime/bestMargin

export default function Rekenrace() {
const TOTAL_STEPS = 20;
const [problems, setProblems] = useState(() => makeProblemSet(TOTAL_STEPS));
const [idx, setIdx] = useState(0);
const [input, setInput] = useState("");
const [catPos, setCatPos] = useState(0);
const [dogPos, setDogPos] = useState(-2);
const [running, setRunning] = useState(false);
const [done, setDone] = useState(false);
const [won, setWon] = useState(false);
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(Date.now());

// Scores (lijst) met veilige migratie vanaf oudere versies
const [scores, setScores] = useState(() => {
try {
const rawNew = localStorage.getItem(SCORES_KEY);
if (rawNew) {
const parsed = JSON.parse(rawNew);
return Array.isArray(parsed) ? parsed : [];
}
const rawLegacy = localStorage.getItem(LEGACY_KEY);
if (rawLegacy) {
const parsedLegacy = JSON.parse(rawLegacy);
// Als de legacy-waarde per ongeluk al een array is: gebruik die
if (Array.isArray(parsedLegacy)) return parsedLegacy;
}
} catch {}
return [];
});

const inputRef = useRef(null);
const timerRef = useRef(null);
const dogTimerRef = useRef(null);

useEffect(() => {
if (running && !done) {
timerRef.current = setInterval(() => setNow(Date.now()), 250);
return () => clearInterval(timerRef.current);
}
}, [running, done]);

useEffect(() => {
if (running && !done) {
const startDelay = setTimeout(() => {
dogTimerRef.current = setInterval(() => {
setDogPos((p) => p + 1);
}, 2000);
}, 3000);
return () => {
clearTimeout(startDelay);
if (dogTimerRef.current) clearInterval(dogTimerRef.current);
};
}
}, [running, done]);

useEffect(() => {
if (!done && running && dogPos >= catPos) {
finish(false);
}tInput("");
setCatPos(0);
setDogPos(-2);
setRunning(true);
setDone(false);
setWon(false);
setStartTime(Date.now());
setNow(Date.now());
}

function stopTimers() {
if (timerRef.current) clearInterval(timerRef.current);
if (dogTimerRef.current) clearInterval(dogTimerRef.current);
}

function finish(didWin) {
stopTimers();
setRunning(false);
setDone(true);
setWon(didWin);

const endTime = Date.now();
const timeMs = startTime ? endTime - startTime : 0;
const margin = Math.max(catPos - dogPos, 0);

try {
  const newScore = { id: crypto?.randomUUID?.() || String(Date.now()), date: new Date().toLocaleString(), won: didWin, timeMs, margin };
  const updated = [...scores, newScore];
  setScores(updated);
  localStorage.setItem(SCORES_KEY, JSON.stringify(updated));
} catch {}

}

const current = problems[idx];
const elapsedMs = startTime ? now - startTime : 0;
const lead = Math.max(catPos - dogPos, 0);

function submit() {
if (!running || done) return;
const val = Number(input.trim());
if (Number.isNaN(val)) return;
const correct = current && val === current.ans;
if (correct) {
setCatPos((p) => p + 1);
const nextIdx = idx + 1;
if (nextIdx >= TOTAL_STEPS) {
setIdx(nextIdx);
setInput("");
finish(true);
} else {
setIdx(nextIdx);
setInput("");
}
} else {
const el = document.getElementById("answerbox");
if (el) {
el.classList.remove("shake");
void el.offsetWidth;
el.classList.add("shake");
}
}
}

const progressPct = (pos) => ${Math.max(0, Math.min(TOTAL_STEPS, pos)) / TOTAL_STEPS * 100}%;

// Samenvatting bovenin
const summary = useMemo(() => {
if (!scores.length) return "Nog geen scores opgeslagen";
const wins = scores.filter(s => s.won);
const bestTimeWin = wins.length ? Math.min(...wins.map(s => s.timeMs)) : null;
const bestMargin = Math.max(...scores.map(s => s.margin));
return [
Gespeeld: ${scores.length},
bestTimeWin != null ? Beste tijd (gewonnen): ${formatTime(bestTimeWin)} : null,
Grootste voorsprong: ${bestMargin} stappen,
].filter(Boolean).join(" · ");
}, [scores]);

return (


<style>{@keyframes shake { 10%, 90% { transform: translateX(-1px); } 20%, 80% { transform: translateX(2px); } 30%, 50%, 70% { transform: translateX(-4px); } 40%, 60% { transform: translateX(4px); } } .shake { animation: shake 0.3s; }}</style>

  <div className="w-full max-w-3xl">
    <div className="mb-4 flex items-center justify-between">
      <h1 className="text-2xl font-bold">Rekenrace: Kat 🐱 vs Hond 🐶</h1>
      <div className="text-sm text-slate-600">{summary}</div>
    </div>

    {/* Track */}
    <div className="relative w-full h-24 bg-white rounded-2xl shadow p-4">
      <div className="absolute left-4 right-4 top-1/2 -translate-y-1/2 h-2 bg-slate-200 rounded-full" />
      <div className="absolute left-4 top-2 text-xs text-slate-500">Start</div>
      <div className="absolute right-4 top-2 text-xs text-slate-500">Finish 🏁</div>

      {/* Kat */}
      <div
        className="absolute -translate-x-1/2 -translate-y-1/2 text-2xl transition-all duration-300"
        style={{ left: `calc(1rem + ${progressPct(catPos)} * 0.92)`, top: "50%" }}
      >
        🐱
      </div>

      {/* Hond */}
      <div
        className="absolute -translate-x-1/2 -translate-y-1/2 text-2xl transition-all duration-500"
        style={{ left: `calc(1rem + ${progressPct(dogPos)} * 0.92)`, top: "50%" }}
      >
        🐶
      </div>
    </div>

    {/* HUD */}
    <div className="mt-4 grid grid-cols-2 gap-4">
      <div className="bg-white rounded-2xl shadow p-4">
        <div className="text-xs uppercase tracking-wide text-slate-500">Tijd</div>
        <div className="text-2xl font-semibold">{running ? formatTime(elapsedMs) : done && startTime ? formatTime(elapsedMs) : "00:00"}</div>
      </div>
      <div className="bg-white rounded-2xl shadow p-4">
        <div className="text-xs uppercase tracking-wide text-slate-500">Voorsprong</div>
        <div className={`text-2xl font-semibold ${lead <= 2 && running ? "text-rose-600" : ""}`}>{lead} stappen</div>
      </div>
    </div>

    {/* Problem area */}
    <div className="mt-4 bg-white rounded-2xl shadow p-4">
      {!running && !done && (
        <div className="flex items-center justify-between">
          <div>
            <h2 className="text-lg font-semibold">Maak 20 sommen goed voordat de hond je inhaalt!</h2>
            <p className="text-slate-600 text-sm mt-1">Plus en min tot en met 20. De hond start na 3 seconden en loopt 1 stap per 2 seconden.</p>
          </div>
          <button
            className="px-4 py-2 rounded-xl bg-indigo-600 text-white shadow hover:bg-indigo-700"
            onClick={start}
          >
            Start
          </button>
        </div>
      )}

      {running && !done && current && (
        <div className="flex items-center gap-3 mt-2">
          <div className="text-lg">Som {idx + 1} / {TOTAL_STEPS}</div>
          <div className="flex-1" />
          <div className="text-2xl font-bold tabular-nums">{current.a} {current.op} {current.b} =</div>
          <input
            id="answerbox"
            ref={inputRef}
            type="number"
            inputMode="numeric"
            pattern="[0-9]*"
            className="w-24 text-2xl font-semibold px-3 py-1 rounded-xl border border-slate-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") submit(); }}
          />
          <button
            className="px-4 py-2 rounded-xl bg-indigo-600 text-white shadow hover:bg-indigo-700"
            onClick={submit}
          >
            OK
          </button>
        </div>
      )}

      {done && (
        <div>
          <div className="flex items-center justify-between">
            <div>
              <h2 className="text-xl font-semibold">{won ? "Gewonnen! 🎉 Je bent veilig." : "Oeps! De hond heeft je gepakt."}</h2>
              <p className="text-slate-600 mt-1">Tijd: {formatTime(elapsedMs)} · Voorsprong: {lead} stappen</p>
            </div>
            <div className="flex items-center gap-2">
              <button
                className="px-4 py-2 rounded-xl bg-slate-100 text-slate-800 shadow hover:bg-slate-200"
                onClick={() => {
                  setIdx(0);
                  setInput("");
                  setCatPos(0);
                  setDogPos(-2);
                  setRunning(true);
                  setDone(false);
                  setWon(false);
                  setStartTime(Date.now());
                  setNow(Date.now());
                }}
              >
                Nog een keer (zelfde sommen)
              </button>
              <button
                className="px-4 py-2 rounded-xl bg-indigo-600 text-white shadow hover:bg-indigo-700"
                onClick={start}
              >
                Nieuwe ronde
              </button>
            </div>
          </div>
          <div className="mt-4">
            <div className="flex items-center justify-between mb-2">
              <h3 className="text-lg font-semibold">Eerdere scores</h3>
              <div className="flex items-center gap-2">
                <button
                  className="px-3 py-1 rounded-lg bg-slate-100 text-slate-800 hover:bg-slate-200 text-sm"
                  onClick={() => {
                    const sorted = scores.slice().sort((a,b)=> a.timeMs - b.timeMs);
                    setScores(sorted);
                    localStorage.setItem(SCORES_KEY, JSON.stringify(sorted));
                  }}
                >
                  Sorteer op tijd
                </button>
                <button
                  className="px-3 py-1 rounded-lg bg-rose-100 text-rose-700 hover:bg-rose-200 text-sm"
                  onClick={() => {
                    if (confirm("Weet je zeker dat je alle scores wilt verwijderen?")) {
                      setScores([]);
                      localStorage.setItem(SCORES_KEY, JSON.stringify([]));
                    }
                  }}
                >
                  Wis scores
                </button>
              </div>
            </div>
            <div className="max-h-48 overflow-y-auto text-sm">
              <table className="w-full text-left border-collapse">
                <thead>
                  <tr className="border-b">
                    <th className="py-1 pr-2">Datum</th>
                    <th className="py-1 pr-2">Resultaat</th>
                    <th className="py-1 pr-2">Tijd</th>
                    <th className="py-1">Voorsprong</th>
                  </tr>
                </thead>
                <tbody>
                  {scores.length ? (
                    scores.slice().reverse().map((s) => (
                      <tr key={s.id} className="border-b last:border-0">
                        <td className="py-1 pr-2 whitespace-nowrap">{s.date}</td>
                        <td className="py-1 pr-2">{s.won ? "Gewonnen" : "Verloren"}</td>
                        <td className="py-1 pr-2">{formatTime(s.timeMs)}</td>
                        <td className="py-1">{s.margin} stappen</td>
                      </tr>
                    ))
                  ) : (
                    <tr><td colSpan={4} className="text-slate-500 text-center py-2">Nog geen scores</td></tr>
                  )}
                </tbody>
              </table>
            </div>
          </div>
        </div>
      )}
    </div>

    {/* Uitleg */}
    <div className="mt-4 bg-white rounded-2xl shadow p-4">
      <details>
        <summary className="cursor-pointer font-medium">Uitleg</summary>
        <ul className="list-disc pl-5 mt-2 text-slate-700 text-sm">
          <li>Er zijn 20 sommen (optellen en aftrekken) met uitkomsten tussen 0 en 20.</li>
          <li>Elke goede som = 1 stap vooruit voor de kat 🐱.</li>
          <li>De hond 🐶 start 3 seconden na de start en loopt 1 stap per 2 seconden vanzelf.</li>
          <li>Bereik de finish (20 stappen) voordat de hond je inhaalt.</li>
          <li>Scores worden lokaal opgeslagen op dit apparaat. Je kunt ze sorteren of wissen.</li>
        </ul>
      </details>
    </div>

    <div className="mt-4 text-center text-xs text-slate-500">© {new Date().getFullYear()} Rekenrace — lokaal opslaan via je browser</div>
  </div>
</div>

);
}

}, [dogPos]);

useEffect(() => {
if (inputRef.current) inputRef.current.focus();
}, [idx, running]);

function start() {
setProblems(makeProblemSet(TOTAL_STEPS));
setIdx(0);
se

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions