Initial import of Brookhaven site
This commit is contained in:
196
templates/play_ai.html
Normal file
196
templates/play_ai.html
Normal file
@@ -0,0 +1,196 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Tapdown vs CPU — Cyber Sale Edition</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-slate-950 text-white min-h-screen flex flex-col">
|
||||
<!-- Top bar -->
|
||||
<header class="max-w-5xl mx-auto w-full px-4 py-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-slate-400">Player:</span>
|
||||
<span class="font-semibold">{{ name }}</span>
|
||||
<span class="mx-2 opacity-50">·</span>
|
||||
<span class="text-sm text-slate-400">Persona:</span>
|
||||
<span class="font-semibold">{{ persona }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm opacity-80">Difficulty</label>
|
||||
<select id="difficulty" class="text-black p-1 rounded">
|
||||
<option value="easy">Easy</option>
|
||||
<option value="normal" selected>Normal</option>
|
||||
<option value="hard">Hard</option>
|
||||
<option value="insane">Insane</option>
|
||||
</select>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Arena -->
|
||||
<main class="max-w-5xl mx-auto w-full px-4 flex-1">
|
||||
<div class="mb-3 text-center text-slate-300 text-sm">
|
||||
Beat the <b>Super Saver</b> bot — then browse Cyber Sale deals!
|
||||
</div>
|
||||
|
||||
<div class="relative h-44 rounded-2xl bg-slate-900/60 border border-slate-800 overflow-hidden">
|
||||
<div class="absolute inset-y-0 left-1/2 w-0.5 bg-slate-700"></div>
|
||||
<div class="absolute inset-y-0 left-0 w-1 bg-emerald-500"></div>
|
||||
<div class="absolute inset-y-0 right-0 w-1 bg-cyan-500"></div>
|
||||
<div id="puck" class="absolute top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-white shadow"></div>
|
||||
</div>
|
||||
|
||||
<p id="status" class="mt-3 text-center text-lg"></p>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mt-6">
|
||||
<button id="tapLeft" class="text-2xl py-12 rounded-xl bg-emerald-600 active:scale-95 font-semibold">YOU TAP</button>
|
||||
<button id="tapRight" class="text-2xl py-12 rounded-xl bg-cyan-700 opacity-60 cursor-not-allowed font-semibold" disabled>CPU</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-center gap-3">
|
||||
<button id="startBtn" class="bg-emerald-500 hover:bg-emerald-600 px-5 py-2 rounded-lg font-semibold">Start</button>
|
||||
<button id="resetBtn" class="bg-slate-800 hover:bg-slate-700 px-5 py-2 rounded-lg font-semibold">Reset</button>
|
||||
<a id="shopBtn" href="/"
|
||||
class="bg-slate-900 border border-slate-700 px-5 py-2 rounded-lg font-semibold">Back</a>
|
||||
</div>
|
||||
|
||||
<!-- Cyber Sale CTA under the arena -->
|
||||
<div class="mt-8 text-center">
|
||||
<a href="{{ url_for('index') }}" class="text-slate-300 underline decoration-dotted">Cyber Sale details & QR on the promo page</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="max-w-5xl mx-auto w-full px-4 py-6 text-center text-slate-400 text-xs">
|
||||
Tip: Holding the button auto-taps. Bursts & fatigue make the bot feel human.
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
/* --- Pull persona from template to lightly tweak the feel --- */
|
||||
const PERSONA = "{{ persona }}";
|
||||
|
||||
/* --- Physics constants --- */
|
||||
const EDGE = 100.0;
|
||||
const BASE_IMPULSE = 7.0;
|
||||
let FRICTION = 0.92;
|
||||
const TICK_HZ = 60;
|
||||
|
||||
/* Persona perk (very light-touch) */
|
||||
let leftImpulseMult = 1.0;
|
||||
if (PERSONA === "Speed Demon") leftImpulseMult = 1.02;
|
||||
if (PERSONA === "Tank") FRICTION = 0.91;
|
||||
|
||||
/* --- Game state --- */
|
||||
let puck = 0.0, vel = 0.0, running = false;
|
||||
let lastTapLeft = 0, lastTapRight = 0;
|
||||
let MIN_TAP_MS = 85;
|
||||
|
||||
/* --- CPU model --- */
|
||||
const DIFF = {
|
||||
easy: { baseHz: 4.8, burstHz: 7.2, burstProb: 0.20, whiff: 0.08, friction: 0.92 },
|
||||
normal: { baseHz: 6.6, burstHz: 9.2, burstProb: 0.28, whiff: 0.05, friction: 0.92 },
|
||||
hard: { baseHz: 8.2, burstHz: 11.5, burstProb: 0.35, whiff: 0.035, friction: 0.91 },
|
||||
insane: { baseHz: 9.8, burstHz: 13.5, burstProb: 0.45, whiff: 0.02, friction: 0.905 },
|
||||
};
|
||||
let cpuCfg = DIFF.normal;
|
||||
let cpuBurstUntil = 0;
|
||||
let cpuNextTapAt = 0;
|
||||
|
||||
/* --- UI elements --- */
|
||||
const statusEl = document.getElementById("status");
|
||||
const puckEl = document.getElementById("puck");
|
||||
const leftBtn = document.getElementById("tapLeft");
|
||||
const startBtn = document.getElementById("startBtn");
|
||||
const resetBtn = document.getElementById("resetBtn");
|
||||
const diffSel = document.getElementById("difficulty");
|
||||
|
||||
/* --- Helpers --- */
|
||||
function flash(msg){ statusEl.textContent = msg; setTimeout(()=>statusEl.textContent="", 1200); }
|
||||
function setDifficulty(key){
|
||||
cpuCfg = DIFF[key] || DIFF.normal;
|
||||
FRICTION = cpuCfg.friction;
|
||||
}
|
||||
diffSel.onchange = () => { setDifficulty(diffSel.value); flash(`Difficulty: ${diffSel.value.toUpperCase()}`); };
|
||||
|
||||
/* --- Controls --- */
|
||||
leftBtn.addEventListener("pointerdown", (e) => {
|
||||
e.preventDefault(); tap("left");
|
||||
const t = setInterval(()=> tap("left"), 100);
|
||||
const stop = ()=>{ clearInterval(t); window.removeEventListener("pointerup", stop); leftBtn.removeEventListener("pointerleave", stop); };
|
||||
window.addEventListener("pointerup", stop, { once: true });
|
||||
leftBtn.addEventListener("pointerleave", stop, { once: true });
|
||||
});
|
||||
startBtn.onclick = startMatch;
|
||||
resetBtn.onclick = resetMatch;
|
||||
|
||||
/* --- Game functions --- */
|
||||
function startMatch(){
|
||||
resetMatch();
|
||||
running = true;
|
||||
scheduleCpuTap(performance.now());
|
||||
flash("Fight!");
|
||||
}
|
||||
function resetMatch(){
|
||||
running = false; vel = 0; puck = 0; updatePuck(puck);
|
||||
statusEl.textContent = "";
|
||||
}
|
||||
function tap(side){
|
||||
if(!running) return;
|
||||
const now = performance.now();
|
||||
if(side === "left"){
|
||||
if(now - lastTapLeft < MIN_TAP_MS) return;
|
||||
lastTapLeft = now;
|
||||
vel -= BASE_IMPULSE * leftImpulseMult;
|
||||
} else {
|
||||
if(now - lastTapRight < MIN_TAP_MS) return;
|
||||
lastTapRight = now;
|
||||
vel += BASE_IMPULSE;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- CPU scheduling --- */
|
||||
function scheduleCpuTap(now){
|
||||
if(now > cpuBurstUntil && Math.random() < cpuCfg.burstProb){
|
||||
cpuBurstUntil = now + (300 + Math.random()*400);
|
||||
}
|
||||
const inBurst = now < cpuBurstUntil;
|
||||
const hz = inBurst ? cpuCfg.burstHz : cpuCfg.baseHz;
|
||||
const intervalMs = 1000 * ( -Math.log(1 - Math.random()) / hz );
|
||||
cpuNextTapAt = now + intervalMs;
|
||||
}
|
||||
function maybeCpuTap(now){
|
||||
if(now < cpuNextTapAt) return;
|
||||
if(Math.random() > cpuCfg.whiff) tap("right");
|
||||
scheduleCpuTap(now);
|
||||
}
|
||||
|
||||
/* --- Loop --- */
|
||||
function tick(){
|
||||
const dt = 1.0 / TICK_HZ;
|
||||
if(running){
|
||||
vel *= FRICTION;
|
||||
puck += vel * dt * 6.0;
|
||||
const now = performance.now();
|
||||
maybeCpuTap(now);
|
||||
if(puck <= -EDGE || puck >= EDGE){
|
||||
running = false;
|
||||
statusEl.textContent = puck <= -EDGE ? "YOU WIN! 🥳" : "CPU WINS! 🤖";
|
||||
}
|
||||
}
|
||||
updatePuck(puck);
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
|
||||
function updatePuck(p){
|
||||
const track = document.querySelector(".relative.h-44");
|
||||
const w = track.clientWidth;
|
||||
const x = (p / 100) * (w/2 - 10);
|
||||
puckEl.style.left = `calc(50% + ${x}px)`;
|
||||
}
|
||||
|
||||
/* init */
|
||||
setDifficulty("normal");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user