{"id":51255,"date":"2026-09-19T19:31:11","date_gmt":"2026-09-19T18:31:11","guid":{"rendered":"https:\/\/cctvacademy.be\/?page_id=51255"},"modified":"2026-09-20T10:29:43","modified_gmt":"2026-09-20T09:29:43","slug":"cctv-pacman","status":"publish","type":"page","link":"https:\/\/cctvacademy.be\/nl\/cctv-pacman\/","title":{"rendered":"CCTV Pacman"},"content":{"rendered":"\t\t<div data-elementor-type=\"masteriyo-course-archive-page\" data-elementor-id=\"51255\" class=\"elementor elementor-51255\">\n\t\t\t\t<div class=\"elementor-element elementor-element-61ff1eb e-flex e-con-boxed e-con e-parent\" data-id=\"61ff1eb\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-051539b elementor-widget elementor-widget-spacer\" data-id=\"051539b\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-b5436fd e-flex e-con-boxed e-con e-parent\" data-id=\"b5436fd\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-9b8fe6c elementor-widget elementor-widget-button\" data-id=\"9b8fe6c\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"button.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<div class=\"elementor-button-wrapper\">\n\t\t\t\t\t<a class=\"elementor-button elementor-button-link elementor-size-sm\" href=\"https:\/\/cctvacademy.be\">\n\t\t\t\t\t\t<span class=\"elementor-button-content-wrapper\">\n\t\t\t\t\t\t\t\t\t<span class=\"elementor-button-text\">Home<\/span>\n\t\t\t\t\t<\/span>\n\t\t\t\t\t<\/a>\n\t\t\t\t<\/div>\n\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-b3d7bcf e-flex e-con-boxed e-con e-parent\" data-id=\"b3d7bcf\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-bb89146 elementor-widget elementor-widget-text-editor\" data-id=\"bb89146\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p><span style=\"color: #ffffff;\">Under Construction<\/span><\/p><p>\u00a0<\/p><p>\u00a0<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-3df00bd e-flex e-con-boxed e-con e-parent\" data-id=\"3df00bd\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-1c88fcd elementor-widget elementor-widget-video\" data-id=\"1c88fcd\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;video_type&quot;:&quot;hosted&quot;,&quot;controls&quot;:&quot;yes&quot;}\" data-widget_type=\"video.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"e-hosted-video elementor-wrapper elementor-open-inline\">\n\t\t\t\t\t<video class=\"elementor-video\" src=\"https:\/\/cctvacademy.be\/wp-content\/uploads\/2026\/09\/cctv-pacmaze-intro-movie-2-cctv-academy.mp4\" controls=\"\" preload=\"metadata\" controlsList=\"nodownload\"><\/video>\n\t\t\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-8b0f16f e-flex e-con-boxed e-con e-parent\" data-id=\"8b0f16f\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-b21b8b9 elementor-widget elementor-widget-spacer\" data-id=\"b21b8b9\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-a5744b7 e-flex e-con-boxed e-con e-parent\" data-id=\"a5744b7\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-762068e elementor-widget elementor-widget-html\" data-id=\"762068e\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!DOCTYPE html>\n<html lang=\"nl\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\">\n<title>CCTV PAC-MAZE \u2014 Night Watch<\/title>\n<style>\n:root{\n  --bg:#020609;--panel:#07131a;--panel2:#0a1d27;--line:#20d8ff;--green:#8dff75;\n  --warning:#ffd166;--danger:#ff4d6d;--text:#eefcff;--muted:#7da4b3;\n}\n*{box-sizing:border-box}\nhtml,body{margin:0;min-height:100%;background:#020609;color:var(--text);font-family:Inter,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}\nbody{padding:16px;background:radial-gradient(circle at 20% 0,rgba(32,216,255,.08),transparent 28%),radial-gradient(circle at 90% 90%,rgba(141,255,117,.05),transparent 30%),#020609}\n#cctvGame{max-width:1120px;margin:auto;outline:none}\n.brand{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:12px}\n.brand h1{margin:0;font-size:clamp(26px,4.4vw,48px);line-height:.95;letter-spacing:.07em}\n.brand .cctv{color:#fff;text-shadow:0 0 8px rgba(255,255,255,.75),0 0 22px rgba(255,255,255,.25)}\n.brand .pac{color:var(--green);text-shadow:0 0 18px rgba(141,255,117,.28)}\n.brandMeta{font-size:11px;color:var(--muted);text-align:right;line-height:1.5}\n.layout{display:grid;grid-template-columns:minmax(0,1fr) 290px;gap:14px;align-items:start}\n.gamePanel,.card{background:linear-gradient(180deg,rgba(9,27,37,.96),rgba(3,12,17,.98));border:1px solid rgba(32,216,255,.24);border-radius:16px}\n.gamePanel{padding:10px;overflow:hidden}\n.hud{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:7px;margin-bottom:9px}\n.hudBox{min-width:0;padding:7px 8px;border:1px solid rgba(125,164,179,.17);border-radius:9px;background:#041018}\n.hudLabel{font-size:9px;text-transform:uppercase;letter-spacing:.13em;color:var(--muted);white-space:nowrap}\n.hudValue{font-weight:900;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px}\n.meter{height:4px;background:#102934;border-radius:99px;overflow:hidden;margin-top:5px}\n.meter>i{display:block;height:100%;width:100%;background:linear-gradient(90deg,var(--line),var(--green));transition:width .08s linear}\n.gameStage{position:relative;overflow:hidden;border:1px solid rgba(32,216,255,.32);border-radius:12px;background:#000;aspect-ratio:10\/7;touch-action:none}\n.gameStage:after{content:\"\";position:absolute;inset:0;z-index:4;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(255,255,255,.017) 0,rgba(255,255,255,.017) 1px,transparent 1px,transparent 4px);mix-blend-mode:screen}\ncanvas{display:block;width:100%;height:100%;outline:none;background:#000}\n.overlay{position:absolute;inset:0;z-index:10;display:grid;place-items:center;padding:22px;background:linear-gradient(rgba(0,7,10,.86),rgba(0,9,13,.97));transition:opacity .2s ease}\n.overlay.hidden{opacity:0;pointer-events:none}\n.intro{width:min(510px,100%);text-align:center}\n.eyebrow{font-size:10px;letter-spacing:.18em;color:var(--line);text-transform:uppercase}\n.intro h2{font-size:clamp(31px,7vw,58px);line-height:.93;margin:8px 0 10px}.intro h2 .white{color:#fff}.intro h2 b{color:var(--green)}\n.intro p{max-width:470px;margin:0 auto 14px;color:#b1d1db;font-size:13px;line-height:1.55}\n.tips{display:grid;grid-template-columns:1fr 1fr;gap:7px;text-align:left;margin:12px 0}\n.tip{padding:9px;border-radius:9px;background:rgba(7,25,34,.8);border:1px solid rgba(125,164,179,.15);font-size:11px;line-height:1.35;color:#b7d4dc}.tip b{color:#fff}\n.startRow{display:flex;gap:8px;margin-top:10px}\ninput,button{font:inherit}input{min-width:0;flex:1;padding:12px;border-radius:10px;border:1px solid rgba(32,216,255,.34);background:#020b10;color:#fff;outline:none;text-transform:uppercase;font-size:16px}input:focus{border-color:var(--line);box-shadow:0 0 0 3px rgba(32,216,255,.08)}\n.btn{min-height:46px;border:0;border-radius:10px;padding:11px 15px;cursor:pointer;font-weight:950;letter-spacing:.06em;color:#001015;background:linear-gradient(135deg,var(--line),var(--green))}.btn:active{transform:scale(.98)}.btn.secondary{margin-top:8px;color:var(--text);background:#0a202a;border:1px solid rgba(125,164,179,.25)}\n.startNote{font-size:9px;letter-spacing:.12em;color:var(--muted);margin-top:10px}\n.side{display:flex;flex-direction:column;gap:12px}.card{padding:14px}.card h3{font-size:12px;color:var(--line);letter-spacing:.12em;text-transform:uppercase;margin:0 0 10px}\n.scoreTable{width:100%;border-collapse:collapse;font-size:11px}.scoreTable th{font-size:9px;color:var(--muted);text-align:left;border-bottom:1px solid rgba(125,164,179,.14);padding:0 0 6px}.scoreTable td{padding:7px 0;border-bottom:1px solid rgba(125,164,179,.07)}.scoreTable td:first-child{color:var(--warning);width:28px}.scoreTable td:last-child{text-align:right;font-weight:900}\n.legend{display:grid;gap:8px;font-size:11px;line-height:1.4;color:#b3d2da}.legendRow{display:grid;grid-template-columns:26px 1fr;gap:7px;align-items:start}.key{display:grid;place-items:center;min-width:23px;height:23px;background:#071820;border:1px solid rgba(125,164,179,.25);border-bottom-width:3px;border-radius:5px;color:#fff;font-size:10px}.status{margin-top:10px;padding-top:10px;border-top:1px solid rgba(125,164,179,.12);font-size:10px;color:var(--muted);line-height:1.45}\n.touchControls{display:none;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px;user-select:none;touch-action:none}.dpad{width:148px;height:148px;margin:auto;display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(3,1fr);gap:5px}.touchBtn{min-width:44px;min-height:44px;border-radius:11px;border:1px solid rgba(32,216,255,.28);background:#0a202b;color:#e7fbff;font-weight:900;font-size:20px}.flashWrap{display:grid;place-items:center;align-content:center;gap:8px}.flashBtn{width:108px;height:108px;border-radius:50%;font-size:12px;letter-spacing:.08em;color:#1d1400;background:radial-gradient(circle at 35% 30%,#fff,#ffe78a 22%,#ffb13b 65%,#ff5f3d);border:4px solid rgba(255,255,255,.12)}.sentryBtn{min-width:108px;padding:10px 12px;font-size:11px;letter-spacing:.09em;color:#dffff2;background:#092419;border-color:rgba(141,255,117,.42)}.sentryBtn.active{color:#00150a;background:linear-gradient(135deg,#8dff75,#20d8ff);box-shadow:0 0 18px rgba(141,255,117,.28)}\n.footer{text-align:center;color:#42636d;font-size:9px;letter-spacing:.11em;margin-top:9px}\n@media (hover:hover) and (pointer:fine){.btn:hover{filter:brightness(1.08);transform:translateY(-1px)}.touchBtn:hover{background:#10303f}}\n@media(max-width:900px){.layout{grid-template-columns:1fr}.side{display:grid;grid-template-columns:1fr 1fr}.touchControls{display:grid}.brandMeta{display:none}}\n@media(max-width:620px){body{padding:7px}.gamePanel{padding:7px}.hud{grid-template-columns:repeat(3,1fr)}.side{grid-template-columns:1fr}.tips{grid-template-columns:1fr}.startRow{flex-direction:column}.touchControls{grid-template-columns:1fr 1fr}.dpad{width:132px;height:132px}.flashBtn{width:96px;height:96px}}\n<\/style>\n<\/head>\n<body>\n<div id=\"cctvGame\" tabindex=\"0\">\n  <div class=\"brand\">\n    <h1><span class=\"cctv\">CCTV<\/span> <span class=\"pac\">PAC-MAZE<\/span><\/h1>\n    <div class=\"brandMeta\">NIGHT WATCH \/\/ LIVE FEED<br>SECURITY MAZE CHASE<\/div>\n  <\/div>\n\n  <div class=\"layout\">\n    <section class=\"gamePanel\">\n      <div class=\"hud\">\n        <div class=\"hudBox\"><div class=\"hudLabel\">Score<\/div><div class=\"hudValue\" id=\"scoreEl\">000000<\/div><\/div>\n        <div class=\"hudBox\"><div class=\"hudLabel\">High<\/div><div class=\"hudValue\" id=\"highEl\">000000<\/div><\/div>\n        <div class=\"hudBox\"><div class=\"hudLabel\">Level<\/div><div class=\"hudValue\" id=\"levelEl\">01<\/div><\/div>\n        <div class=\"hudBox\"><div class=\"hudLabel\">Cams<\/div><div class=\"hudValue\" id=\"livesEl\">\ud83d\udcf9 \ud83d\udcf9 \ud83d\udcf9<\/div><\/div>\n        <div class=\"hudBox\"><div class=\"hudLabel\">Evidence<\/div><div class=\"hudValue\" id=\"progressEl\">0%<\/div><div class=\"meter\"><i id=\"progressFill\"><\/i><\/div><\/div>\n        <div class=\"hudBox\"><div class=\"hudLabel\">Flash<\/div><div class=\"hudValue\" id=\"flashText\">READY<\/div><div class=\"meter\"><i id=\"flashFill\"><\/i><\/div><\/div>\n      <\/div>\n\n      <div class=\"gameStage\" id=\"stage\">\n        <canvas id=\"gameCanvas\" width=\"800\" height=\"560\" aria-label=\"Interactief CCTV Pac-Maze spel\"><\/canvas>\n\n        <div class=\"overlay\" id=\"overlay\">\n          <div class=\"intro\">\n            <div class=\"eyebrow\" id=\"overlayEyebrow\">SECURITY SYSTEM \/\/ STANDBY<\/div>\n            <h2 id=\"overlayTitle\"><span class=\"white\">CCTV<\/span> <b>NIGHT WATCH<\/b><\/h2>\n            <p id=\"overlayText\">Beweeg continu door het beveiligingsdoolhof zoals in de video. Kies v\u00f3\u00f3r een kruispunt al je volgende richting: het spel onthoudt die toets en draait automatisch zodra het kan.<\/p>\n\n            <div class=\"tips\" id=\"introTips\">\n              <div class=\"tip\"><b>Continue beweging<\/b><br>\u2191 \u2193 \u2190 \u2192 of W A S D. Je volgende bocht wordt onthouden.<\/div>\n              <div class=\"tip\"><b>Camera-flits<\/b><br>SPATIE verdooft inbrekers die v\u00f3\u00f3r de lens staan.<\/div>\n              <div class=\"tip\"><b>Alarm-orb \u25c9<\/b><br>Maakt inbrekers tijdelijk kwetsbaar. Raak ze dan voor combo-punten.<\/div>\n              <div class=\"tip\"><b>Sentry-modus<\/b><br>Druk E: stop op een blok, scan 360\u00b0 en flash automatisch zodra een inbreker in de volledige gang verschijnt.<\/div>\n            <\/div>\n\n            <div class=\"startRow\">\n              <input id=\"nameInput\" maxlength=\"12\" value=\"SECURITY\" autocomplete=\"off\" aria-label=\"Operatornaam\">\n              <button type=\"button\" class=\"btn\" id=\"startBtn\">\u25b6 START GAME<\/button>\n            <\/div>\n            <button type=\"button\" class=\"btn secondary\" id=\"soundBtn\">\ud83d\udd0a GELUID AAN<\/button>\n            <div class=\"startNote\">HET SPEL BEGINT PAS NADAT JE OP START GAME DRUKT<\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"touchControls\" aria-label=\"Touchbesturing\">\n        <div class=\"dpad\">\n          <span><\/span><button type=\"button\" class=\"touchBtn\" data-dir=\"up\">\u25b2<\/button><span><\/span>\n          <button type=\"button\" class=\"touchBtn\" data-dir=\"left\">\u25c0<\/button><button type=\"button\" class=\"touchBtn\" data-dir=\"down\">\u25bc<\/button><button type=\"button\" class=\"touchBtn\" data-dir=\"right\">\u25b6<\/button>\n          <span><\/span><span><\/span><span><\/span>\n        <\/div>\n        <div class=\"flashWrap\"><button type=\"button\" class=\"touchBtn flashBtn\" id=\"flashTouch\">FLASH<\/button><button type=\"button\" class=\"touchBtn sentryBtn\" id=\"sentryTouch\">SENTRY<\/button><\/div>\n      <\/div>\n    <\/section>\n\n    <aside class=\"side\">\n      <section class=\"card\">\n        <h3>Local High Scores<\/h3>\n        <table class=\"scoreTable\">\n          <thead><tr><th>#<\/th><th>Operator<\/th><th>Score<\/th><\/tr><\/thead>\n          <tbody id=\"scoreTable\"><\/tbody>\n        <\/table>\n      <\/section>\n      <section class=\"card\">\n        <h3>Security Protocol<\/h3>\n        <div class=\"legend\">\n          <div class=\"legendRow\"><span class=\"key\">\u25cf<\/span><span><b>Evidence<\/b> \u2014 10 punten per datapunt.<\/span><\/div>\n          <div class=\"legendRow\"><span class=\"key\">\u25c9<\/span><span><b>Alarm<\/b> \u2014 meerdere alarmzones per level; inbrekers vluchten en kunnen worden gepakt.<\/span><\/div>\n          <div class=\"legendRow\"><span class=\"key\">\u25a3<\/span><span><b>Evidence bag<\/b> \u2014 tijdelijke bonus die in het level verschijnt.<\/span><\/div>\n          <div class=\"legendRow\"><span class=\"key\">\u2423<\/span><span><b>Flash<\/b> \u2014 krachtige lichtbundel die zichtbare inbrekers in je kijkrichting verdooft.<\/span><\/div>\n          <div class=\"legendRow\"><span class=\"key\">E<\/span><span><b>Sentry<\/b> \u2014 camera stopt, draait rond en bewaakt de volledige zichtbare gang. Auto-flash bij detectie.<\/span><\/div><div class=\"legendRow\"><span class=\"key\">P<\/span><span><b>Pauze<\/b> \u2014 M schakelt geluid aan of uit.<\/span><\/div>\n        <\/div>\n        <div class=\"status\" id=\"statusLine\">Wacht op operator\u2026<\/div>\n      <\/section>\n    <\/aside>\n  <\/div>\n  <div class=\"footer\">CCTV NIGHT WATCH \/\/ INTERACTIVE SECURITY FEED<\/div>\n<\/div>\n\n<script>\n(() => {\n'use strict';\n\nconst root = document.getElementById('cctvGame');\nconst canvas = document.getElementById('gameCanvas');\nconst ctx = canvas.getContext('2d');\nconst overlay = document.getElementById('overlay');\nconst overlayEyebrow = document.getElementById('overlayEyebrow');\nconst overlayTitle = document.getElementById('overlayTitle');\nconst overlayText = document.getElementById('overlayText');\nconst introTips = document.getElementById('introTips');\nconst startBtn = document.getElementById('startBtn');\nconst soundBtn = document.getElementById('soundBtn');\nconst nameInput = document.getElementById('nameInput');\nconst scoreEl = document.getElementById('scoreEl');\nconst highEl = document.getElementById('highEl');\nconst levelEl = document.getElementById('levelEl');\nconst livesEl = document.getElementById('livesEl');\nconst progressEl = document.getElementById('progressEl');\nconst progressFill = document.getElementById('progressFill');\nconst flashText = document.getElementById('flashText');\nconst flashFill = document.getElementById('flashFill');\nconst scoreTable = document.getElementById('scoreTable');\nconst statusLine = document.getElementById('statusLine');\nconst sentryTouch = document.getElementById('sentryTouch');\n\nconst VIEW_W = 800;\nconst VIEW_H = 560;\nconst TILE = 28;\nconst MAZE_W = 31;\nconst MAZE_H = 39;\nconst TUNNEL_ROW = 19;\nconst WORLD_W = MAZE_W * TILE;\nconst WORLD_H = MAZE_H * TILE;\nconst TAU = Math.PI * 2;\n\nconst DPR = Math.min(window.devicePixelRatio || 1, 2);\ncanvas.width = VIEW_W * DPR;\ncanvas.height = VIEW_H * DPR;\nctx.setTransform(DPR,0,0,DPR,0,0);\n\nconst DIRS = {\n  up:{x:0,y:-1,name:'up'},\n  down:{x:0,y:1,name:'down'},\n  left:{x:-1,y:0,name:'left'},\n  right:{x:1,y:0,name:'right'},\n  none:{x:0,y:0,name:'none'}\n};\nconst CARDINALS = [DIRS.up,DIRS.left,DIRS.down,DIRS.right];\n\nlet memoryScores = [];\nlet audioCtx = null;\n\nconst state = {\n  phase:'intro', paused:false, score:0, level:1, lives:3, name:'SECURITY',\n  map:[], player:null, burglars:[], dots:0, initialDots:0, collected:0,\n  exposed:0, exposedCombo:0, flashCooldown:0, flashFx:0, flashPulse:0, invuln:0,\n  bonus:null, bonusMilestones:[], banner:'', bannerTime:0, readyTimer:0,\n  sound:true, shake:0, chaseMode:true, modeTimer:10,\n  viewX:0, viewY:0, lastTime:performance.now(), levelSeed:0,\n  sentry:false, sentryPending:false, sentryTurnTimer:0, sentryHold:0, sentryDetected:false, sentryScanPulse:0, sentryResumeDir:null\n};\n\nfunction clamp(v,a,b){ return Math.max(a,Math.min(b,v)); }\nfunction dist(a,b){ return Math.hypot(a.x-b.x,a.y-b.y); }\nfunction reverseOf(a,b){ return !!a && !!b && a.x===-b.x && a.y===-b.y; }\nfunction formatScore(n){ return String(Math.max(0,Math.floor(n))).padStart(6,'0'); }\nfunction escapeHtml(value){ return String(value).replace(\/[&<>'\"]\/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;',\"'\":'&#39;','\"':'&quot;'}[ch])); }\nfunction seeded(seed){ let a=seed|0; return () => { a=(a+0x6D2B79F5)|0; let t=a; t=Math.imul(t^(t>>>15),t|1); t^=t+Math.imul(t^(t>>>7),t|61); return ((t^(t>>>14))>>>0)\/4294967296; }; }\nfunction shuffle(array,rnd){ for(let i=array.length-1;i>0;i--){ const j=Math.floor(rnd()*(i+1)); [array[i],array[j]]=[array[j],array[i]]; } return array; }\n\nfunction ensureAudio(){\n  if(!state.sound) return;\n  const AC = window.AudioContext || window.webkitAudioContext;\n  if(!AC) return;\n  if(!audioCtx) audioCtx = new AC();\n  if(audioCtx.state==='suspended') audioCtx.resume();\n}\nfunction beep(freq=440,dur=.05,type='square',vol=.02,slide=0){\n  if(!state.sound) return;\n  ensureAudio();\n  if(!audioCtx) return;\n  const o=audioCtx.createOscillator(), g=audioCtx.createGain(), t=audioCtx.currentTime;\n  o.type=type; o.frequency.setValueAtTime(freq,t);\n  if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(40,freq+slide),t+dur);\n  g.gain.setValueAtTime(vol,t); g.gain.exponentialRampToValueAtTime(.0001,t+dur);\n  o.connect(g); g.connect(audioCtx.destination); o.start(t); o.stop(t+dur);\n}\nfunction sfx(type){\n  if(type==='dot') beep(620,.018,'square',.009,70);\n  if(type==='orb'){ beep(190,.11,'sawtooth',.03,520); setTimeout(()=>beep(720,.08,'square',.02,-140),70); }\n  if(type==='flash') beep(1050,.055,'sawtooth',.025,-650);\n  if(type==='stun') beep(260,.08,'square',.022,190);\n  if(type==='capture'){ beep(430,.07,'square',.028,320); setTimeout(()=>beep(780,.07,'square',.02,80),65); }\n  if(type==='hit'){ beep(160,.17,'sawtooth',.04,-90); setTimeout(()=>beep(85,.20,'square',.02,-20),100); }\n  if(type==='bonus'){ [820,1060,1320].forEach((f,i)=>setTimeout(()=>beep(f,.06,'triangle',.02),i*50)); }\n  if(type==='level'){ [440,560,680,900].forEach((f,i)=>setTimeout(()=>beep(f,.09,'square',.025,60),i*80)); }\n}\n\nfunction generateMaze(level){\n  const rnd=seeded(24681357 + level*7919);\n  const grid=Array.from({length:MAZE_H},()=>Array(MAZE_W).fill('#'));\n  const stack=[[1,1]]; grid[1][1]=' ';\n  while(stack.length){\n    const [x,y]=stack[stack.length-1];\n    const dirs=shuffle([[2,0],[-2,0],[0,2],[0,-2]],rnd).filter(([dx,dy])=>{\n      const nx=x+dx, ny=y+dy;\n      return nx>0 && ny>0 && nx<MAZE_W-1 && ny<MAZE_H-1 && grid[ny][nx]==='#';\n    });\n    if(!dirs.length){ stack.pop(); continue; }\n    const [dx,dy]=dirs[0];\n    grid[y+dy\/2][x+dx\/2]=' ';\n    grid[y+dy][x+dx]=' ';\n    stack.push([x+dx,y+dy]);\n  }\n\n  \/\/ Extra lussen maken: hierdoor speelt het meer als een arcade-maze en minder als een doodlopend doolhof.\n  const loopCount=34 + Math.min(24,level*2);\n  for(let i=0;i<loopCount;i++){\n    const x=1+Math.floor(rnd()*(MAZE_W-2));\n    const y=1+Math.floor(rnd()*(MAZE_H-2));\n    if(grid[y][x]!=='#') continue;\n    const horizontal=grid[y][x-1]!=='#' && grid[y][x+1]!=='#';\n    const vertical=grid[y-1][x]!=='#' && grid[y+1][x]!=='#';\n    if(horizontal || vertical) grid[y][x]=' ';\n  }\n\n  \/\/ Centrale inbrekersruimte.\n  const cx=Math.floor(MAZE_W\/2), cy=TUNNEL_ROW;\n  for(let y=cy-2;y<=cy+2;y++) for(let x=cx-3;x<=cx+3;x++) grid[y][x]=' ';\n  \/\/ Horizontale tunnel zoals in klassieke maze-games.\n  grid[TUNNEL_ROW][0]=' '; grid[TUNNEL_ROW][1]=' ';\n  grid[TUNNEL_ROW][MAZE_W-2]=' '; grid[TUNNEL_ROW][MAZE_W-1]=' ';\n\n  \/\/ Alle begaanbare tegels krijgen evidence, behalve de centrale basis.\n  for(let y=0;y<MAZE_H;y++) for(let x=0;x<MAZE_W;x++){\n    if(grid[y][x]===' ') grid[y][x]='.';\n  }\n  for(let y=cy-2;y<=cy+2;y++) for(let x=cx-3;x<=cx+3;x++) grid[y][x]=' ';\n  grid[TUNNEL_ROW][0]=' '; grid[TUNNEL_ROW][MAZE_W-1]=' ';\n\n  function nearestFloor(tx,ty){\n    let best=null,bestD=Infinity;\n    for(let y=1;y<MAZE_H-1;y++) for(let x=1;x<MAZE_W-1;x++){\n      if(grid[y][x]==='#') continue;\n      const d=Math.abs(x-tx)+Math.abs(y-ty);\n      if(d<bestD){bestD=d;best={x,y};}\n    }\n    return best;\n  }\n  \/\/ Extra alarm-orbs verspreid over het volledige doolhof.\n  \/\/ We kiezen ze met afstand tot bestaande alarmen zodat ze niet allemaal samenklonteren.\n  const alarmSeeds=[[1,1],[MAZE_W-2,1],[1,MAZE_H-2],[MAZE_W-2,MAZE_H-2],[Math.floor(MAZE_W\/2),3],[3,Math.floor(MAZE_H\/2)],[MAZE_W-4,Math.floor(MAZE_H\/2)],[Math.floor(MAZE_W\/2),MAZE_H-4]];\n  const alarmCells=[];\n  for(const [sx,sy] of alarmSeeds){\n    const p=nearestFloor(sx,sy);\n    if(p && !alarmCells.some(a=>a.x===p.x&&a.y===p.y)){ grid[p.y][p.x]='o'; alarmCells.push(p); }\n  }\n  const extraAlarmCount=4+Math.min(8,Math.floor(level\/2));\n  const candidates=[];\n  for(let y=1;y<MAZE_H-1;y++) for(let x=1;x<MAZE_W-1;x++){\n    if(grid[y][x]!=='.') continue;\n    const dc=Math.abs(x-cx)+Math.abs(y-cy);\n    if(dc<6) continue;\n    candidates.push({x,y,noise:rnd()});\n  }\n  for(let n=0;n<extraAlarmCount && candidates.length;n++){\n    candidates.sort((a,b)=>{\n      const da=alarmCells.length?Math.min(...alarmCells.map(q=>Math.abs(a.x-q.x)+Math.abs(a.y-q.y))):99;\n      const db=alarmCells.length?Math.min(...alarmCells.map(q=>Math.abs(b.x-q.x)+Math.abs(b.y-q.y))):99;\n      return (db+b.noise*3)-(da+a.noise*3);\n    });\n    const p=candidates.shift();\n    if(!p) break;\n    grid[p.y][p.x]='o'; alarmCells.push({x:p.x,y:p.y});\n    for(let i=candidates.length-1;i>=0;i--){\n      if(Math.abs(candidates[i].x-p.x)+Math.abs(candidates[i].y-p.y)<5) candidates.splice(i,1);\n    }\n  }\n  return grid;\n}\n\nfunction isPassable(col,row){\n  if(row===TUNNEL_ROW && (col<0 || col>=MAZE_W)) return true;\n  return row>=0 && row<MAZE_H && col>=0 && col<MAZE_W && state.map[row][col]!=='#';\n}\nfunction canStep(col,row,dir){ return isPassable(col+dir.x,row+dir.y); }\nfunction applyWrap(actor){\n  if(Math.round(actor.y)!==TUNNEL_ROW) return;\n  if(actor.x>=MAZE_W) actor.x=0;\n  if(actor.x<=-1) actor.x=MAZE_W-1;\n}\nfunction nearestPassable(tx,ty){\n  let best={x:1,y:1}, bestD=Infinity;\n  for(let y=0;y<MAZE_H;y++) for(let x=0;x<MAZE_W;x++){\n    if(!isPassable(x,y)) continue;\n    const d=Math.abs(x-tx)+Math.abs(y-ty);\n    if(d<bestD){bestD=d;best={x,y};}\n  }\n  return best;\n}\nfunction chooseInitialDir(p){\n  const prefs=[DIRS.right,DIRS.up,DIRS.left,DIRS.down];\n  return prefs.find(d=>canStep(p.x,p.y,d)) || DIRS.none;\n}\n\nfunction makeBurglar(index){\n  const cx=Math.floor(MAZE_W\/2), cy=TUNNEL_ROW;\n  const homes=[{x:cx,y:cy},{x:cx-1,y:cy},{x:cx+1,y:cy},{x:cx,y:cy+1}];\n  const colors=['#ff4d6d','#ff9f1c','#c77dff','#35e8a3'];\n  const h=homes[index];\n  return {x:h.x,y:h.y,home:{...h},dir:index%2?DIRS.left:DIRS.right,desired:DIRS.left,color:colors[index],index,stun:0,respawn:0};\n}\n\nfunction createCameraFeet(player){\n  const base=[\n    {side:-1,phase:0,anchor:null,from:null,to:null,t:1},\n    {side: 1,phase:1,anchor:null,from:null,to:null,t:1},\n    {side:-1,phase:2,anchor:null,from:null,to:null,t:1}\n  ];\n  player.feet=base;\n  player.footStep=0;\n  player.lastFootTile=`${Math.round(player.x)},${Math.round(player.y)}`;\n  retargetCameraFeet(player,true);\n}\n\nfunction localFrameForPlayer(player){\n  const f=player.facing&&player.facing!==DIRS.none?player.facing:DIRS.right;\n  return {f,perp:{x:-f.y,y:f.x}};\n}\n\nfunction wallAnchorCandidates(player,side){\n  const {f,perp}=localFrameForPlayer(player);\n  const px=Math.round(player.x), py=Math.round(player.y);\n  const out=[];\n  for(let oy=-3;oy<=3;oy++) for(let ox=-3;ox<=3;ox++){\n    const x=px+ox,y=py+oy;\n    if(y<0||y>=MAZE_H||x<0||x>=MAZE_W||state.map[y][x]!=='#') continue;\n    const vx=x-player.x,vy=y-player.y;\n    const sideScore=vx*perp.x+vy*perp.y;\n    const forward=vx*f.x+vy*f.y;\n    if(side<0 && sideScore>-.15) continue;\n    if(side>0 && sideScore<.15) continue;\n    const d=Math.hypot(vx,vy);\n    if(d>.15&&d<3.6) out.push({x,y,score:d+Math.abs(forward)*.16-Math.abs(sideScore)*.08});\n  }\n  out.sort((a,b)=>a.score-b.score);\n  return out;\n}\n\nfunction fallbackFootAnchor(player,side,index){\n  const {f,perp}=localFrameForPlayer(player);\n  const reach=1.05+(index===2?.25:0);\n  return {\n    x:player.x+perp.x*side*reach-f.x*.15,\n    y:player.y+perp.y*side*reach-f.y*.15,\n    synthetic:true\n  };\n}\n\nfunction retargetCameraFeet(player,immediate=false){\n  if(!player.feet) return;\n  const used=new Set();\n  player.feet.forEach((foot,index)=>{\n    const preferredSide=index===2?-((player.footStep%2)?1:-1):foot.side;\n    let candidates=wallAnchorCandidates(player,preferredSide);\n    if(!candidates.length) candidates=wallAnchorCandidates(player,-preferredSide);\n    let next=candidates.find(c=>!used.has(`${c.x},${c.y}`));\n    if(next) used.add(`${next.x},${next.y}`);\n    else next=fallbackFootAnchor(player,preferredSide,index);\n    const current=foot.anchor||next;\n    foot.from={x:current.x,y:current.y,synthetic:current.synthetic};\n    foot.to={x:next.x,y:next.y,synthetic:next.synthetic};\n    foot.anchor=immediate?{...next}:{...current};\n    foot.t=immediate?1:0;\n  });\n}\n\nfunction updateCameraFeet(player,dt){\n  if(!player?.feet) return;\n  const tileKey=`${Math.round(player.x)},${Math.round(player.y)}`;\n  if(tileKey!==player.lastFootTile && Math.abs(player.x-Math.round(player.x))<.12 && Math.abs(player.y-Math.round(player.y))<.12){\n    player.lastFootTile=tileKey; player.footStep++; retargetCameraFeet(player,false);\n  }\n  player.feet.forEach((foot,index)=>{\n    if(foot.t>=1) return;\n    \/\/ Elk voetje beweegt net na elkaar: dit geeft een herkenbare driepoot-gang.\n    const stagger=index*.12;\n    foot.t=clamp(foot.t+dt*4.4-stagger*dt,0,1);\n    const t=foot.t<.5?2*foot.t*foot.t:1-Math.pow(-2*foot.t+2,2)\/2;\n    foot.anchor={x:foot.from.x+(foot.to.x-foot.from.x)*t,y:foot.from.y+(foot.to.y-foot.from.y)*t,synthetic:foot.to.synthetic};\n  });\n}\n\nfunction loadLevel(first=false){\n  state.levelSeed=24681357+state.level*7919;\n  state.map=generateMaze(state.level);\n  const spawn=nearestPassable(2,MAZE_H-4);\n  state.map[spawn.y][spawn.x]=' ';\n  const firstDir=chooseInitialDir(spawn);\n  state.player={x:spawn.x,y:spawn.y,dir:firstDir,desired:firstDir,facing:firstDir===DIRS.none?DIRS.right:firstDir};\n  createCameraFeet(state.player);\n  state.burglars=[0,1,2,3].map(makeBurglar);\n  state.dots=0;\n  for(const row of state.map) for(const cell of row) if(cell==='.' || cell==='o') state.dots++;\n  state.initialDots=state.dots; state.collected=0; state.exposed=0; state.exposedCombo=0;\n  state.flashCooldown=first?0:.8; state.flashFx=0; state.invuln=1.5; state.bonus=null;\n  state.bonusMilestones=[Math.floor(state.initialDots*.30),Math.floor(state.initialDots*.67)];\n  state.modeTimer=18; state.chaseMode=true; state.sentry=false; state.sentryPending=false; state.sentryTurnTimer=0; state.sentryHold=0; state.sentryDetected=false; state.sentryScanPulse=0; state.sentryResumeDir=null;\n  if(sentryTouch) sentryTouch.classList.remove('active');\n  state.banner=first?'READY!':`LEVEL ${state.level}`; state.bannerTime=1.4;\n  centerView(true); updateHUD();\n}\n\nfunction startGame(){\n  ensureAudio();\n  state.name=(nameInput.value.trim().toUpperCase()||'SECURITY').slice(0,12);\n  state.score=0; state.level=1; state.lives=3; state.paused=false; state.phase='ready'; state.readyTimer=1.15;\n  loadLevel(true);\n  overlay.classList.add('hidden');\n  nameInput.blur(); startBtn.blur(); root.focus();\n  statusLine.textContent=`Operator ${state.name} actief \u2014 NIGHT WATCH gestart.`;\n  state.lastTime=performance.now();\n}\n\nfunction playerCentered(p=state.player){\n  return !!p && Math.abs(p.x-Math.round(p.x))<.001 && Math.abs(p.y-Math.round(p.y))<.001;\n}\n\nfunction directionIndex(dir){\n  const order=[DIRS.right,DIRS.down,DIRS.left,DIRS.up];\n  const i=order.findIndex(d=>d===dir || (d.x===dir?.x && d.y===dir?.y));\n  return i<0?0:i;\n}\n\nfunction activateSentryNow(){\n  if(!state.player) return;\n  const p=state.player;\n  p.x=Math.round(p.x); p.y=Math.round(p.y);\n  state.sentryResumeDir=(p.dir&&p.dir!==DIRS.none)?p.dir:(p.facing||DIRS.right);\n  p.dir=DIRS.none; p.desired=DIRS.none;\n  state.sentry=true; state.sentryPending=false; state.sentryTurnTimer=.18; state.sentryHold=0; state.sentryDetected=false; state.sentryScanPulse=0;\n  \/\/ Een wachtende beveiligingscamera moet vrijwel meteen paraat zijn.\n  state.flashCooldown=Math.min(state.flashCooldown,.75);\n  state.banner='SENTRY ONLINE'; state.bannerTime=1;\n  statusLine.textContent='SENTRY actief \u2014 360\u00b0 scan. Volledige zichtbare gang wordt bewaakt.';\n  if(sentryTouch) sentryTouch.classList.add('active');\n}\n\nfunction deactivateSentry(moveDir=null){\n  if(!state.player) return;\n  state.sentry=false; state.sentryPending=false; state.sentryDetected=false; state.sentryHold=0;\n  if(sentryTouch) sentryTouch.classList.remove('active');\n  const d=moveDir || state.sentryResumeDir || state.player.facing || DIRS.right;\n  state.player.desired=d; state.player.dir=d; state.player.facing=d;\n  statusLine.textContent=`Operator ${state.name} actief \u2014 mobiele camera.`;\n}\n\nfunction toggleSentry(){\n  if(!state.player || state.phase!=='playing' || state.paused) return;\n  if(state.sentry || state.sentryPending){\n    deactivateSentry();\n    state.banner='SENTRY OFF'; state.bannerTime=.7;\n    return;\n  }\n  if(playerCentered()) activateSentryNow();\n  else {\n    state.sentryPending=true;\n    state.sentryResumeDir=state.player.dir&&state.player.dir!==DIRS.none?state.player.dir:state.player.facing;\n    state.banner='SENTRY ARMING'; state.bannerTime=.8;\n    statusLine.textContent='SENTRY wordt geactiveerd op het eerstvolgende maze-blok\u2026';\n    if(sentryTouch) sentryTouch.classList.add('active');\n  }\n}\n\nfunction corridorRangeTiles(player=state.player,facing=player?.facing||DIRS.right){\n  if(!player) return 0;\n  const sx=Math.round(player.x), sy=Math.round(player.y);\n  let x=sx,y=sy,range=0;\n  const max=Math.max(MAZE_W,MAZE_H)+2;\n  for(let i=0;i<max;i++){\n    let nx=x+facing.x, ny=y+facing.y;\n    \/\/ Voor de zijtunnel scannen we tot aan de rand; we tekenen\/detecteren niet oneindig rond de wrap.\n    if(nx<0||nx>=MAZE_W||ny<0||ny>=MAZE_H) break;\n    if(!isPassable(nx,ny)) break;\n    range++; x=nx; y=ny;\n  }\n  return range+.45;\n}\n\nfunction burglarInSentryCorridor(b){\n  if(!state.sentry || !state.player || b.respawn>0) return false;\n  const p=state.player, f=p.facing||DIRS.right;\n  const vx=b.x-p.x, vy=b.y-p.y;\n  const forward=vx*f.x+vy*f.y;\n  const side=Math.abs(vx*(-f.y)+vy*f.x);\n  if(forward<=.20 || side>.42) return false;\n  const maxRange=corridorRangeTiles(p,f);\n  if(forward>maxRange+.35) return false;\n  return lineClear(p.x,p.y,b.x,b.y);\n}\n\nfunction updateSentry(dt){\n  if(!state.sentry || !state.player) return;\n  const p=state.player;\n  p.dir=DIRS.none; p.desired=DIRS.none;\n  state.sentryScanPulse+=dt;\n  state.sentryHold=Math.max(0,state.sentryHold-dt);\n\n  const visible=state.burglars.filter(burglarInSentryCorridor);\n  state.sentryDetected=visible.length>0;\n\n  if(visible.length){\n    state.sentryHold=Math.max(state.sentryHold,.75);\n    if(state.flashCooldown<=0){\n      state.banner='INTRUDER DETECTED'; state.bannerTime=.75;\n      useFlash(true);\n    }\n  }else if(state.sentryHold<=0){\n    state.sentryTurnTimer-=dt;\n    if(state.sentryTurnTimer<=0){\n      const order=[DIRS.right,DIRS.down,DIRS.left,DIRS.up];\n      p.facing=order[(directionIndex(p.facing)+1)%4];\n      state.sentryTurnTimer=.82;\n    }\n  }\n}\n\nfunction setDesired(dir){\n  if(!state.player || !['ready','playing'].includes(state.phase) || state.paused) return;\n  if(state.sentry || state.sentryPending){\n    deactivateSentry(dir);\n    return;\n  }\n  state.player.desired=dir;\n  if(reverseOf(dir,state.player.dir)){\n    state.player.dir=dir; state.player.facing=dir;\n  }\n}\n\nfunction moveActor(actor,speed,dt,isPlayer=false){\n  if(!actor || actor.dir===DIRS.none && actor.desired===DIRS.none) return;\n  if(actor.desired && reverseOf(actor.desired,actor.dir)){\n    actor.dir=actor.desired;\n    if(isPlayer) actor.facing=actor.dir;\n  }\n  let remaining=speed*dt, loops=0;\n  while(remaining>0.00001 && loops++<6){\n    applyWrap(actor);\n    const rx=Math.round(actor.x), ry=Math.round(actor.y);\n    const centered=Math.abs(actor.x-rx)<0.0001 && Math.abs(actor.y-ry)<0.0001;\n    if(centered){\n      actor.x=rx; actor.y=ry;\n      if(actor.desired && actor.desired!==DIRS.none && canStep(rx,ry,actor.desired)) actor.dir=actor.desired;\n      if(!actor.dir || actor.dir===DIRS.none || !canStep(rx,ry,actor.dir)){ actor.dir=DIRS.none; break; }\n      if(isPlayer) actor.facing=actor.dir;\n    }\n    if(actor.dir===DIRS.none) break;\n    let nx=actor.x, ny=actor.y;\n    if(actor.dir.x>0) nx=Math.floor(actor.x+1e-7)+1;\n    else if(actor.dir.x<0) nx=Math.ceil(actor.x-1e-7)-1;\n    else if(actor.dir.y>0) ny=Math.floor(actor.y+1e-7)+1;\n    else ny=Math.ceil(actor.y-1e-7)-1;\n    let toCenter=Math.abs(nx-actor.x)+Math.abs(ny-actor.y);\n    if(toCenter<0.000001) toCenter=1;\n    const step=Math.min(remaining,toCenter);\n    actor.x+=actor.dir.x*step; actor.y+=actor.dir.y*step; remaining-=step;\n    if(step>=toCenter-0.000001){ actor.x=nx; actor.y=ny; applyWrap(actor); }\n  }\n}\n\nfunction availableDirs(b){\n  const x=Math.round(b.x), y=Math.round(b.y);\n  const all=CARDINALS.filter(d=>canStep(x,y,d));\n  const noReverse=all.filter(d=>!reverseOf(d,b.dir));\n  return noReverse.length?noReverse:all;\n}\nfunction burglarTarget(b){\n  const p=state.player, f=p.facing||DIRS.right, perp={x:-f.y,y:f.x};\n  if(!state.chaseMode){\n    \/\/ Ze blijven zelfs tijdens de korte patrolfase dicht in de buurt van de speler.\n    const offsets=[{x:-3,y:-3},{x:3,y:-3},{x:-3,y:3},{x:3,y:3}];\n    const o=offsets[b.index];\n    return nearestPassable(Math.round(p.x+o.x),Math.round(p.y+o.y));\n  }\n  if(b.index===0) return nearestPassable(Math.round(p.x),Math.round(p.y));\n  if(b.index===1) return nearestPassable(Math.round(p.x+f.x*4),Math.round(p.y+f.y*4));\n  if(b.index===2) return nearestPassable(Math.round(p.x+f.x*2+perp.x*3),Math.round(p.y+f.y*2+perp.y*3));\n  \/\/ De vierde inbreker probeert verder voor de camera af te snijden.\n  return nearestPassable(Math.round(p.x+f.x*6-perp.x*2),Math.round(p.y+f.y*6-perp.y*2));\n}\n\nfunction shortestMazeDistance(sx,sy,tx,ty,maxDepth=1800){\n  sx=Math.round(sx);sy=Math.round(sy);tx=Math.round(tx);ty=Math.round(ty);\n  if(sx===tx&&sy===ty) return 0;\n  const key=(x,y)=>y*MAZE_W+x;\n  const qx=new Int16Array(MAZE_W*MAZE_H), qy=new Int16Array(MAZE_W*MAZE_H), qd=new Int16Array(MAZE_W*MAZE_H);\n  const seen=new Uint8Array(MAZE_W*MAZE_H);\n  let head=0,tail=0;\n  if(sy<0||sy>=MAZE_H) return 9999;\n  sx=(sx+MAZE_W)%MAZE_W;\n  qx[tail]=sx;qy[tail]=sy;qd[tail]=0;tail++;seen[key(sx,sy)]=1;\n  while(head<tail && head<maxDepth){\n    const x=qx[head],y=qy[head],d=qd[head++];\n    for(const dir of CARDINALS){\n      let nx=x+dir.x,ny=y+dir.y;\n      if(y===TUNNEL_ROW){ if(nx<0) nx=MAZE_W-1; else if(nx>=MAZE_W) nx=0; }\n      if(nx<0||nx>=MAZE_W||ny<0||ny>=MAZE_H||!isPassable(nx,ny)) continue;\n      if(nx===tx&&ny===ty) return d+1;\n      const k=key(nx,ny); if(seen[k]) continue; seen[k]=1; qx[tail]=nx;qy[tail]=ny;qd[tail]=d+1;tail++;\n    }\n  }\n  return 9999;\n}\n\nfunction chooseBurglarDir(b){\n  const opts=availableDirs(b); if(!opts.length){ b.desired=DIRS.none; return; }\n  if(state.exposed>0){\n    opts.sort((a,c)=>{\n      const da=shortestMazeDistance(b.x+a.x,b.y+a.y,state.player.x,state.player.y);\n      const dc=shortestMazeDistance(b.x+c.x,b.y+c.y,state.player.x,state.player.y);\n      return dc-da;\n    });\n    b.desired=opts[Math.random()<.08?Math.floor(Math.random()*opts.length):0]; return;\n  }\n  const t=burglarTarget(b);\n  opts.sort((a,c)=>{\n    const da=shortestMazeDistance(b.x+a.x,b.y+a.y,t.x,t.y)+Math.random()*.03;\n    const dc=shortestMazeDistance(b.x+c.x,b.y+c.y,t.x,t.y)+Math.random()*.03;\n    return da-dc;\n  });\n  b.desired=opts[0];\n}\nfunction updateBurglars(dt){\n  for(const b of state.burglars){\n    if(b.respawn>0){ b.respawn-=dt; continue; }\n    if(b.stun>0){ b.stun-=dt; continue; }\n    const rx=Math.round(b.x), ry=Math.round(b.y);\n    if(Math.abs(b.x-rx)<.001 && Math.abs(b.y-ry)<.001){ b.x=rx; b.y=ry; chooseBurglarDir(b); }\n    const base=3.48+Math.min(1.65,(state.level-1)*.14);\n    moveActor(b,state.exposed>0?base*.72:base,dt,false);\n  }\n}\n\nfunction collectTile(){\n  const p=state.player, x=Math.round(p.x), y=Math.round(p.y);\n  if(Math.abs(p.x-x)>.13 || Math.abs(p.y-y)>.13) return;\n  const cell=state.map[y]?.[x];\n  if(cell==='.'){\n    state.map[y][x]=' '; state.score+=10; state.dots--; state.collected++; sfx('dot'); checkBonusSpawn();\n  }else if(cell==='o'){\n    state.map[y][x]=' '; state.score+=50; state.dots--; state.collected++; state.exposed=7.5; state.exposedCombo=0;\n    state.banner='ALARM MODE'; state.bannerTime=1; sfx('orb'); checkBonusSpawn();\n  }\n  if(state.dots<=0 && state.phase==='playing'){\n    state.phase='levelclear'; state.banner='SECTOR CLEARED'; state.bannerTime=2; sfx('level');\n    setTimeout(()=>{\n      if(state.phase==='levelclear'){\n        state.level++; state.phase='ready'; state.readyTimer=1; loadLevel(false);\n      }\n    },1450);\n  }\n}\n\nfunction checkBonusSpawn(){\n  if(state.bonus || !state.bonusMilestones.length || state.collected<state.bonusMilestones[0]) return;\n  state.bonusMilestones.shift();\n  const cells=[];\n  for(let y=1;y<MAZE_H-1;y++) for(let x=1;x<MAZE_W-1;x++){\n    if(isPassable(x,y) && state.map[y][x]!=='o' && Math.hypot(x-state.player.x,y-state.player.y)>7) cells.push({x,y});\n  }\n  if(cells.length){ state.bonus={...cells[Math.floor(Math.random()*cells.length)],time:8,pulse:0}; state.banner='EVIDENCE DROP'; state.bannerTime=.9; }\n}\nfunction updateBonus(dt){\n  if(!state.bonus) return;\n  state.bonus.time-=dt; state.bonus.pulse+=dt;\n  if(dist(state.player,state.bonus)<.55){\n    const value=500*state.level; state.score+=value; state.banner=`+${value} EVIDENCE`; state.bannerTime=1; sfx('bonus'); state.bonus=null; return;\n  }\n  if(state.bonus && state.bonus.time<=0) state.bonus=null;\n}\n\nfunction lineClear(x1,y1,x2,y2){\n  const d=Math.hypot(x2-x1,y2-y1), steps=Math.max(1,Math.ceil(d\/.16));\n  for(let i=1;i<steps;i++){\n    const t=i\/steps, x=x1+(x2-x1)*t, y=y1+(y2-y1)*t;\n    if(!isPassable(Math.round(x),Math.round(y))) return false;\n  }\n  return true;\n}\nfunction burglarVisibleInFacingCorridor(b,maxRange){\n  const p=state.player, f=p.facing||DIRS.right;\n  const vx=b.x-p.x, vy=b.y-p.y;\n  const forward=vx*f.x+vy*f.y;\n  const side=Math.abs(vx*(-f.y)+vy*f.x);\n  return forward>.15 && forward<=maxRange+.35 && side<.48 && lineClear(p.x,p.y,b.x,b.y);\n}\nfunction useFlash(auto=false){\n  if(state.phase!=='playing' || state.paused || state.flashCooldown>0) return false;\n  const p=state.player, fullCorridor=state.sentry || auto;\n  const maxRange=fullCorridor?corridorRangeTiles(p,p.facing||DIRS.right):5.4;\n  state.flashCooldown=fullCorridor?3.6:5.5; state.flashFx=.92; state.flashPulse=0; sfx('flash');\n  let hits=0;\n  for(const b of state.burglars){\n    if(b.respawn>0) continue;\n    let visible=false;\n    if(fullCorridor){\n      visible=burglarVisibleInFacingCorridor(b,maxRange);\n    }else{\n      const f=p.facing||DIRS.right, vx=b.x-p.x, vy=b.y-p.y, r=Math.hypot(vx,vy);\n      if(r>=.01 && r<=maxRange){\n        const dot=(vx\/r)*f.x+(vy\/r)*f.y;\n        visible=dot>.74 && lineClear(p.x,p.y,b.x,b.y);\n      }\n    }\n    if(visible){\n      b.stun=Math.max(b.stun,fullCorridor?2.05:1.65); state.score+=auto?175:125; hits++; sfx('stun');\n    }\n  }\n  if(hits){ state.banner=auto?`AUTO FLASH \u00d7${hits}`:`FLASH HIT \u00d7${hits}`; state.bannerTime=.8; state.shake=.15; }\n  return true;\n}\n\nfunction resetPositions(){\n  const spawn=nearestPassable(2,MAZE_H-4), d=chooseInitialDir(spawn);\n  state.player={x:spawn.x,y:spawn.y,dir:d,desired:d,facing:d===DIRS.none?DIRS.right:d};\n  createCameraFeet(state.player);\n  state.burglars=[0,1,2,3].map(makeBurglar); state.invuln=1.6; state.exposed=0; state.exposedCombo=0; state.sentry=false; state.sentryPending=false; state.sentryDetected=false; if(sentryTouch)sentryTouch.classList.remove('active'); centerView(true);\n}\nfunction collisions(){\n  if(state.invuln>0) return;\n  for(const b of state.burglars){\n    if(b.respawn>0 || dist(state.player,b)>=.54) continue;\n    if(state.exposed>0){\n      const gain=200*Math.pow(2,Math.min(3,state.exposedCombo++)); state.score+=gain;\n      b.x=b.home.x; b.y=b.home.y; b.dir=DIRS.up; b.desired=DIRS.up; b.respawn=.7; b.stun=0;\n      state.banner=`INTRUDER TAGGED +${gain}`; state.bannerTime=.8; state.shake=.14; sfx('capture');\n    }else if(b.stun<=0){ loseLife(); }\n    break;\n  }\n}\nfunction loseLife(){\n  state.lives--; state.shake=.4; sfx('hit'); updateHUD();\n  if(state.lives<=0){ gameOver(); return; }\n  state.phase='hit'; state.banner='CAMERA OFFLINE'; state.bannerTime=1.1;\n  setTimeout(()=>{\n    if(state.phase==='hit'){\n      resetPositions(); state.phase='ready'; state.readyTimer=.9; state.banner='BACKUP CAMERA'; state.bannerTime=.9;\n    }\n  },850);\n}\nfunction gameOver(){\n  state.phase='gameover'; saveScore(state.name,state.score,state.level); renderScores(); updateHUD();\n  overlayEyebrow.textContent='SHIFT TERMINATED';\n  overlayTitle.innerHTML='<span class=\"white\">CCTV<\/span> <b>LOCKDOWN<\/b>';\n  overlayText.innerHTML=`Operator <b>${escapeHtml(state.name)}<\/b> behaalde <b>${formatScore(state.score)}<\/b> punten en bereikte level <b>${state.level}<\/b>.`;\n  introTips.style.display='none'; startBtn.textContent='\u21bb OPNIEUW SPELEN'; overlay.classList.remove('hidden');\n  statusLine.textContent=`Shift be\u00ebindigd \u2014 ${formatScore(state.score)} punten.`;\n}\n\nfunction centerView(immediate=false,dt=.016){\n  if(!state.player) return;\n  const px=(state.player.x+.5)*TILE, py=(state.player.y+.5)*TILE;\n  const tx=clamp(px-VIEW_W\/2,0,Math.max(0,WORLD_W-VIEW_W));\n  const ty=clamp(py-VIEW_H\/2,0,Math.max(0,WORLD_H-VIEW_H));\n  if(immediate){ state.viewX=tx; state.viewY=ty; return; }\n  const k=1-Math.pow(.0008,dt);\n  state.viewX+=(tx-state.viewX)*k; state.viewY+=(ty-state.viewY)*k;\n}\n\nfunction updateHUD(){\n  scoreEl.textContent=formatScore(state.score);\n  const scores=getScores(); highEl.textContent=formatScore(Math.max(state.score,scores[0]?.score||0));\n  levelEl.textContent=String(state.level).padStart(2,'0'); livesEl.textContent='\ud83d\udcf9 '.repeat(Math.max(0,state.lives)).trim()||'\u2014';\n  const progress=state.initialDots?clamp((state.initialDots-state.dots)\/state.initialDots*100,0,100):0;\n  progressEl.textContent=`${Math.floor(progress)}%`; progressFill.style.width=`${progress}%`;\n  flashText.textContent=state.flashFx>0?'ACTIVE':(state.sentry?(state.sentryDetected?'TARGET':'SCAN'):(state.flashCooldown<=0?'READY':`${state.flashCooldown.toFixed(1)}s`));\n  const flashBase=state.sentry?3.6:5.5; flashFill.style.width=`${clamp((1-state.flashCooldown\/flashBase)*100,0,100)}%`;\n}\n\nfunction update(dt){\n  if(state.phase==='ready'){\n    state.readyTimer-=dt; state.bannerTime=Math.max(state.bannerTime,Math.min(1,state.readyTimer));\n    centerView(false,dt);\n    if(state.readyTimer<=0){ state.phase='playing'; state.banner=''; state.bannerTime=0; }\n    return;\n  }\n  if(state.phase!=='playing' || state.paused){ centerView(false,dt); return; }\n  state.invuln=Math.max(0,state.invuln-dt); state.exposed=Math.max(0,state.exposed-dt);\n  state.flashCooldown=Math.max(0,state.flashCooldown-dt); state.flashFx=Math.max(0,state.flashFx-dt); if(state.flashFx>0) state.flashPulse+=dt;\n  state.shake=Math.max(0,state.shake-dt); state.bannerTime=Math.max(0,state.bannerTime-dt);\n  state.modeTimer-=dt;\n  if(state.modeTimer<=0){ state.chaseMode=!state.chaseMode; state.modeTimer=state.chaseMode?18:2.5; }\n  if(state.sentryPending){\n    moveActor(state.player,4.75,dt,true);\n    if(playerCentered()) activateSentryNow();\n  }else if(state.sentry){\n    updateSentry(dt);\n  }else{\n    moveActor(state.player,4.75,dt,true);\n  }\n  updateCameraFeet(state.player,dt); updateBurglars(dt); if(!state.sentry) collectTile(); updateBonus(dt); collisions(); centerView(false,dt); updateHUD();\n}\n\nfunction rr(x,y,w,h,r,fill=true,stroke=false){\n  r=Math.min(r,w\/2,h\/2); ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); if(fill)ctx.fill(); if(stroke)ctx.stroke();\n}\nfunction worldToPx(x,y){ return {x:(x+.5)*TILE,y:(y+.5)*TILE}; }\nfunction drawMaze(){\n  const minX=Math.max(0,Math.floor(state.viewX\/TILE)-1), maxX=Math.min(MAZE_W-1,Math.ceil((state.viewX+VIEW_W)\/TILE)+1);\n  const minY=Math.max(0,Math.floor(state.viewY\/TILE)-1), maxY=Math.min(MAZE_H-1,Math.ceil((state.viewY+VIEW_H)\/TILE)+1);\n  for(let y=minY;y<=maxY;y++) for(let x=minX;x<=maxX;x++) if(state.map[y][x]==='#'){\n    const px=x*TILE, py=y*TILE;\n    ctx.fillStyle='#06131f'; ctx.fillRect(px+1,py+1,TILE-2,TILE-2);\n    ctx.strokeStyle='rgba(54,129,255,.78)'; ctx.lineWidth=1.6; ctx.strokeRect(px+3,py+3,TILE-6,TILE-6);\n    ctx.strokeStyle='rgba(32,216,255,.12)'; ctx.lineWidth=.8; ctx.strokeRect(px+7,py+7,TILE-14,TILE-14);\n  }\n}\nfunction drawDots(){\n  const now=performance.now()\/1000;\n  const minX=Math.max(0,Math.floor(state.viewX\/TILE)-1), maxX=Math.min(MAZE_W-1,Math.ceil((state.viewX+VIEW_W)\/TILE)+1);\n  const minY=Math.max(0,Math.floor(state.viewY\/TILE)-1), maxY=Math.min(MAZE_H-1,Math.ceil((state.viewY+VIEW_H)\/TILE)+1);\n  for(let y=minY;y<=maxY;y++) for(let x=minX;x<=maxX;x++){\n    const c=state.map[y][x], px=(x+.5)*TILE, py=(y+.5)*TILE;\n    if(c==='.'){\n      ctx.beginPath(); ctx.fillStyle='rgba(232,251,255,.88)'; ctx.arc(px,py,2.25,0,TAU); ctx.fill();\n    }else if(c==='o'){\n      const r=6.5+Math.sin(now*5+x+y)*1.1; ctx.beginPath(); ctx.shadowColor='#ffd166'; ctx.shadowBlur=13; ctx.fillStyle='#ffd166'; ctx.arc(px,py,r,0,TAU); ctx.fill(); ctx.shadowBlur=0;\n    }\n  }\n}\nfunction facingAngle(f){ if(f===DIRS.down)return Math.PI\/2;if(f===DIRS.left)return Math.PI;if(f===DIRS.up)return-Math.PI\/2;return 0; }\nfunction drawCameraFeet(p){\n  if(!p.feet) return;\n  const body=worldToPx(p.x,p.y);\n  const {f}=localFrameForPlayer(p);\n  const attachOffsets=[-6,6,0];\n  p.feet.forEach((foot,index)=>{\n    if(!foot.anchor) return;\n    const target=worldToPx(foot.anchor.x,foot.anchor.y);\n    const attach={x:body.x-f.y*attachOffsets[index]-f.x*4,y:body.y+f.x*attachOffsets[index]-f.y*4};\n    const dx=target.x-attach.x,dy=target.y-attach.y;\n    const len=Math.hypot(dx,dy)||1;\n    const nx=-dy\/len,ny=dx\/len;\n    const lift=(foot.t<1?Math.sin(foot.t*Math.PI)*8:0)*(index===1?-1:1);\n    const joint={x:(attach.x+target.x)\/2+nx*(8+lift),y:(attach.y+target.y)\/2+ny*(8+lift)};\n\n    ctx.save();\n    ctx.lineCap='round';ctx.lineJoin='round';\n    ctx.shadowColor='rgba(141,255,117,.45)';ctx.shadowBlur=6;\n    ctx.strokeStyle='rgba(188,255,176,.92)';ctx.lineWidth=3.2;\n    ctx.beginPath();ctx.moveTo(attach.x,attach.y);ctx.lineTo(joint.x,joint.y);ctx.lineTo(target.x,target.y);ctx.stroke();\n    ctx.shadowBlur=0;ctx.strokeStyle='#173e1e';ctx.lineWidth=1.1;ctx.stroke();\n\n    \/\/ Het \"klikkende\" voetje landt zichtbaar op een ander muurblokje.\n    const landing=foot.t<1?1-Math.sin(foot.t*Math.PI)*.35:1;\n    ctx.translate(target.x,target.y);ctx.rotate(Math.atan2(dy,dx));ctx.scale(landing,landing);\n    ctx.fillStyle=foot.anchor.synthetic?'#84c77b':'#d9ffd2';\n    ctx.strokeStyle='#5bea50';ctx.lineWidth=1.3;\n    rr(-4.8,-3.2,9.6,6.4,2.4,true,true);\n    ctx.beginPath();ctx.fillStyle='#061209';ctx.arc(0,0,1.5,0,TAU);ctx.fill();\n    ctx.restore();\n  });\n}\n\nfunction drawCamera(p){\n  \/\/ Eerst de drie armen tekenen zodat ze achter de camerabehuizing liggen.\n  drawCameraFeet(p);\n  const pos=worldToPx(p.x,p.y); ctx.save(); ctx.translate(pos.x,pos.y); ctx.rotate(facingAngle(p.facing));\n  if(state.invuln>0 && Math.floor(performance.now()\/85)%2===0) ctx.globalAlpha=.32;\n  const flashBoost=state.flashFx>0?22:12;\n  ctx.shadowColor=state.flashFx>0?'#fff6bf':'#8dff75'; ctx.shadowBlur=flashBoost; ctx.fillStyle='#d8ffd0'; rr(-10,-8,17,16,4,true,false);\n  ctx.fillStyle='#75ee65'; ctx.beginPath(); ctx.moveTo(7,-6);ctx.lineTo(15,-10);ctx.lineTo(15,10);ctx.lineTo(7,6);ctx.closePath();ctx.fill();\n  ctx.fillStyle='#041a08';ctx.beginPath();ctx.arc(-2,0,4.3,0,TAU);ctx.fill();\n  ctx.fillStyle=state.flashFx>0?'#fffbdc':'#fff';ctx.shadowColor='#fff7b0';ctx.shadowBlur=state.flashFx>0?18:0;ctx.beginPath();ctx.arc(-3.3,-1.4,state.flashFx>0?2.2:1.3,0,TAU);ctx.fill();\n  ctx.restore();\n}\nfunction drawBurglar(b){\n  const pos=worldToPx(b.x,b.y), frightened=state.exposed>0, stunned=b.stun>0;\n  let body=b.color;\n  if(frightened){ const flashing=state.exposed<2 && Math.floor(performance.now()\/140)%2===0; body=flashing?'#f2fbff':'#184fd5'; }\n  ctx.save();ctx.translate(pos.x,pos.y);if(stunned){ctx.rotate(Math.sin(performance.now()\/38)*.08);ctx.globalAlpha=.78;}\n  ctx.shadowColor=body;ctx.shadowBlur=frightened?16:8;ctx.fillStyle=body;rr(-9,-4,18,17,6,true,false);ctx.beginPath();ctx.arc(0,-6,7.5,Math.PI,TAU);ctx.fill();ctx.shadowBlur=0;\n  ctx.fillStyle='#101114';ctx.fillRect(-8,-8,16,5);ctx.fillStyle='#f7ffff';ctx.fillRect(-5,-7,3,2);ctx.fillRect(2,-7,3,2);ctx.fillStyle='#001018';ctx.fillRect(-4,-6,1,1);ctx.fillRect(3,-6,1,1);\n  ctx.strokeStyle='#111';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(-6,12);ctx.lineTo(-9,16);ctx.moveTo(6,12);ctx.lineTo(9,16);ctx.stroke();\n  if(stunned){ctx.fillStyle='#ffd166';ctx.font='bold 10px monospace';ctx.textAlign='center';ctx.fillText('\u2726',-8,-15);ctx.fillText('\u2726',8,-18);}ctx.restore();\n}\nfunction drawBonus(){\n  if(!state.bonus) return; const p=worldToPx(state.bonus.x,state.bonus.y); const s=1+Math.sin(state.bonus.pulse*8)*.08;\n  ctx.save();ctx.translate(p.x,p.y);ctx.scale(s,s);ctx.shadowColor='#ffd166';ctx.shadowBlur=16;ctx.fillStyle='#ffd166';rr(-9,-7,18,15,3,true,false);ctx.shadowBlur=0;ctx.fillStyle='#3b2b02';ctx.fillRect(-4,-10,8,4);ctx.fillStyle='#fff7cc';ctx.font='bold 10px monospace';ctx.textAlign='center';ctx.fillText('E',0,4);ctx.restore();\n}\nfunction drawCone(){\n  if(!state.player) return;\n  const p=worldToPx(state.player.x,state.player.y), f=state.player.facing||DIRS.right, angle=Math.atan2(f.y,f.x);\n  const active=state.flashFx>0;\n  const sentry=state.sentry;\n  const pulse=active?.90+.10*Math.sin(state.flashPulse*32):1;\n  const corridorTiles=sentry?corridorRangeTiles(state.player,f):0;\n  const len=TILE*(sentry?Math.max(.8,corridorTiles):(active?7.2:5.5)), spread=active?(sentry?.22:.43):(sentry?.085:.30);\n  ctx.save();ctx.translate(p.x,p.y);ctx.rotate(angle);\n  if(active) ctx.globalCompositeOperation='screen';\n\n  const g=ctx.createLinearGradient(0,0,len,0);\n  g.addColorStop(0,active?`rgba(255,255,238,${.96*pulse})`:(sentry?(state.sentryDetected?'rgba(255,110,90,.24)':'rgba(141,255,117,.16)'):'rgba(255,246,185,.045)'));\n  g.addColorStop(.18,active?`rgba(255,246,174,${.70*pulse})`:(sentry?(state.sentryDetected?'rgba(255,80,70,.12)':'rgba(32,216,255,.07)'):'rgba(255,246,185,.028)'));\n  g.addColorStop(1,'rgba(255,246,185,0)');\n  ctx.fillStyle=g;ctx.beginPath();ctx.moveTo(8,0);ctx.lineTo(len,-len*spread);ctx.lineTo(len,len*spread);ctx.closePath();ctx.fill();\n\n  if(active){\n    \/\/ Felle witte kern van de lamp.\n    const core=ctx.createLinearGradient(0,0,len*.86,0);core.addColorStop(0,`rgba(255,255,255,${.98*pulse})`);core.addColorStop(.35,`rgba(255,252,210,${.48*pulse})`);core.addColorStop(1,'rgba(255,255,255,0)');\n    ctx.fillStyle=core;ctx.shadowColor='#fff7b0';ctx.shadowBlur=28;ctx.beginPath();ctx.moveTo(9,-4);ctx.lineTo(len*.86,-len*.075);ctx.lineTo(len*.86,len*.075);ctx.lineTo(9,4);ctx.closePath();ctx.fill();\n    ctx.shadowBlur=0;\n    \/\/ Halo rond de lens gedurende de volledige activatie.\n    const halo=ctx.createRadialGradient(10,0,0,10,0,TILE*2.2);halo.addColorStop(0,`rgba(255,255,245,${.88*pulse})`);halo.addColorStop(.25,`rgba(255,239,150,${.32*pulse})`);halo.addColorStop(1,'rgba(255,230,120,0)');ctx.fillStyle=halo;ctx.beginPath();ctx.arc(10,0,TILE*2.2,0,TAU);ctx.fill();\n  }\n  ctx.restore();\n}\n\nfunction drawFlashScreenBloom(){\n  if(state.flashFx<=0 || !state.player) return;\n  const p={x:(state.player.x+.5)*TILE-state.viewX,y:(state.player.y+.5)*TILE-state.viewY};\n  const strength=clamp(state.flashFx\/.82,0,1);\n  const glow=ctx.createRadialGradient(p.x,p.y,10,p.x,p.y,260);\n  glow.addColorStop(0,`rgba(255,250,210,${.16*strength})`);glow.addColorStop(1,'rgba(255,250,210,0)');\n  ctx.save();ctx.globalCompositeOperation='screen';ctx.fillStyle=glow;ctx.fillRect(0,0,VIEW_W,VIEW_H);ctx.restore();\n}\nfunction drawScreenHUD(){\n  ctx.save();\n  ctx.fillStyle='rgba(0,10,14,.40)';ctx.fillRect(12,12,176,38);ctx.strokeStyle='rgba(32,216,255,.35)';ctx.strokeRect(12.5,12.5,176,38);\n  ctx.fillStyle='#ff4d6d';ctx.beginPath();ctx.arc(27,31,4,0,TAU);ctx.fill();\n  ctx.fillStyle='#dff8ff';ctx.font='bold 12px monospace';ctx.textBaseline='middle';ctx.fillText('REC  CAM-07',39,31);\n  ctx.textAlign='right';ctx.fillStyle='rgba(223,248,255,.75)';ctx.font='10px monospace';ctx.fillText(`SECTOR ${String(state.level).padStart(2,'0')}`,VIEW_W-16,27);\n  ctx.fillText(state.exposed>0?`ALARM ${state.exposed.toFixed(1)}s`:(state.sentry?(state.sentryDetected?'SENTRY \u2022 TARGET':'SENTRY \u2022 SCANNING'):'STATUS CLEAR'),VIEW_W-16,43);\n  ctx.restore();\n}\nfunction drawBanner(){\n  if(state.bannerTime<=0 || !state.banner) return; const a=clamp(state.bannerTime*2,0,1);\n  ctx.save();ctx.globalAlpha=a;ctx.fillStyle='rgba(0,8,12,.84)';rr(VIEW_W\/2-150,VIEW_H\/2-27,300,54,8,true,false);ctx.strokeStyle='rgba(32,216,255,.45)';ctx.lineWidth=1;rr(VIEW_W\/2-150,VIEW_H\/2-27,300,54,8,false,true);ctx.fillStyle='#eefcff';ctx.font='bold 18px monospace';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(state.banner,VIEW_W\/2,VIEW_H\/2);ctx.restore();\n}\nfunction draw(){\n  ctx.save();\n  ctx.setTransform(DPR,0,0,DPR,0,0);\n  ctx.clearRect(0,0,VIEW_W,VIEW_H);\n  const bg=ctx.createRadialGradient(VIEW_W*.5,VIEW_H*.48,20,VIEW_W*.5,VIEW_H*.48,VIEW_H*.8);bg.addColorStop(0,'#06141d');bg.addColorStop(1,'#010407');ctx.fillStyle=bg;ctx.fillRect(0,0,VIEW_W,VIEW_H);\n  if(state.shake>0){const m=state.shake*8;ctx.translate((Math.random()-.5)*m,(Math.random()-.5)*m);}\n  ctx.save();ctx.translate(-state.viewX,-state.viewY);drawMaze();drawDots();drawBonus();drawCone();for(const b of state.burglars)if(b.respawn<=0)drawBurglar(b);if(state.player)drawCamera(state.player);ctx.restore();\n  drawFlashScreenBloom();drawScreenHUD();drawBanner();ctx.restore();\n}\n\nfunction getScores(){\n  try{ const v=JSON.parse(localStorage.getItem('cctvPacMazeInteractiveV1')||'[]'); return Array.isArray(v)?v:[]; }catch(e){ return memoryScores; }\n}\nfunction saveScore(name,score,level){\n  const scores=getScores().slice(); scores.push({name:(name||'SECURITY').slice(0,12),score:Math.floor(score),level}); scores.sort((a,b)=>b.score-a.score); const out=scores.slice(0,8); memoryScores=out;\n  try{localStorage.setItem('cctvPacMazeInteractiveV1',JSON.stringify(out));}catch(e){}\n}\nfunction renderScores(){\n  const scores=getScores(); scoreTable.innerHTML=''; const rows=scores.length?scores:Array.from({length:5},()=>({name:'---',score:0}));\n  rows.slice(0,8).forEach((s,i)=>{const tr=document.createElement('tr');tr.innerHTML=`<td>${i+1}<\/td><td>${escapeHtml(s.name||'---')}<\/td><td>${formatScore(s.score||0)}<\/td>`;scoreTable.appendChild(tr);});\n  highEl.textContent=formatScore(Math.max(state.score,scores[0]?.score||0));\n}\n\nfunction togglePause(){\n  if(state.phase!=='playing') return; state.paused=!state.paused; state.banner=state.paused?'PAUSED':'RESUMED';state.bannerTime=.8;statusLine.textContent=state.paused?'Pauze \u2014 druk P om verder te gaan.':`Operator ${state.name} actief \u2014 feed live.`;\n}\nfunction toggleSound(){state.sound=!state.sound;soundBtn.textContent=state.sound?'\ud83d\udd0a GELUID AAN':'\ud83d\udd07 GELUID UIT';if(state.sound)ensureAudio();}\n\nroot.addEventListener('keydown',event=>{\n  const editable=event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement || event.target.isContentEditable;\n  if(editable) return;\n  const codes=['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','KeyW','KeyA','KeyS','KeyD','Space','KeyE','KeyP','KeyM'];\n  if(['ready','playing'].includes(state.phase) && codes.includes(event.code)) event.preventDefault();\n  if(event.code==='ArrowUp'||event.code==='KeyW') setDesired(DIRS.up);\n  else if(event.code==='ArrowDown'||event.code==='KeyS') setDesired(DIRS.down);\n  else if(event.code==='ArrowLeft'||event.code==='KeyA') setDesired(DIRS.left);\n  else if(event.code==='ArrowRight'||event.code==='KeyD') setDesired(DIRS.right);\n  else if(event.code==='Space'&&!event.repeat) useFlash(false);\n  else if(event.code==='KeyE'&&!event.repeat) toggleSentry();\n  else if(event.code==='KeyP'&&!event.repeat) togglePause();\n  else if(event.code==='KeyM'&&!event.repeat) toggleSound();\n},{passive:false});\n\nstartBtn.addEventListener('click',()=>{\n  introTips.style.display='grid'; overlayEyebrow.textContent='SECURITY SYSTEM \/\/ ONLINE'; overlayTitle.innerHTML='<span class=\"white\">CCTV<\/span> <b>NIGHT WATCH<\/b>';\n  overlayText.textContent='Beweeg door het beveiligingsdoolhof of druk E voor SENTRY: de camera stopt, draait rond en flitst automatisch zodra een inbreker in de volledige zichtbare gang verschijnt.'; startBtn.textContent='\u25b6 START GAME'; startGame();\n});\nsoundBtn.addEventListener('click',toggleSound);\n\ndocument.querySelectorAll('[data-dir]').forEach(btn=>{\n  btn.addEventListener('pointerdown',e=>{e.preventDefault();setDesired(DIRS[btn.dataset.dir]);root.focus();},{passive:false});\n});\ndocument.getElementById('flashTouch').addEventListener('pointerdown',e=>{e.preventDefault();useFlash(false);root.focus();},{passive:false});\nif(sentryTouch) sentryTouch.addEventListener('pointerdown',e=>{e.preventDefault();toggleSentry();root.focus();},{passive:false});\n\nlet swipeStart=null;\ndocument.getElementById('stage').addEventListener('pointerdown',e=>{\n  if(!['ready','playing'].includes(state.phase)) return; swipeStart={x:e.clientX,y:e.clientY}; root.focus();\n});\ndocument.getElementById('stage').addEventListener('pointerup',e=>{\n  if(!swipeStart) return; const dx=e.clientX-swipeStart.x,dy=e.clientY-swipeStart.y;swipeStart=null;\n  if(Math.hypot(dx,dy)<20){useFlash(false);return;}\n  if(Math.abs(dx)>Math.abs(dy)) setDesired(dx>0?DIRS.right:DIRS.left); else setDesired(dy>0?DIRS.down:DIRS.up);\n});\n\nfunction loop(now){\n  const dt=Math.min(.034,Math.max(0,(now-state.lastTime)\/1000||0));state.lastTime=now;update(dt);draw();requestAnimationFrame(loop);\n}\n\n\/\/ Stilstaand voorbeeld achter het startscherm.\nstate.map=generateMaze(1);\nconst previewSpawn=nearestPassable(2,MAZE_H-4);state.map[previewSpawn.y][previewSpawn.x]=' ';\nconst previewDir=chooseInitialDir(previewSpawn);state.player={x:previewSpawn.x,y:previewSpawn.y,dir:previewDir,desired:previewDir,facing:previewDir};createCameraFeet(state.player);state.burglars=[0,1,2,3].map(makeBurglar);state.initialDots=1;state.dots=1;centerView(true);renderScores();updateHUD();draw();requestAnimationFrame(loop);\n})();\n<\/script>\n<\/body>\n<\/html>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-b262009 e-flex e-con-boxed e-con e-parent\" data-id=\"b262009\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-4a4e74b elementor-widget elementor-widget-spacer\" data-id=\"4a4e74b\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Home Under Construction \u00a0 \u00a0 https:\/\/cctvacademy.be\/wp-content\/uploads\/2026\/09\/cctv-pacmaze-intro-movie-2-cctv-academy.mp4 CCTV PAC-MAZE \u2014 Night Watch CCTV PAC-MAZE NIGHT WATCH \/\/ LIVE FEEDSECURITY MAZE CHASE Score000000 High000000 Level01 Cams\ud83d\udcf9 \ud83d\udcf9 \ud83d\udcf9 Evidence0% FlashREADY SECURITY SYSTEM \/\/ STANDBY CCTV NIGHT WATCH Beweeg continu door het beveiligingsdoolhof zoals in de video. Kies v\u00f3\u00f3r een kruispunt al je volgende richting: het spel onthoudt [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_canvas","meta":{"_acf_changed":false,"zakra_page_container_layout":"customizer","zakra_page_sidebar_layout":"customizer","zakra_remove_content_margin":false,"zakra_sidebar":"customizer","zakra_transparent_header":"customizer","zakra_logo":0,"zakra_main_header_style":"default","zakra_menu_item_color":"","zakra_menu_item_hover_color":"","zakra_menu_item_active_color":"","zakra_menu_active_style":"","zakra_page_header":true,"h5ap_radio_sources":[],"_glsr_average":0,"_glsr_ranking":0,"_glsr_reviews":0,"footnotes":""},"class_list":["post-51255","page","type-page","status-publish","hentry"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>CCTV Pacman - RAS\/AIS CCTV Academy<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cctvacademy.be\/nl\/cctv-pacman\/\" \/>\n<meta property=\"og:locale\" content=\"nl_BE\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"CCTV Pacman - RAS\/AIS CCTV Academy\" \/>\n<meta property=\"og:description\" content=\"Home Under Construction \u00a0 \u00a0 https:\/\/cctvacademy.be\/wp-content\/uploads\/2026\/09\/cctv-pacmaze-intro-movie-2-cctv-academy.mp4 CCTV PAC-MAZE \u2014 Night Watch CCTV PAC-MAZE NIGHT WATCH \/\/ LIVE FEEDSECURITY MAZE CHASE Score000000 High000000 Level01 Cams\ud83d\udcf9 \ud83d\udcf9 \ud83d\udcf9 Evidence0% FlashREADY SECURITY SYSTEM \/\/ STANDBY CCTV NIGHT WATCH Beweeg continu door het beveiligingsdoolhof zoals in de video. Kies v\u00f3\u00f3r een kruispunt al je volgende richting: het spel onthoudt [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cctvacademy.be\/nl\/cctv-pacman\/\" \/>\n<meta property=\"og:site_name\" content=\"RAS\/AIS CCTV Academy\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/profile.php?id=61576769152561\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-20T09:29:43+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/cctvacademy.be\\\/cctv-pacman\\\/\",\"url\":\"https:\\\/\\\/cctvacademy.be\\\/cctv-pacman\\\/\",\"name\":\"CCTV Pacman - RAS\\\/AIS CCTV Academy\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/cctvacademy.be\\\/#website\"},\"datePublished\":\"2026-09-19T18:31:11+00:00\",\"dateModified\":\"2026-09-20T09:29:43+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/cctvacademy.be\\\/cctv-pacman\\\/#breadcrumb\"},\"inLanguage\":\"nl-BE\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/cctvacademy.be\\\/cctv-pacman\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/cctvacademy.be\\\/cctv-pacman\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/cctvacademy.be\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"CCTV Pacman\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/cctvacademy.be\\\/#website\",\"url\":\"https:\\\/\\\/cctvacademy.be\\\/\",\"name\":\"CCTV Academy\",\"description\":\"CCTV Opleidingen en CCTV Micro Learning\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/cctvacademy.be\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"nl-BE\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"CCTV Pacman - RAS\/AIS CCTV Academy","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cctvacademy.be\/nl\/cctv-pacman\/","og_locale":"nl_BE","og_type":"article","og_title":"CCTV Pacman - RAS\/AIS CCTV Academy","og_description":"Home Under Construction \u00a0 \u00a0 https:\/\/cctvacademy.be\/wp-content\/uploads\/2026\/09\/cctv-pacmaze-intro-movie-2-cctv-academy.mp4 CCTV PAC-MAZE \u2014 Night Watch CCTV PAC-MAZE NIGHT WATCH \/\/ LIVE FEEDSECURITY MAZE CHASE Score000000 High000000 Level01 Cams\ud83d\udcf9 \ud83d\udcf9 \ud83d\udcf9 Evidence0% FlashREADY SECURITY SYSTEM \/\/ STANDBY CCTV NIGHT WATCH Beweeg continu door het beveiligingsdoolhof zoals in de video. Kies v\u00f3\u00f3r een kruispunt al je volgende richting: het spel onthoudt [&hellip;]","og_url":"https:\/\/cctvacademy.be\/nl\/cctv-pacman\/","og_site_name":"RAS\/AIS CCTV Academy","article_publisher":"https:\/\/www.facebook.com\/profile.php?id=61576769152561","article_modified_time":"2026-09-20T09:29:43+00:00","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/cctvacademy.be\/cctv-pacman\/","url":"https:\/\/cctvacademy.be\/cctv-pacman\/","name":"CCTV Pacman - RAS\/AIS CCTV Academy","isPartOf":{"@id":"https:\/\/cctvacademy.be\/#website"},"datePublished":"2026-09-19T18:31:11+00:00","dateModified":"2026-09-20T09:29:43+00:00","breadcrumb":{"@id":"https:\/\/cctvacademy.be\/cctv-pacman\/#breadcrumb"},"inLanguage":"nl-BE","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cctvacademy.be\/cctv-pacman\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/cctvacademy.be\/cctv-pacman\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cctvacademy.be\/"},{"@type":"ListItem","position":2,"name":"CCTV Pacman"}]},{"@type":"WebSite","@id":"https:\/\/cctvacademy.be\/#website","url":"https:\/\/cctvacademy.be\/","name":"CCTV Academy","description":"CCTV Opleidingen en CCTV Micro Learning","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cctvacademy.be\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"nl-BE"}]}},"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/pages\/51255","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/comments?post=51255"}],"version-history":[{"count":60,"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/pages\/51255\/revisions"}],"predecessor-version":[{"id":51328,"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/pages\/51255\/revisions\/51328"}],"wp:attachment":[{"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/media?parent=51255"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}