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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions