dokázal bys vytvořit hru běžící v prohlížeči?


claude claude-code-guide Explore general-purpose Plan statusline-setup

- claude: Catch-all for any task that doesn't fit a more specific agent. FleetView's default when no agent name is typed. (Tools: *)

- claude-code-guide: Use this agent when the user asks questions ("Can Claude...", "Does Claude...", "How do I...") about: (1) Claude Code (the CLI tool) - features, hooks, slash commands, MCP servers, settings, IDE integrations, keyboard shortcuts; (2) Claude Agent SDK - building custom agents; (3) Claude API (formerly Anthropic API) - Messages API for directly passing messages to Claude, Tool Runner (`client.beta.messages.tool_runner`) for running an agentic loop over your own tools, manual tool-use loops, Managed Agents for server-hosted agents with a managed sandbox, prompt caching, and general Anthropic SDK usage; (4) Claude Tag (Claude in Slack) - what it is, setting it up for a Slack workspace, `/install-slack-app`; (5) `claude plugin eval` (writing and running plugin eval suites, its JSON/report, sandbox, CI, early-access enablement) and the `/skill-doctor` report. **IMPORTANT:** Before spawning a new agent, check if there is already a running or recently completed claude-code-guide agent that you can continue via SendMessage. (Tools: Bash, Read, WebFetch, WebSearch)

- Explore: Read-only search agent for broad fan-out searches — when answering means sweeping many files, directories, or naming conventions and you only need the conclusion, not the file dumps. It reads excerpts rather than whole files, so it locates code; it doesn't review or audit it. Specify search breadth: "medium" for moderate exploration, "very thorough" for multiple locations and naming conventions. (Tools: All tools except Agent, Artifact, ExitPlanMode, Edit, Write, NotebookEdit)

- general-purpose: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)

- Plan: Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs. (Tools: All tools except Agent, Artifact, ExitPlanMode, Edit, Write, NotebookEdit)

- statusline-setup: Use this agent to configure the user's Claude Code status line setting. (Tools: Read, Edit)


- design: Create a design canvas - a multi-artboard visual design published as an Artifact that runs Claude Design's canvas editor (an early preview of Claude Design inside Claude Code). You DRAFT the design as .dc.html artboards laid out on one pan/zoom canvas; where saving is enabled for the user's account they refine every element visually (click-to-select, a properties panel, inline text editing, undo/redo) and Save publishes a new version for everyone, otherwise they get a view-and-export (PNG/PDF) preview of your draft. Good for UI mockups and screen flows, landing pages, marketing and social graphics, and print pieces - posters, flyers, brochures as single-page artboards; memos and reports as one flowing artboard. Use when someone wants a design, mockup, wireframe, UI or screen design, landing page, poster, flyer, brochure, banner, card, one-pager, or any visual layout they would rather tweak by hand than in code. Only for CREATING or re-seeding a canvas; an existing one is edited in its published Artifact.
- dataviz: Use this skill whenever you are about to create ANY chart, graph, plot, dashboard, or data visualization, in ANY output medium — an HTML or React artifact, inline SVG, plotting code in any library (matplotlib, plotly, d3, Recharts, …), an image/PNG you will render and upload, or a chart shared into Slack. Read it BEFORE writing the first line of chart code, choosing chart colors, building a stat tile / meter / KPI row, or laying out a dashboard. Produces visualizations that read as one system — elegant, accessible, consistent in light and dark — using a brand-neutral placeholder palette you swap for your own. Teaches a design-system-agnostic method: a form heuristic, a color formula with a runnable validator, mark specs, and interaction rules. A validated default palette is documented in `references/palette.md` — swap that file's values for your brand's. Triggers on: "chart", "graph", "plot", "data viz", "visualization", "dashboard", "analytics", "visualize data", "categorical colors", "sequential / diverging palette", "stat tile", "sparkline", "heatmap", "legend", "axis", "tooltip", "chart colors", "color by series".
- artifact-design: Design guidance and fundamentals for Artifacts. - Load before writing any artifact, including a skill-instructed Markdown one - Markdown is never a shortcut past the design pass.
- artifact-diagramming: Diagramming know-how for Artifacts - when a picture earns its place, how to draw one that shows the real mechanism, and the inline-SVG mechanics that keep it legible in both themes.
- artifact-capabilities: Runtime capabilities a published Artifact page can be granted — behavior static HTML cannot provide on its own, such as the page reading live or connected data, remembering what people do on it (a poll, a sign-up sheet, a checklist, a document edited in place — it saves new versions of itself), keeping state shared across viewers, knowing who is viewing, asking Claude a question of its own, storing files people add, or handing the viewer a file to save. Serves this user's live capability roster and the typed call definitions. Load it whenever the user asks for an artifact needing any such runtime behavior.
- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- code-review: Review the current diff, or a PR number/branch/path target, for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high→max: broader coverage, may include uncertain findings; ultra: deep multi-agent review in the cloud); with no level given, it reuses the level you typed last. Pass --comment to post findings as inline PR comments, or --fix to apply the findings to the working tree after the review. For ultra on a GitHub.com PR target, --post asks to post the finished review’s findings to the PR as a single comment from the user’s GitHub account (not a review; the launch dialog still confirms in interactive sessions, while non-interactive mode posts on the flag alone) and --no-post hides that option.
- simplify: Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled cloud agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring cloud agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.
TRIGGER — read BEFORE opening the target file; don't skip because it "looks like a one-liner" — whenever: the prompt names Claude/Anthropic in any form (Claude, Anthropic, Fable, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens).
SKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).
- claude-in-chrome: Automates your Chrome browser to interact with web pages - clicking elements, filling forms, capturing screenshots, reading console logs, and navigating sites. Opens pages in new tabs within your existing Chrome session. Requires site-level permissions before executing (configured in the extension). - When the user wants to interact with web pages, automate browser tasks, capture screenshots, read console logs, or perform any browser-based actions. Always invoke BEFORE attempting to use any mcp__claude-in-chrome__* tools.
- run: Launch and drive this project's app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).
- init: Initialize a new CLAUDE.md file with codebase documentation
- security-review: Complete a security review of the pending changes on the current branch

design dataviz artifact-design artifact-diagramming artifact-capabilities update-config keybindings-help code-review simplify fewer-permission-prompts loop schedule claude-api claude-in-chrome run init security-review


<total_tokens>15000000 tokens left</total_tokens>


Jasně, to zvládnu. Hru udělám jako samostatnou HTML stránku (Artifact), kterou si otevřeš v prohlížeči a případně sdílíš.

Abych trefil, co chceš:


AskUserQuestion

Question: Jaký typ hry chceš?

Choices:
Rychlá akce – např. vyhýbání se překážkám, střílečka, endless runner. Ovládání klávesnicí/myší, skóre, postupné zrychlování.
Hra na přemýšlení – např. 2048, Sokoban, match-3, minolovka, piškvorky proti AI.
Remake známé hry – Snake, Tetris, Breakout, Pong, Space Invaders.
Udělám dobře zpracovanou hru dle svého uvážení (nejspíš vylepšený Snake nebo Breakout s efekty a leaderboardem).

Question: Jak má vypadat a fungovat?

Choices:
Čisté tvary, plynulé animace, jemné částicové efekty, light/dark motiv.
Pixelový nebo neonový vzhled, CRT efekt, chiptune-ladění.
Zvolím vizuál, který se k dané hře nejlíp hodí.


Your questions have been answered: "Jaký typ hry chceš?"="Retro klasika", "Jak má vypadat a fungovat?"="Retro pixel/neon". You can now continue with these answers in mind.

Question: Jaký typ hry chceš?

Choices:
Rychlá akce – např. vyhýbání se překážkám, střílečka, endless runner. Ovládání klávesnicí/myší, skóre, postupné zrychlování.
Hra na přemýšlení – např. 2048, Sokoban, match-3, minolovka, piškvorky proti AI.
Remake známé hry – Snake, Tetris, Breakout, Pong, Space Invaders.
Udělám dobře zpracovanou hru dle svého uvážení (nejspíš vylepšený Snake nebo Breakout s efekty a leaderboardem).

Question: Jak má vypadat a fungovat?

Choices:
Čisté tvary, plynulé animace, jemné částicové efekty, light/dark motiv.
Pixelový nebo neonový vzhled, CRT efekt, chiptune-ladění.
Zvolím vizuál, který se k dané hře nejlíp hodí.


<total_tokens>14971753 tokens left</total_tokens>


Launching skill: artifact-design


Approach this as the design lead at a small studio known for their versatility, giving every client a visual identity pitched at the treatment the task actually calls for. Make deliberate choices about palette, typography, and layout that are specific to this subject, and avoid templated designs.

## Read the request first

Calibrate treatment, not whether to design. A doc deserves the same craft as a landing page - what changes is the treatment that craft is delivered in. Format is not part of this read: author HTML, and publish Markdown only when a loaded skill explicitly instructs it - a Markdown publish keeps its filename as its title and takes almost none of the craft below, and is never a way to save time.

Many requests call for a more utilitarian treatment: a plan, a memo, a demo. Make it polished: include real typographic hierarchy, considered spacing, and a proper palette, but avoid over-designing. Most pages do not need a flashy, gigantic hero. Keep flourishes tasteful and limited.

Some requests call for an editorial treatment: a landing page, a game, an app or tool they'll keep or share.

When unsure: a well-composed page is never the wrong answer; an over-designed visual identity sometimes is.

Fundamentals below apply to everything. The editorial process after that runs only when the read above says so.

## Fundamentals for every artifact

**Honor what's already there** Look for an existing design system first - CLAUDE.md, a tokens or theme file, existing component styles. When one exists, apply it; everything below fills gaps and never overrides. Precedence is always: the user's own words, then the project's existing system, then your choices.

**Ground it in the subject.** If the subject isn't already clear, pin it: one concrete subject, its audience, and the page's single job. The subject's own world - its materials, instruments, vernacular - is where distinctive choices come from. Build with real content throughout, never lorem.

**Pair typefaces** Typography carries the page even when the page isn't about typography. Google Fonts is the one font host the Artifact CSP admits - link it directly (`<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=...&display=swap">`); a face from anywhere else must be inlined as a @font-face data URI or it falls back silently. Either way, declare a real fallback stack. Keep running text near 65 characters wide; set a type scale and stay on it; give headings `text-wrap: balance`, body text room to breathe, and uppercase labels a touch of letter-spacing.

**Choose neutrals, don't default to them.** A pure mid-grey reads as unconsidered; a grey with a slight hue bias toward the page's accent reads as chosen. Pure white and near-black are fine grounds when they suit the subject - the point is that the neutral was picked, not inherited.

**Design both themes.** The page renders in the viewer's theme, and the viewer has three states, not two: an explicit choice stamps `data-theme="dark"` / `data-theme="light"` on the root element, and the default "system" setting stamps *nothing* - most viewers see the un-stamped document, where only `prefers-color-scheme` separates light from dark. Structure the CSS token-level for all three: the bare `:root` block defines the complete light palette (for a deliberately dark-first design, swap light and dark consistently through this whole pattern); `@media (prefers-color-scheme: dark)` redefines only the tokens, guarded as `:root:not([data-theme="light"])` so an explicit light choice beats a dark OS; `:root[data-theme="dark"]` redefines them again so the toggle also wins in the other direction. Style components through the tokens, never directly inside a media or `[data-theme]` block - a color whose only definition sits behind `[data-theme]` never applies in the un-stamped state, and the page renders one theme's text on the other theme's ground. Two more rules keep each theme resolving as a set: the artifact composites over a ground the viewer paints in *its* theme, so `body` must set an explicit `background` from a token - a transparent body silently borrows the host's ground; and every element that sets a color takes it from the same token set as the surface behind it, never a literal that only works in one theme. Before publishing, scan the stylesheet for any color declared only inside a media or `[data-theme]` block - that is the classic unreadable-artifact bug. Give the second theme the same care as the first - don't naively invert; keep contrast legible and the accent working on both grounds. A design that deliberately commits to one visual world (a neon arcade screen, a letterpress invitation) may stay single-theme - then skip the media query and stamps entirely but still paint the background and every color explicitly, so the page holds on either host ground; make it a choice, not an omission.

**Let layout do the spacing.** Lay out sibling groups with flex or grid and `gap`, not per-element margins that silently collapse or double. Wide content - tables, code, diagrams - gets `overflow-x: auto` on its own container so the page body never scrolls sideways. Reach for `font-variant-numeric: tabular-nums` wherever digits line up in columns.

**Avoid AI-generated design** AI-generated design currently clusters around a few looks: warm cream (#F4F1EA) with a serif display and terracotta accent; near-black with a lone acid-green or vermilion pop; broadsheet hairline rules with dense columns; a purple-to-blue gradient hero on white; Inter or Space Grotesk as the "safe" face; emoji as section markers; everything centered; `rounded-lg` everywhere; accent bar/rail on rounded cards. Where the user pins down a visual direction, follow it exactly - their words always win, including when they ask for one of these looks. Where nothing is specified, don't spend that freedom on one of these defaults.

**Build cleanly** Be cognizant of overlapping elements, cascade collisions, silent font fallbacks; visual bugs hide in the gap between source and output. Close every non-void element, double-quote attributes, give keyboard focus a visible state, respect `prefers-reduced-motion`. For generative or decorative graphics, reach for Canvas or WebGL rather than hand-authoring long SVG path data.

**CSS rules** When writing the CSS, watch your selector specificities. It is easy to generate classes that cancel each other out - a type-based selector like `.section` fighting an element-based one like `.cta` over padding and margins between sections. Structure the cascade so it doesn't silently undo your spacing.

**Writing the copy** Words are design material, not decoration. Write from the user's side of the screen - name things by what people recognize, not how the system is built (a person manages *notifications*, not *webhook config*). Active voice; a control says exactly what happens ("Publish", then a toast that says "Published"). Errors explain what went wrong and how to fix it - no apologies, no vagueness. Specific beats clever.

**Name the page like a product, not a caption.** The `<title>` is the artifact's name in the gallery and the browser tab, and it sets the reader's first impression of care. Give the page a real name: a short noun phrase, typically two to four words, specific to the subject - or, for a page that exists to answer one question, that question itself, which is then the page's name. Stop at the name - a title that carries its own explainer after a dash or colon reads as generated filler. The name must also identify the page among many: in the gallery it sits beside dozens of other artifacts, and a generic category label that could sit on any of them fails as a name just as surely as an appended explainer. When a candidate title pairs the name with a generic word - a greeting, a category, a page-type label - the name is the half to keep; a trim that drops the identity and keeps the generic word produces exactly the title that could sit on any page. And the rule removes explainers, it does not impose brevity: a multi-word title that already reads as one specific name is finished, and shortening it further only makes it generic. The one-sentence publish `description` is where the explanation belongs; the gallery shows it right under the title.

**Structure is information** Structural devices, numbering, eyebrows, dividers, labels, should encode something true about the content, not decorate it. Many generic designs use numbered markers (01 / 02 / 03), but that's only appropriate if the content actually is a sequence - like a real process or a typed timeline where order carries information the reader needs. Question if choices like numbered markers actually make sense before incorporating them.

**When it's a UI, not a document** A dashboard or tool is scanned and operated, not read top-to-bottom, so the craft shifts from typography to information design. Surface the summary before the detail; encode state in form as well as number - a pill, a chip, a severity stripe - so what needs attention reads at a glance. Semantic color (good / warning / critical) is separate from the accent hue and doesn't count as your accent. Give sparklines and charts the same care as type: an area fill, a faint grid, an emphasized endpoint. What's interactive should look interactive.

## Process

Before writing code, sketch a short design plan - a compact token system with color, type, and layout:
- **Color**: describe the palette as 4-6 named hex values.
- **Type**: typefaces for 2+ roles - a characterful display face used with restraint, a complementary body face, and a utility face for captions or data if needed.
- **Layout**: a layout concept in one or two sentences.

Then build, following the plan and deriving every color and type decision from it.

## When the request is editorial

The stance shifts: the client has already rejected proposals that felt templated, and is paying for a distinctive point of view. Make opinionated calls, and take one real aesthetic risk where it serves the work.

Review the design plan against the subject before building: if any part of it reads like the generic default you would produce for any similar page, revise that part, and note what you changed and why. Only after you've confirmed the plan's uniqueness do you write the code, following the revised plan exactly.

**Principles**

- The hero is a thesis: open with the most characteristic thing in the subject's world - headline, image, live demo, interactive moment.
- Typography carries the personality of the page. Pair the display and body faces deliberately, not the same families you would reach for on any other project, and set a clear type scale with intentional weights, widths, and spacing. Make the type treatment itself a memorable part of the design, not a neutral delivery vehicle for the content.
- Leverage motion deliberately. Think about where and if animation can serve the subject: a page-load sequence, a scroll-triggered reveal, hover micro-interactions, ambient atmosphere. An orchestrated moment usually lands harder than scattered effects; choose what the direction calls for. However, sometimes less is more, and extra animation contributes to the feeling that the design is AI-generated.
- Match complexity to the vision. Maximalist directions need elaborate execution; minimal directions need precision in spacing, type, and detail. Elegance is executing the chosen vision well.
- Spend your boldness in one place; keep everything around it quiet. If the accent fights the ground, shift it toward analogous or drop saturation rather than replacing it.


<total_tokens>14970968 tokens left</total_tokens>


btw jakej typ hry vytváříš (tetris,pacman....)


Write

<title>Neon Drop</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Silkscreen:wght@400;700&family=Share+Tech+Mono&display=swap">

NEON DROP

STACK · CLEAR · SURVIVE

HOLD

          <canvas id="hold" width="120" height="120"></canvas>

SCORE

0

LINES

0

LEVEL

1

BEST

0

        <canvas id="board" width="300" height="600"></canvas>

NEON DROP

Fill a row to clear it.
Don’t let the stack reach the top.

            <button id="startBtn">PRESS START</button>

NEXT

          <canvas id="next" width="120" height="330"></canvas>

←→ Move

↑ / X Rotate

Z Rotate CCW

↓ Soft drop

Space Hard drop

C Hold

P Pause

      <button type="button" data-act="left" aria-label="Move left">◀</button>
      <button type="button" data-act="rotate" aria-label="Rotate">↻</button>
      <button type="button" data-act="right" aria-label="Move right">▶</button>
      <button type="button" data-act="soft" aria-label="Soft drop">▼</button>
      <button type="button" data-act="hard" aria-label="Hard drop">⤓</button>
      <button type="button" data-act="hold" aria-label="Hold piece">HOLD</button>

Neon Drop needs JavaScript enabled.


File created successfully at: /tmp/claude-1000/-home-tonda/809a6534-5852-498c-9da5-0fcab3335c9a/scratchpad/neon-drop.html (file state is current in your context — no need to Read it back)

<title>Neon Drop</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Silkscreen:wght@400;700&family=Share+Tech+Mono&display=swap">

NEON DROP

STACK · CLEAR · SURVIVE

HOLD

          <canvas id="hold" width="120" height="120"></canvas>

SCORE

0

LINES

0

LEVEL

1

BEST

0

        <canvas id="board" width="300" height="600"></canvas>

NEON DROP

Fill a row to clear it.
Don’t let the stack reach the top.

            <button id="startBtn">PRESS START</button>

NEXT

          <canvas id="next" width="120" height="330"></canvas>

←→ Move

↑ / X Rotate

Z Rotate CCW

↓ Soft drop

Space Hard drop

C Hold

P Pause

      <button type="button" data-act="left" aria-label="Move left">◀</button>
      <button type="button" data-act="rotate" aria-label="Rotate">↻</button>
      <button type="button" data-act="right" aria-label="Move right">▶</button>
      <button type="button" data-act="soft" aria-label="Soft drop">▼</button>
      <button type="button" data-act="hard" aria-label="Hard drop">⤓</button>
      <button type="button" data-act="hold" aria-label="Hold piece">HOLD</button>

Neon Drop needs JavaScript enabled.


Prompt: btw jakej typ hry vytváříš (tetris,pacman....)


<total_tokens>14929500 tokens left</total_tokens>


Dělám **Tetris** – neonová varianta „Neon Drop": 7-bag náhodné dílky, ghost piece, hold, fronta dalších 5, hard/soft drop, level a zrychlování, ukládání nejlepšího skóre, klávesnice i dotykové ovládání na mobilu. Vzhled je arkádový CRT (scanlines, neonový nápis).

Pokračuju v kódu.


The file /tmp/claude-1000/-home-tonda/809a6534-5852-498c-9da5-0fcab3335c9a/scratchpad/neon-drop.html has been updated successfully. (file state is current in your context — no need to Read it back)

<title>Neon Drop</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Silkscreen:wght@400;700&family=Share+Tech+Mono&display=swap">

NEON DROP

STACK · CLEAR · SURVIVE

HOLD

          <canvas id="hold" width="120" height="120"></canvas>

SCORE

0

LINES

0

LEVEL

1

BEST

0

        <canvas id="board" width="300" height="600"></canvas>

NEON DROP

Fill a row to clear it.
Don’t let the stack reach the top.

            <button id="startBtn">PRESS START</button>

NEXT

          <canvas id="next" width="120" height="330"></canvas>

←→ Move

↑ / X Rotate

Z Rotate CCW

↓ Soft drop

Space Hard drop

C Hold

P Pause

      <button type="button" data-act="left" aria-label="Move left">◀</button>
      <button type="button" data-act="rotate" aria-label="Rotate">↻</button>
      <button type="button" data-act="right" aria-label="Move right">▶</button>
      <button type="button" data-act="soft" aria-label="Soft drop">▼</button>
      <button type="button" data-act="hard" aria-label="Hard drop">⤓</button>
      <button type="button" data-act="hold" aria-label="Hold piece">HOLD</button>

Neon Drop needs JavaScript enabled.

+function setup(cv,w,h){ cv.width=w*DPR; cv.height=h*DPR; cv.getContext('2d').setTransform(DPR,0,0,DPR,0,0); }

+const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

+let grid=null, current=null, heldPiece=null, canHold=true, bag=[], queue=[];

+let score=0, lines=0, level=1, combo=-1, best=0;

+let gravityInterval=1000, gravityAcc=0, lockTimer=0, lockResets=0, grounded=false;

+let running=false, paused=false, over=false, clearing=null, last=0, softHeld=false;


<total_tokens>14926870 tokens left</total_tokens>


Edit

function collides(p, cells){
  const cs = cells || PIECES[p.type][p.rot];
  for(let i=0;i<cs.length;i++){
    const x=p.x+cs[i][0], y=p.y+cs[i][1];
    if(x<0 || x>=COLS || y>=ROWS) return true;
    if(y>=0 && grid[y][x]) return true;
  }
  return false;
}
function nextFromBag(){
  if(bag.length===0){
    bag=['I','O','T','S','Z','J','L'];
    for(let i=bag.length-1;i>0;i--){ const j=Math.random()*(i+1)|0; const t=bag[i]; bag[i]=bag[j]; bag[j]=t; }
  }
  return bag.pop();
}
function ensureQueue(){ while(queue.length<7) queue.push(nextFromBag()); }

function newPiece(type){
  current={type:type, rot:0, x:3, y:0};
  gravityAcc=0; lockTimer=0; lockResets=0;
  grounded=collides({type:type,rot:0,x:3,y:1});
  if(collides(current)) endGame();
}
function nextPiece(){ ensureQueue(); newPiece(queue.shift()); ensureQueue(); }

function afterMove(){
  grounded=collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});
  if(grounded){ if(lockResets<15){ lockTimer=0; lockResets++; } }
  else { lockTimer=0; }
}
function move(dx){
  if(!current||paused||over||clearing) return;
  if(!collides({type:current.type,rot:current.rot,x:current.x+dx,y:current.y})){
    current.x+=dx; afterMove();
  }
}
function softStep(){
  return !collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});
}
function rotate(dir){
  if(!current||paused||over||clearing||current.type==='O') return;
  const from=current.rot, to=(from+dir+4)%4;
  const table=current.type==='I'?KICK_I:KICK_JLSTZ;
  const kicks=table[''+from+to];
  const cells=PIECES[current.type][to];
  for(let i=0;i<kicks.length;i++){
    const nx=current.x+kicks[i][0], ny=current.y+kicks[i][1];
    if(!collides({type:current.type,rot:to,x:nx,y:ny}, cells)){
      current.x=nx; current.y=ny; current.rot=to; afterMove(); return;
    }
  }
}
function ghostY(){
  let y=current.y;
  while(!collides({type:current.type,rot:current.rot,x:current.x,y:y+1})) y++;
  return y;
}
function hardDrop(){
  if(!current||paused||over||clearing) return;
  let d=0;
  while(!collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1})){ current.y++; d++; }
  score+=d*2;
  lockPiece();
}
function holdPiece(){
  if(!current||!canHold||paused||over||clearing) return;
  const cur=current.type;
  if(heldPiece){ const h=heldPiece; heldPiece=cur; newPiece(h); }
  else { heldPiece=cur; nextPiece(); }
  canHold=false;
}
function lockPiece(){
  const cs=PIECES[current.type][current.rot];
  let topOut=true;
  for(let i=0;i<cs.length;i++){
    const x=current.x+cs[i][0], y=current.y+cs[i][1];
    if(y>=0){ grid[y][x]=current.type; topOut=false; }
  }
  lockResets=0; lockTimer=0; grounded=false; canHold=true;
  const full=[];
  for(let y=0;y<ROWS;y++){
    let f=true;
    for(let x=0;x<COLS;x++){ if(!grid[y][x]){ f=false; break; } }
    if(f) full.push(y);
  }
  if(full.length){
    resolveClear(full);
  } else {
    combo=-1;
    if(topOut){ endGame(); return; }
    nextPiece();
  }
  updateHUD();
}
function resolveClear(rows){
  clearing={rows:rows, t:0};
  const n=rows.length;
  const base=[0,100,300,500,800][n];
  combo++;
  let gain=base*level;
  if(combo>0) gain+=50*combo*level;
  score+=gain;
  lines+=n;
  const nl=Math.floor(lines/10)+1;
  if(nl>level){ level=nl; recalcGravity(); }
  if(!reduceMotion){
    for(let r=0;r<rows.length;r++){
      const y=rows[r];
      for(let x=0;x<COLS;x++) spawnParticles(x,y,grid[y][x]);
    }
  }
  updateHUD();
}
function finishClear(){
  const set={};
  for(let i=0;i<clearing.rows.length;i++) set[clearing.rows[i]]=1;
  const kept=[];
  for(let y=0;y<ROWS;y++){ if(!set[y]) kept.push(grid[y]); }
  while(kept.length<ROWS) kept.unshift(new Array(COLS).fill(0));
  grid=kept;
  clearing=null;
  nextPiece();
}
function recalcGravity(){
  gravityInterval=Math.max(15, Math.pow(0.8-((level-1)*0.007), level-1)*1000);
}
function tick(dtMs){
  if(!current) return;
  const step = softHeld ? Math.min(gravityInterval, 45) : gravityInterval;
  gravityAcc+=dtMs;
  let moved=false;
  while(gravityAcc>=step){
    gravityAcc-=step;
    if(softStep()){ current.y++; moved=true; if(softHeld) score+=1; }
    else { grounded=true; gravityAcc=0; break; }
  }
  if(moved){
    lockTimer=0;
    grounded=collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});
    if(softHeld) updateHUD();
  }
  if(grounded){
    lockTimer+=dtMs;
    if(lockTimer>=LOCK_DELAY) lockPiece();
  }
}
function endGame(){
  over=true; running=false;
  if(score>best){
    best=score;
    try{ localStorage.setItem('neondrop.best', String(best)); }catch(e){}
    elBest.textContent=best;
  }
  showOverlay('GAME OVER', 'Score '+score+'  ·  Lines '+lines+'\nLevel '+level, 'PLAY AGAIN');
}
function updateHUD(){
  elScore.textContent=score;
  elLines.textContent=lines;
  elLevel.textContent=level;
}


function collides(p, cells){
  const cs = cells || PIECES[p.type][p.rot];
  for(let i=0;i<cs.length;i++){
    const x=p.x+cs[i][0], y=p.y+cs[i][1];
    if(x<0 || x>=COLS || y>=ROWS) return true;
    if(y>=0 && grid[y][x]) return true;
  }
  return false;
}
function nextFromBag(){
  if(bag.length===0){
    bag=['I','O','T','S','Z','J','L'];
    for(let i=bag.length-1;i>0;i--){ const j=Math.random()*(i+1)|0; const t=bag[i]; bag[i]=bag[j]; bag[j]=t; }
  }
  return bag.pop();
}
function ensureQueue(){ while(queue.length<7) queue.push(nextFromBag()); }

function newPiece(type){
  current={type:type, rot:0, x:3, y:0};
  gravityAcc=0; lockTimer=0; lockResets=0;
  grounded=collides({type:type,rot:0,x:3,y:1});
  if(collides(current)) endGame();
}
function nextPiece(){ ensureQueue(); newPiece(queue.shift()); ensureQueue(); }

function afterMove(){
  grounded=collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});
  if(grounded){ if(lockResets<15){ lockTimer=0; lockResets++; } }
  else { lockTimer=0; }
}
function move(dx){
  if(!current||paused||over||clearing) return;
  if(!collides({type:current.type,rot:current.rot,x:current.x+dx,y:current.y})){
    current.x+=dx; afterMove();
  }
}
function softStep(){
  return !collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});
}
function rotate(dir){
  if(!current||paused||over||clearing||current.type==='O') return;
  const from=current.rot, to=(from+dir+4)%4;
  const table=current.type==='I'?KICK_I:KICK_JLSTZ;
  const kicks=table[''+from+to];
  const cells=PIECES[current.type][to];
  for(let i=0;i<kicks.length;i++){
    const nx=current.x+kicks[i][0], ny=current.y+kicks[i][1];
    if(!collides({type:current.type,rot:to,x:nx,y:ny}, cells)){
      current.x=nx; current.y=ny; current.rot=to; afterMove(); return;
    }
  }
}
function ghostY(){
  let y=current.y;
  while(!collides({type:current.type,rot:current.rot,x:current.x,y:y+1})) y++;
  return y;
}
function hardDrop(){
  if(!current||paused||over||clearing) return;
  let d=0;
  while(!collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1})){ current.y++; d++; }
  score+=d*2;
  lockPiece();
}
function holdPiece(){
  if(!current||!canHold||paused||over||clearing) return;
  const cur=current.type;
  if(heldPiece){ const h=heldPiece; heldPiece=cur; newPiece(h); }
  else { heldPiece=cur; nextPiece(); }
  canHold=false;
}
function lockPiece(){
  const cs=PIECES[current.type][current.rot];
  let topOut=true;
  for(let i=0;i<cs.length;i++){
    const x=current.x+cs[i][0], y=current.y+cs[i][1];
    if(y>=0){ grid[y][x]=current.type; topOut=false; }
  }
  lockResets=0; lockTimer=0; grounded=false; canHold=true;
  const full=[];
  for(let y=0;y<ROWS;y++){
    let f=true;
    for(let x=0;x<COLS;x++){ if(!grid[y][x]){ f=false; break; } }
    if(f) full.push(y);
  }
  if(full.length){
    resolveClear(full);
  } else {
    combo=-1;
    if(topOut){ endGame(); return; }
    nextPiece();
  }
  updateHUD();
}
function resolveClear(rows){
  clearing={rows:rows, t:0};
  const n=rows.length;
  const base=[0,100,300,500,800][n];
  combo++;
  let gain=base*level;
  if(combo>0) gain+=50*combo*level;
  score+=gain;
  lines+=n;
  const nl=Math.floor(lines/10)+1;
  if(nl>level){ level=nl; recalcGravity(); }
  if(!reduceMotion){
    for(let r=0;r<rows.length;r++){
      const y=rows[r];
      for(let x=0;x<COLS;x++) spawnParticles(x,y,grid[y][x]);
    }
  }
  updateHUD();
}
function finishClear(){
  const set={};
  for(let i=0;i<clearing.rows.length;i++) set[clearing.rows[i]]=1;
  const kept=[];
  for(let y=0;y<ROWS;y++){ if(!set[y]) kept.push(grid[y]); }
  while(kept.length<ROWS) kept.unshift(new Array(COLS).fill(0));
  grid=kept;
  clearing=null;
  nextPiece();
}
function recalcGravity(){
  gravityInterval=Math.max(15, Math.pow(0.8-((level-1)*0.007), level-1)*1000);
}
function tick(dtMs){
  if(!current) return;
  const step = softHeld ? Math.min(gravityInterval, 45) : gravityInterval;
  gravityAcc+=dtMs;
  let moved=false;
  while(gravityAcc>=step){
    gravityAcc-=step;
    if(softStep()){ current.y++; moved=true; if(softHeld) score+=1; }
    else { grounded=true; gravityAcc=0; break; }
  }
  if(moved){
    lockTimer=0;
    grounded=collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});
    if(softHeld) updateHUD();
  }
  if(grounded){
    lockTimer+=dtMs;
    if(lockTimer>=LOCK_DELAY) lockPiece();
  }
}
function endGame(){
  over=true; running=false;
  if(score>best){
    best=score;
    try{ localStorage.setItem('neondrop.best', String(best)); }catch(e){}
    elBest.textContent=best;
  }
  showOverlay('GAME OVER', 'Score '+score+'  ·  Lines '+lines+'\nLevel '+level, 'PLAY AGAIN');
}
function updateHUD(){
  elScore.textContent=score;
  elLines.textContent=lines;
  elLevel.textContent=level;
}

+  const cs = cells || PIECES[p.type][p.rot];

+    if(x<0 || x>=COLS || y>=ROWS) return true;

+    if(y>=0 && grid[y][x]) return true;

+    for(let i=bag.length-1;i>0;i--){ const j=Math.random()*(i+1)|0; const t=bag[i]; bag[i]=bag[j]; bag[j]=t; }

+function ensureQueue(){ while(queue.length<7) queue.push(nextFromBag()); }

+  current={type:type, rot:0, x:3, y:0};

+  gravityAcc=0; lockTimer=0; lockResets=0;

+function nextPiece(){ ensureQueue(); newPiece(queue.shift()); ensureQueue(); }

+  if(grounded){ if(lockResets<15){ lockTimer=0; lockResets++; } }

+  if(!current||paused||over||clearing) return;

+  return !collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});

+  if(!current||paused||over||clearing||current.type==='O') return;

+  const from=current.rot, to=(from+dir+4)%4;

+  const table=current.type==='I'?KICK_I:KICK_JLSTZ;

+  const cells=PIECES[current.type][to];

+    const nx=current.x+kicks[i][0], ny=current.y+kicks[i][1];

+    if(!collides({type:current.type,rot:to,x:nx,y:ny}, cells)){

+      current.x=nx; current.y=ny; current.rot=to; afterMove(); return;

+  while(!collides({type:current.type,rot:current.rot,x:current.x,y:y+1})) y++;

+  while(!collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1})){ current.y++; d++; }

+  if(!current||!canHold||paused||over||clearing) return;

+  if(heldPiece){ const h=heldPiece; heldPiece=cur; newPiece(h); }

+  const cs=PIECES[current.type][current.rot];

+    const x=current.x+cs[i][0], y=current.y+cs[i][1];

+    if(y>=0){ grid[y][x]=current.type; topOut=false; }

+  lockResets=0; lockTimer=0; grounded=false; canHold=true;

+    for(let x=0;x<COLS;x++){ if(!grid[y][x]){ f=false; break; } }

+  if(nl>level){ level=nl; recalcGravity(); }

+      for(let x=0;x<COLS;x++) spawnParticles(x,y,grid[y][x]);

+  for(let i=0;i<clearing.rows.length;i++) set[clearing.rows[i]]=1;

+  for(let y=0;y<ROWS;y++){ if(!set[y]) kept.push(grid[y]); }

+  while(kept.length<ROWS) kept.unshift(new Array(COLS).fill(0));

+  gravityInterval=Math.max(15, Math.pow(0.8-((level-1)*0.007), level-1)*1000);

+  const step = softHeld ? Math.min(gravityInterval, 45) : gravityInterval;

+    if(softStep()){ current.y++; moved=true; if(softHeld) score+=1; }

+    else { grounded=true; gravityAcc=0; break; }

+    grounded=collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});

+    if(lockTimer>=LOCK_DELAY) lockPiece();

+    try{ localStorage.setItem('neondrop.best', String(best)); }catch(e){}

+  showOverlay('GAME OVER', 'Score '+score+'  ·  Lines '+lines+'\nLevel '+level, 'PLAY AGAIN');


<total_tokens>14924122 tokens left</total_tokens>


function rr(ctx,x,y,w,h,r){
  if(w<0)w=0; if(h<0)h=0;
  const k=Math.min(r, w/2, h/2);
  ctx.beginPath();
  ctx.moveTo(x+k,y);
  ctx.arcTo(x+w,y,x+w,y+h,k);
  ctx.arcTo(x+w,y+h,x,y+h,k);
  ctx.arcTo(x,y+h,x,y,k);
  ctx.arcTo(x,y,x+w,y,k);
  ctx.closePath();
}
function paintCell(ctx,px,py,s,color,glow){
  ctx.save();
  if(glow){ ctx.shadowColor=color; ctx.shadowBlur=13; }
  ctx.fillStyle=color;
  rr(ctx, px+2, py+2, s-4, s-4, 4); ctx.fill();
  ctx.shadowBlur=0;
  ctx.globalAlpha=0.32; ctx.fillStyle='#ffffff';
  rr(ctx, px+4, py+4, s-8, (s-8)*0.4, 3); ctx.fill();
  ctx.globalAlpha=0.22; ctx.fillStyle='#000000';
  rr(ctx, px+4, py+(s-4)-(s-8)*0.26, s-8, (s-8)*0.26, 3); ctx.fill();
  ctx.restore();
}
function drawPieceInBox(ctx,bx,by,bw,bh,type,dim){
  const cells=PIECES[type][0];
  let a=9,b=-9,c=9,d=-9;
  for(let i=0;i<cells.length;i++){
    const cx=cells[i][0], cy=cells[i][1];
    if(cx<a)a=cx; if(cx>b)b=cx; if(cy<c)c=cy; if(cy>d)d=cy;
  }
  const w=b-a+1, h=d-c+1;
  const s=Math.min(bw/(w+1), bh/(h+1), 20);
  const ox=bx+(bw-w*s)/2-a*s, oy=by+(bh-h*s)/2-c*s;
  const col=COLORS[type];
  for(let i=0;i<cells.length;i++){
    const px=ox+cells[i][0]*s, py=oy+cells[i][1]*s;
    ctx.save();
    ctx.globalAlpha=dim?0.4:1;
    ctx.shadowColor=col; ctx.shadowBlur=8;
    ctx.fillStyle=col;
    rr(ctx, px+1.5, py+1.5, s-3, s-3, 3); ctx.fill();
    ctx.restore();
  }
}
function spawnParticles(x,y,type){
  const col=COLORS[type]||'#ffffff';
  for(let i=0;i<3;i++){
    if(particles.length>240) return;
    particles.push({
      x:x*CELL+CELL/2+(Math.random()-0.5)*CELL,
      y:y*CELL+CELL/2+(Math.random()-0.5)*CELL,
      vx:(Math.random()-0.5)*0.22,
      vy:(Math.random()-0.9)*0.28,
      life:1, color:col
    });
  }
}
function updateParticles(dtMs){
  for(let i=particles.length-1;i>=0;i--){
    const p=particles[i];
    p.x+=p.vx*dtMs; p.y+=p.vy*dtMs; p.vy+=0.0018*dtMs;
    p.life-=dtMs/700;
    if(p.life<=0) particles.splice(i,1);
  }
}
function render(){
  bctx.clearRect(0,0,300,600);
  bctx.fillStyle='rgba(125,90,190,0.05)';
  bctx.fillRect(0,0,300,600);
  bctx.strokeStyle='rgba(130,100,200,0.10)';
  bctx.lineWidth=1;
  for(let x=1;x<COLS;x++){ bctx.beginPath(); bctx.moveTo(x*CELL+0.5,0); bctx.lineTo(x*CELL+0.5,600); bctx.stroke(); }
  for(let y=1;y<ROWS;y++){ bctx.beginPath(); bctx.moveTo(0,y*CELL+0.5); bctx.lineTo(300,y*CELL+0.5); bctx.stroke(); }

  if(grid){
    for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++){
      const v=grid[y][x]; if(!v) continue;
      if(clearing && clearing.rows.indexOf(y)>=0){
        const k=Math.max(0, 1-clearing.t/CLEAR_TIME);
        bctx.save();
        bctx.globalAlpha=0.35+0.65*k;
        bctx.shadowColor='#ffffff'; bctx.shadowBlur=24*k;
        bctx.fillStyle='#ffffff';
        rr(bctx, x*CELL+2, y*CELL+2, CELL-4, CELL-4, 4); bctx.fill();
        bctx.restore();
      } else {
        paintCell(bctx, x*CELL, y*CELL, CELL, COLORS[v], false);
      }
    }
  }

  if(current && !clearing){
    const gy=ghostY(), cells=PIECES[current.type][current.rot], col=COLORS[current.type];
    bctx.save();
    bctx.globalAlpha=0.30; bctx.strokeStyle=col; bctx.lineWidth=2;
    for(let i=0;i<cells.length;i++){
      const yy=gy+cells[i][1];
      if(yy<0) continue;
      rr(bctx, (current.x+cells[i][0])*CELL+3, yy*CELL+3, CELL-6, CELL-6, 3); bctx.stroke();
    }
    bctx.restore();
    for(let i=0;i<cells.length;i++){
      const yy=current.y+cells[i][1];
      if(yy<0) continue;
      paintCell(bctx, (current.x+cells[i][0])*CELL, yy*CELL, CELL, col, true);
    }
  }

  for(let i=0;i<particles.length;i++){
    const p=particles[i];
    bctx.globalAlpha=Math.max(0, p.life);
    bctx.fillStyle=p.color;
    bctx.fillRect(p.x-2, p.y-2, 4, 4);
  }
  bctx.globalAlpha=1;

  hctx.clearRect(0,0,120,120);
  if(heldPiece) drawPieceInBox(hctx, 0, 0, 120, 120, heldPiece, !canHold);
  nctx.clearRect(0,0,120,330);
  const q=queue.slice(0,5);
  for(let i=0;i<q.length;i++) drawPieceInBox(nctx, 0, i*66, 120, 66, q[i], false);
}


function rr(ctx,x,y,w,h,r){
  if(w<0)w=0; if(h<0)h=0;
  const k=Math.min(r, w/2, h/2);
  ctx.beginPath();
  ctx.moveTo(x+k,y);
  ctx.arcTo(x+w,y,x+w,y+h,k);
  ctx.arcTo(x+w,y+h,x,y+h,k);
  ctx.arcTo(x,y+h,x,y,k);
  ctx.arcTo(x,y,x+w,y,k);
  ctx.closePath();
}
function paintCell(ctx,px,py,s,color,glow){
  ctx.save();
  if(glow){ ctx.shadowColor=color; ctx.shadowBlur=13; }
  ctx.fillStyle=color;
  rr(ctx, px+2, py+2, s-4, s-4, 4); ctx.fill();
  ctx.shadowBlur=0;
  ctx.globalAlpha=0.32; ctx.fillStyle='#ffffff';
  rr(ctx, px+4, py+4, s-8, (s-8)*0.4, 3); ctx.fill();
  ctx.globalAlpha=0.22; ctx.fillStyle='#000000';
  rr(ctx, px+4, py+(s-4)-(s-8)*0.26, s-8, (s-8)*0.26, 3); ctx.fill();
  ctx.restore();
}
function drawPieceInBox(ctx,bx,by,bw,bh,type,dim){
  const cells=PIECES[type][0];
  let a=9,b=-9,c=9,d=-9;
  for(let i=0;i<cells.length;i++){
    const cx=cells[i][0], cy=cells[i][1];
    if(cx<a)a=cx; if(cx>b)b=cx; if(cy<c)c=cy; if(cy>d)d=cy;
  }
  const w=b-a+1, h=d-c+1;
  const s=Math.min(bw/(w+1), bh/(h+1), 20);
  const ox=bx+(bw-w*s)/2-a*s, oy=by+(bh-h*s)/2-c*s;
  const col=COLORS[type];
  for(let i=0;i<cells.length;i++){
    const px=ox+cells[i][0]*s, py=oy+cells[i][1]*s;
    ctx.save();
    ctx.globalAlpha=dim?0.4:1;
    ctx.shadowColor=col; ctx.shadowBlur=8;
    ctx.fillStyle=col;
    rr(ctx, px+1.5, py+1.5, s-3, s-3, 3); ctx.fill();
    ctx.restore();
  }
}
function spawnParticles(x,y,type){
  const col=COLORS[type]||'#ffffff';
  for(let i=0;i<3;i++){
    if(particles.length>240) return;
    particles.push({
      x:x*CELL+CELL/2+(Math.random()-0.5)*CELL,
      y:y*CELL+CELL/2+(Math.random()-0.5)*CELL,
      vx:(Math.random()-0.5)*0.22,
      vy:(Math.random()-0.9)*0.28,
      life:1, color:col
    });
  }
}
function updateParticles(dtMs){
  for(let i=particles.length-1;i>=0;i--){
    const p=particles[i];
    p.x+=p.vx*dtMs; p.y+=p.vy*dtMs; p.vy+=0.0018*dtMs;
    p.life-=dtMs/700;
    if(p.life<=0) particles.splice(i,1);
  }
}
function render(){
  bctx.clearRect(0,0,300,600);
  bctx.fillStyle='rgba(125,90,190,0.05)';
  bctx.fillRect(0,0,300,600);
  bctx.strokeStyle='rgba(130,100,200,0.10)';
  bctx.lineWidth=1;
  for(let x=1;x<COLS;x++){ bctx.beginPath(); bctx.moveTo(x*CELL+0.5,0); bctx.lineTo(x*CELL+0.5,600); bctx.stroke(); }
  for(let y=1;y<ROWS;y++){ bctx.beginPath(); bctx.moveTo(0,y*CELL+0.5); bctx.lineTo(300,y*CELL+0.5); bctx.stroke(); }

  if(grid){
    for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++){
      const v=grid[y][x]; if(!v) continue;
      if(clearing && clearing.rows.indexOf(y)>=0){
        const k=Math.max(0, 1-clearing.t/CLEAR_TIME);
        bctx.save();
        bctx.globalAlpha=0.35+0.65*k;
        bctx.shadowColor='#ffffff'; bctx.shadowBlur=24*k;
        bctx.fillStyle='#ffffff';
        rr(bctx, x*CELL+2, y*CELL+2, CELL-4, CELL-4, 4); bctx.fill();
        bctx.restore();
      } else {
        paintCell(bctx, x*CELL, y*CELL, CELL, COLORS[v], false);
      }
    }
  }

  if(current && !clearing){
    const gy=ghostY(), cells=PIECES[current.type][current.rot], col=COLORS[current.type];
    bctx.save();
    bctx.globalAlpha=0.30; bctx.strokeStyle=col; bctx.lineWidth=2;
    for(let i=0;i<cells.length;i++){
      const yy=gy+cells[i][1];
      if(yy<0) continue;
      rr(bctx, (current.x+cells[i][0])*CELL+3, yy*CELL+3, CELL-6, CELL-6, 3); bctx.stroke();
    }
    bctx.restore();
    for(let i=0;i<cells.length;i++){
      const yy=current.y+cells[i][1];
      if(yy<0) continue;
      paintCell(bctx, (current.x+cells[i][0])*CELL, yy*CELL, CELL, col, true);
    }
  }

  for(let i=0;i<particles.length;i++){
    const p=particles[i];
    bctx.globalAlpha=Math.max(0, p.life);
    bctx.fillStyle=p.color;
    bctx.fillRect(p.x-2, p.y-2, 4, 4);
  }
  bctx.globalAlpha=1;

  hctx.clearRect(0,0,120,120);
  if(heldPiece) drawPieceInBox(hctx, 0, 0, 120, 120, heldPiece, !canHold);
  nctx.clearRect(0,0,120,330);
  const q=queue.slice(0,5);
  for(let i=0;i<q.length;i++) drawPieceInBox(nctx, 0, i*66, 120, 66, q[i], false);
}

+  if(glow){ ctx.shadowColor=color; ctx.shadowBlur=13; }

+  ctx.globalAlpha=0.32; ctx.fillStyle='#ffffff';

+  ctx.globalAlpha=0.22; ctx.fillStyle='#000000';

+    const cx=cells[i][0], cy=cells[i][1];

+    if(cx<a)a=cx; if(cx>b)b=cx; if(cy<c)c=cy; if(cy>d)d=cy;

+  const ox=bx+(bw-w*s)/2-a*s, oy=by+(bh-h*s)/2-c*s;

+    const px=ox+cells[i][0]*s, py=oy+cells[i][1]*s;

+    ctx.shadowColor=col; ctx.shadowBlur=8;

+      x:x*CELL+CELL/2+(Math.random()-0.5)*CELL,

+      y:y*CELL+CELL/2+(Math.random()-0.5)*CELL,

+  for(let i=particles.length-1;i>=0;i--){

+    p.x+=p.vx*dtMs; p.y+=p.vy*dtMs; p.vy+=0.0018*dtMs;

+    if(p.life<=0) particles.splice(i,1);

+  for(let x=1;x<COLS;x++){ bctx.beginPath(); bctx.moveTo(x*CELL+0.5,0); bctx.lineTo(x*CELL+0.5,600); bctx.stroke(); }

+  for(let y=1;y<ROWS;y++){ bctx.beginPath(); bctx.moveTo(0,y*CELL+0.5); bctx.lineTo(300,y*CELL+0.5); bctx.stroke(); }

+    for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++){

+      const v=grid[y][x]; if(!v) continue;

+      if(clearing && clearing.rows.indexOf(y)>=0){

+        const k=Math.max(0, 1-clearing.t/CLEAR_TIME);

+        bctx.shadowColor='#ffffff'; bctx.shadowBlur=24*k;

+        rr(bctx, x*CELL+2, y*CELL+2, CELL-4, CELL-4, 4); bctx.fill();

+        paintCell(bctx, x*CELL, y*CELL, CELL, COLORS[v], false);

+    const gy=ghostY(), cells=PIECES[current.type][current.rot], col=COLORS[current.type];

+    bctx.globalAlpha=0.30; bctx.strokeStyle=col; bctx.lineWidth=2;

+      rr(bctx, (current.x+cells[i][0])*CELL+3, yy*CELL+3, CELL-6, CELL-6, 3); bctx.stroke();

+      paintCell(bctx, (current.x+cells[i][0])*CELL, yy*CELL, CELL, col, true);

+    bctx.globalAlpha=Math.max(0, p.life);

+  if(heldPiece) drawPieceInBox(hctx, 0, 0, 120, 120, heldPiece, !canHold);

+  for(let i=0;i<q.length;i++) drawPieceInBox(nctx, 0, i*66, 120, 66, q[i], false);


<total_tokens>14921379 tokens left</total_tokens>


function showOverlay(title,text,btn){
  ovTitle.textContent=title;
  if(text){ ovText.innerHTML=text.replace(/\n/g,'<br>'); ovText.style.display=''; }
  else { ovText.textContent=''; ovText.style.display='none'; }
  startBtn.textContent=btn;
  overlay.classList.remove('hidden');
}
function hideOverlay(){ overlay.classList.add('hidden'); }

function startGame(){
  grid=[];
  for(let y=0;y<ROWS;y++) grid.push(new Array(COLS).fill(0));
  bag=[]; queue=[]; ensureQueue();
  score=0; lines=0; level=1; combo=-1;
  heldPiece=null; canHold=true;
  particles.length=0; clearing=null;
  gravityAcc=0; lockTimer=0; lockResets=0; grounded=false;
  over=false; paused=false;
  recalcGravity(); updateHUD();
  nextPiece();
  running=true; last=0;
  hideOverlay();
}
function togglePause(){
  if(over||!running) return;
  paused=!paused;
  if(paused) showOverlay('PAUSED', '', 'RESUME');
  else hideOverlay();
}

startBtn.addEventListener('click', function(){
  if(paused) togglePause();
  else startGame();
});

const repeats={};
function rep(id,fn){ fn(); repeats[id]={fn:fn, acc:0, go:false}; }
function stopRep(id){ delete repeats[id]; }
function updateRepeats(dtMs){
  for(const id in repeats){
    const r=repeats[id];
    r.acc+=dtMs;
    if(!r.go){ if(r.acc>=150){ r.go=true; r.acc-=150; r.fn(); } }
    else { while(r.acc>=40){ r.acc-=40; r.fn(); } }
  }
}

window.addEventListener('keydown', function(e){
  const k=e.key.toLowerCase();
  if(k==='arrowleft'||k==='arrowright'||k==='arrowup'||k==='arrowdown'||k===' ') e.preventDefault();
  if(!running && (k===' '||k==='enter')){ if(!paused) startGame(); return; }
  if(k==='p'){ togglePause(); return; }
  if(paused||over||!running||e.repeat) return;
  switch(k){
    case 'arrowleft':  rep('left', function(){ move(-1); }); break;
    case 'arrowright': rep('right', function(){ move(1); }); break;
    case 'arrowdown':  softHeld=true; break;
    case 'arrowup': case 'x': rotate(1); break;
    case 'z': rotate(3); break;
    case ' ': hardDrop(); break;
    case 'c': case 'shift': holdPiece(); break;
  }
});
window.addEventListener('keyup', function(e){
  const k=e.key.toLowerCase();
  if(k==='arrowleft') stopRep('left');
  else if(k==='arrowright') stopRep('right');
  else if(k==='arrowdown') softHeld=false;
});

const touch=document.getElementById('touch');
if(touch){
  touch.addEventListener('contextmenu', function(e){ e.preventDefault(); });
  const btns=touch.querySelectorAll('button');
  for(let i=0;i<btns.length;i++){
    (function(btn){
      const act=btn.getAttribute('data-act');
      btn.addEventListener('pointerdown', function(e){
        e.preventDefault();
        if(!running||paused||over) return;
        if(act==='left') rep('tl', function(){ move(-1); });
        else if(act==='right') rep('tr', function(){ move(1); });
        else if(act==='soft') softHeld=true;
        else if(act==='rotate') rotate(1);
        else if(act==='hard') hardDrop();
        else if(act==='hold') holdPiece();
      });
      const end=function(){
        if(act==='left') stopRep('tl');
        else if(act==='right') stopRep('tr');
        else if(act==='soft') softHeld=false;
      };
      btn.addEventListener('pointerup', end);
      btn.addEventListener('pointercancel', end);
      btn.addEventListener('pointerleave', end);
    })(btns[i]);
  }
}

document.addEventListener('visibilitychange', function(){
  if(document.hidden && running && !paused && !over) togglePause();
});

function frame(ts){
  requestAnimationFrame(frame);
  if(!last) last=ts;
  let dt=ts-last; last=ts;
  if(dt>100) dt=100;
  if(running && !paused){
    updateRepeats(dt);
    if(clearing){
      clearing.t+=dt;
      if(clearing.t>=CLEAR_TIME) finishClear();
    } else {
      tick(dt);
    }
    updateParticles(dt);
  }
  render();
}
requestAnimationFrame(frame);

showOverlay('NEON DROP', 'Fill a row to clear it.\nDon’t let the stack reach the top.', 'PRESS START');
render();


function showOverlay(title,text,btn){
  ovTitle.textContent=title;
  if(text){ ovText.innerHTML=text.replace(/\n/g,'<br>'); ovText.style.display=''; }
  else { ovText.textContent=''; ovText.style.display='none'; }
  startBtn.textContent=btn;
  overlay.classList.remove('hidden');
}
function hideOverlay(){ overlay.classList.add('hidden'); }

function startGame(){
  grid=[];
  for(let y=0;y<ROWS;y++) grid.push(new Array(COLS).fill(0));
  bag=[]; queue=[]; ensureQueue();
  score=0; lines=0; level=1; combo=-1;
  heldPiece=null; canHold=true;
  particles.length=0; clearing=null;
  gravityAcc=0; lockTimer=0; lockResets=0; grounded=false;
  over=false; paused=false;
  recalcGravity(); updateHUD();
  nextPiece();
  running=true; last=0;
  hideOverlay();
}
function togglePause(){
  if(over||!running) return;
  paused=!paused;
  if(paused) showOverlay('PAUSED', '', 'RESUME');
  else hideOverlay();
}

startBtn.addEventListener('click', function(){
  if(paused) togglePause();
  else startGame();
});

const repeats={};
function rep(id,fn){ fn(); repeats[id]={fn:fn, acc:0, go:false}; }
function stopRep(id){ delete repeats[id]; }
function updateRepeats(dtMs){
  for(const id in repeats){
    const r=repeats[id];
    r.acc+=dtMs;
    if(!r.go){ if(r.acc>=150){ r.go=true; r.acc-=150; r.fn(); } }
    else { while(r.acc>=40){ r.acc-=40; r.fn(); } }
  }
}

window.addEventListener('keydown', function(e){
  const k=e.key.toLowerCase();
  if(k==='arrowleft'||k==='arrowright'||k==='arrowup'||k==='arrowdown'||k===' ') e.preventDefault();
  if(!running && (k===' '||k==='enter')){ if(!paused) startGame(); return; }
  if(k==='p'){ togglePause(); return; }
  if(paused||over||!running||e.repeat) return;
  switch(k){
    case 'arrowleft':  rep('left', function(){ move(-1); }); break;
    case 'arrowright': rep('right', function(){ move(1); }); break;
    case 'arrowdown':  softHeld=true; break;
    case 'arrowup': case 'x': rotate(1); break;
    case 'z': rotate(3); break;
    case ' ': hardDrop(); break;
    case 'c': case 'shift': holdPiece(); break;
  }
});
window.addEventListener('keyup', function(e){
  const k=e.key.toLowerCase();
  if(k==='arrowleft') stopRep('left');
  else if(k==='arrowright') stopRep('right');
  else if(k==='arrowdown') softHeld=false;
});

const touch=document.getElementById('touch');
if(touch){
  touch.addEventListener('contextmenu', function(e){ e.preventDefault(); });
  const btns=touch.querySelectorAll('button');
  for(let i=0;i<btns.length;i++){
    (function(btn){
      const act=btn.getAttribute('data-act');
      btn.addEventListener('pointerdown', function(e){
        e.preventDefault();
        if(!running||paused||over) return;
        if(act==='left') rep('tl', function(){ move(-1); });
        else if(act==='right') rep('tr', function(){ move(1); });
        else if(act==='soft') softHeld=true;
        else if(act==='rotate') rotate(1);
        else if(act==='hard') hardDrop();
        else if(act==='hold') holdPiece();
      });
      const end=function(){
        if(act==='left') stopRep('tl');
        else if(act==='right') stopRep('tr');
        else if(act==='soft') softHeld=false;
      };
      btn.addEventListener('pointerup', end);
      btn.addEventListener('pointercancel', end);
      btn.addEventListener('pointerleave', end);
    })(btns[i]);
  }
}

document.addEventListener('visibilitychange', function(){
  if(document.hidden && running && !paused && !over) togglePause();
});

function frame(ts){
  requestAnimationFrame(frame);
  if(!last) last=ts;
  let dt=ts-last; last=ts;
  if(dt>100) dt=100;
  if(running && !paused){
    updateRepeats(dt);
    if(clearing){
      clearing.t+=dt;
      if(clearing.t>=CLEAR_TIME) finishClear();
    } else {
      tick(dt);
    }
    updateParticles(dt);
  }
  render();
}
requestAnimationFrame(frame);

showOverlay('NEON DROP', 'Fill a row to clear it.\nDon’t let the stack reach the top.', 'PRESS START');
render();

for(let i=0;i<q.length;i++) drawPieceInBox(nctx, 0, i*66, 120, 66, q[i], false);

+  if(text){ ovText.innerHTML=text.replace(/\n/g,'<br>'); ovText.style.display=''; }

+  else { ovText.textContent=''; ovText.style.display='none'; }

+function hideOverlay(){ overlay.classList.add('hidden'); }

+  for(let y=0;y<ROWS;y++) grid.push(new Array(COLS).fill(0));

+  gravityAcc=0; lockTimer=0; lockResets=0; grounded=false;

+  if(paused) showOverlay('PAUSED', '', 'RESUME');

+function rep(id,fn){ fn(); repeats[id]={fn:fn, acc:0, go:false}; }

+function stopRep(id){ delete repeats[id]; }

+  if(k==='arrowleft'||k==='arrowright'||k==='arrowup'||k==='arrowdown'||k===' ') e.preventDefault();

+  if(!running && (k===' '||k==='enter')){ if(!paused) startGame(); return; }

+  if(k==='p'){ togglePause(); return; }

+  if(paused||over||!running||e.repeat) return;

+    case 'arrowleft':  rep('left', function(){ move(-1); }); break;

+    case 'arrowright': rep('right', function(){ move(1); }); break;

+    case 'arrowdown':  softHeld=true; break;

+    case 'arrowup': case 'x': rotate(1); break;

+    case 'c': case 'shift': holdPiece(); break;

+  else if(k==='arrowright') stopRep('right');

+  else if(k==='arrowdown') softHeld=false;

+  touch.addEventListener('contextmenu', function(e){ e.preventDefault(); });

+  const btns=touch.querySelectorAll('button');

+      const act=btn.getAttribute('data-act');

+      btn.addEventListener('pointerdown', function(e){

+        if(!running||paused||over) return;

+        if(act==='left') rep('tl', function(){ move(-1); });

+        else if(act==='right') rep('tr', function(){ move(1); });

+        else if(act==='soft') softHeld=true;

+        else if(act==='rotate') rotate(1);

+        else if(act==='hard') hardDrop();

+        else if(act==='hold') holdPiece();

+        if(act==='left') stopRep('tl');

+        else if(act==='right') stopRep('tr');

+        else if(act==='soft') softHeld=false;

+      btn.addEventListener('pointerup', end);

+      btn.addEventListener('pointercancel', end);

+      btn.addEventListener('pointerleave', end);

+  if(document.hidden && running && !paused && !over) togglePause();

+      if(clearing.t>=CLEAR_TIME) finishClear();

+showOverlay('NEON DROP', 'Fill a row to clear it.\nDon’t let the stack reach the top.', 'PRESS START');


<total_tokens>14919271 tokens left</total_tokens>


250

251
252
Neon Drop needs JavaScript enabled.

253
254

Neon Drop needs JavaScript enabled.


<total_tokens>14918973 tokens left</total_tokens>


  .crt::after{
    content:""; position:absolute; inset:0; border-radius:16px; pointer-events:none; z-index:5;
    background:repeating-linear-gradient(0deg, rgba(0,0,0,.17) 0 1px, transparent 1px 3px);
    mix-blend-mode:multiply;
  }
  .crt::before{
    content:""; position:absolute; inset:0; border-radius:16px; pointer-events:none; z-index:6;
    background:radial-gradient(125% 95% at 50% 34%, transparent 55%, rgba(3,1,8,.6) 100%);
  }

  .crt::after{
    content:""; position:absolute; inset:0; border-radius:16px; pointer-events:none; z-index:5;
    background:repeating-linear-gradient(0deg, rgba(0,0,0,.13) 0 1px, transparent 1px 3px);
    mix-blend-mode:multiply;
  }
  .crt::before{
    content:""; position:absolute; inset:0; border-radius:16px; pointer-events:none; z-index:6;
    background:radial-gradient(135% 100% at 50% 32%, transparent 70%, rgba(3,1,8,.34) 100%);
  }


  .crt::after{
    content:""; position:absolute; inset:0; border-radius:16px; pointer-events:none; z-index:5;
    background:repeating-linear-gradient(0deg, rgba(0,0,0,.17) 0 1px, transparent 1px 3px);
    mix-blend-mode:multiply;
  }
  .crt::before{
    content:""; position:absolute; inset:0; border-radius:16px; pointer-events:none; z-index:6;
    background:radial-gradient(125% 95% at 50% 34%, transparent 55%, rgba(3,1,8,.6) 100%);
  }

  .crt::after{
    content:""; position:absolute; inset:0; border-radius:16px; pointer-events:none; z-index:5;
    background:repeating-linear-gradient(0deg, rgba(0,0,0,.13) 0 1px, transparent 1px 3px);
    mix-blend-mode:multiply;
  }
  .crt::before{
    content:""; position:absolute; inset:0; border-radius:16px; pointer-events:none; z-index:6;
    background:radial-gradient(135% 100% at 50% 32%, transparent 70%, rgba(3,1,8,.34) 100%);
  }

-    background:repeating-linear-gradient(0deg, rgba(0,0,0,.17) 0 1px, transparent 1px 3px);

+    background:repeating-linear-gradient(0deg, rgba(0,0,0,.13) 0 1px, transparent 1px 3px);

-    background:radial-gradient(125% 95% at 50% 34%, transparent 55%, rgba(3,1,8,.6) 100%);

+    background:radial-gradient(135% 100% at 50% 32%, transparent 70%, rgba(3,1,8,.34) 100%);

.marquee{ text-align:center; margin-bottom:clamp(12px, 2.6vw, 20px); }


<total_tokens>14909757 tokens left</total_tokens>


330	const holdCv=document.getElementById('hold');
331	const hctx=holdCv.getContext('2d');
332	const nextCv=document.getElementById('next');
333	const nctx=nextCv.getContext('2d');
334	const overlay=document.getElementById('overlay');
335	const ovTitle=document.getElementById('ov-title');
336	const ovText=document.getElementById('ov-text');
337	const startBtn=document.getElementById('startBtn');
338	const elScore=document.getElementById('score');
339	const elLines=document.getElementById('lines');
340	const elLevel=document.getElementById('level');
341	const elBest=document.getElementById('best');
342
343	const DPR=Math.min(2, window.devicePixelRatio||1);
344	function setup(cv,w,h){ cv.width=w*DPR; cv.height=h*DPR; cv.getContext('2d').setTransform(DPR,0,0,DPR,0,0); }
345	setup(board,300,600); setup(holdCv,120,120); setup(nextCv,120,330);
346
347	const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
348
349	let grid=null, current=null, heldPiece=null, canHold=true, bag=[], queue=[];
350	let score=0, lines=0, level=1, combo=-1, best=0;
351	let gravityInterval=1000, gravityAcc=0, lockTimer=0, lockResets=0, grounded=false;
352	let running=false, paused=false, over=false, clearing=null, last=0, softHeld=false;
353	const particles=[];
354
355	try{ best=parseInt(localStorage.getItem('neondrop.best'),10)||0; }catch(e){}
356	elBest.textContent=best;
357	function collides(p, cells){
358	  const cs = cells || PIECES[p.type][p.rot];
359	  for(let i=0;i<cs.length;i++){
360	    const x=p.x+cs[i][0], y=p.y+cs[i][1];
361	    if(x<0 || x>=COLS || y>=ROWS) return true;
362	    if(y>=0 && grid[y][x]) return true;
363	  }
364	  return false;
365	}
366	function nextFromBag(){
367	  if(bag.length===0){
368	    bag=['I','O','T','S','Z','J','L'];
369	    for(let i=bag.length-1;i>0;i--){ const j=Math.random()*(i+1)|0; const t=bag[i]; bag[i]=bag[j]; bag[j]=t; }
370	  }
371	  return bag.pop();
372	}
373	function ensureQueue(){ while(queue.length<7) queue.push(nextFromBag()); }
374
375	function newPiece(type){
376	  current={type:type, rot:0, x:3, y:0};
377	  gravityAcc=0; lockTimer=0; lockResets=0;
378	  grounded=collides({type:type,rot:0,x:3,y:1});
379	  if(collides(current)) endGame();
380	}
381	function nextPiece(){ ensureQueue(); newPiece(queue.shift()); ensureQueue(); }
382
383	function afterMove(){
384	  grounded=collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});
385	  if(grounded){ if(lockResets<15){ lockTimer=0; lockResets++; } }
386	  else { lockTimer=0; }
387	}
388	function move(dx){
389	  if(!current||paused||over||clearing) return;
390	  if(!collides({type:current.type,rot:current.rot,x:current.x+dx,y:current.y})){
391	    current.x+=dx; afterMove();
392	  }
393	}
394	function softStep(){
395	  return !collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});
396	}
397	function rotate(dir){
398	  if(!current||paused||over||clearing||current.type==='O') return;
399	  const from=current.rot, to=(from+dir+4)%4;
400	  const table=current.type==='I'?KICK_I:KICK_JLSTZ;
401	  const kicks=table[''+from+to];
402	  const cells=PIECES[current.type][to];
403	  for(let i=0;i<kicks.length;i++){
404	    const nx=current.x+kicks[i][0], ny=current.y+kicks[i][1];
405	    if(!collides({type:current.type,rot:to,x:nx,y:ny}, cells)){
406	      current.x=nx; current.y=ny; current.rot=to; afterMove(); return;
407	    }
408	  }
409	}
410	function ghostY(){
411	  let y=current.y;
412	  while(!collides({type:current.type,rot:current.rot,x:current.x,y:y+1})) y++;
413	  return y;
414	}
415	function hardDrop(){
416	  if(!current||paused||over||clearing) return;
417	  let d=0;
418	  while(!collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1})){ current.y++; d++; }
419	  score+=d*2;
420	  lockPiece();
421	}
422	function holdPiece(){
423	  if(!current||!canHold||paused||over||clearing) return;
424	  const cur=current.type;
425	  if(heldPiece){ const h=heldPiece; heldPiece=cur; newPiece(h); }
426	  else { heldPiece=cur; nextPiece(); }
427	  canHold=false;
428	}
429	function lockPiece(){
430	  const cs=PIECES[current.type][current.rot];
431	  let topOut=true;
432	  for(let i=0;i<cs.length;i++){
433	    const x=current.x+cs[i][0], y=current.y+cs[i][1];
434	    if(y>=0){ grid[y][x]=current.type; topOut=false; }
435	  }
436	  lockResets=0; lockTimer=0; grounded=false; canHold=true;
437	  const full=[];
438	  for(let y=0;y<ROWS;y++){
439	    let f=true;
440	    for(let x=0;x<COLS;x++){ if(!grid[y][x]){ f=false; break; } }
441	    if(f) full.push(y);
442	  }
443	  if(full.length){
444	    resolveClear(full);
445	  } else {
446	    combo=-1;
447	    if(topOut){ endGame(); return; }
448	    nextPiece();
449	  }
450	  updateHUD();
451	}
452	function resolveClear(rows){
453	  clearing={rows:rows, t:0};
454	  const n=rows.length;
455	  const base=[0,100,300,500,800][n];
456	  combo++;
457	  let gain=base*level;
458	  if(combo>0) gain+=50*combo*level;
459	  score+=gain;
460	  lines+=n;
461	  const nl=Math.floor(lines/10)+1;
462	  if(nl>level){ level=nl; recalcGravity(); }
463	  if(!reduceMotion){
464	    for(let r=0;r<rows.length;r++){
465	      const y=rows[r];
466	      for(let x=0;x<COLS;x++) spawnParticles(x,y,grid[y][x]);
467	    }
468	  }
469	  updateHUD();
470	}
471	function finishClear(){
472	  const set={};
473	  for(let i=0;i<clearing.rows.length;i++) set[clearing.rows[i]]=1;
474	  const kept=[];
475	  for(let y=0;y<ROWS;y++){ if(!set[y]) kept.push(grid[y]); }
476	  while(kept.length<ROWS) kept.unshift(new Array(COLS).fill(0));
477	  grid=kept;
478	  clearing=null;
479	  nextPiece();
480	}
481	function recalcGravity(){
482	  gravityInterval=Math.max(15, Math.pow(0.8-((level-1)*0.007), level-1)*1000);
483	}
484	function tick(dtMs){
485	  if(!current) return;
486	  const step = softHeld ? Math.min(gravityInterval, 45) : gravityInterval;
487	  gravityAcc+=dtMs;
488	  let moved=false;
489	  while(gravityAcc>=step){

const holdCv=document.getElementById('hold');
const hctx=holdCv.getContext('2d');
const nextCv=document.getElementById('next');
const nctx=nextCv.getContext('2d');
const overlay=document.getElementById('overlay');
const ovTitle=document.getElementById('ov-title');
const ovText=document.getElementById('ov-text');
const startBtn=document.getElementById('startBtn');
const elScore=document.getElementById('score');
const elLines=document.getElementById('lines');
const elLevel=document.getElementById('level');
const elBest=document.getElementById('best');

const DPR=Math.min(2, window.devicePixelRatio||1);
function setup(cv,w,h){ cv.width=w*DPR; cv.height=h*DPR; cv.getContext('2d').setTransform(DPR,0,0,DPR,0,0); }
setup(board,300,600); setup(holdCv,120,120); setup(nextCv,120,330);

const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

let grid=null, current=null, heldPiece=null, canHold=true, bag=[], queue=[];
let score=0, lines=0, level=1, combo=-1, best=0;
let gravityInterval=1000, gravityAcc=0, lockTimer=0, lockResets=0, grounded=false;
let running=false, paused=false, over=false, clearing=null, last=0, softHeld=false;
const particles=[];

try{ best=parseInt(localStorage.getItem('neondrop.best'),10)||0; }catch(e){}
elBest.textContent=best;
function collides(p, cells){
  const cs = cells || PIECES[p.type][p.rot];
  for(let i=0;i<cs.length;i++){
    const x=p.x+cs[i][0], y=p.y+cs[i][1];
    if(x<0 || x>=COLS || y>=ROWS) return true;
    if(y>=0 && grid[y][x]) return true;
  }
  return false;
}
function nextFromBag(){
  if(bag.length===0){
    bag=['I','O','T','S','Z','J','L'];
    for(let i=bag.length-1;i>0;i--){ const j=Math.random()*(i+1)|0; const t=bag[i]; bag[i]=bag[j]; bag[j]=t; }
  }
  return bag.pop();
}
function ensureQueue(){ while(queue.length<7) queue.push(nextFromBag()); }

function newPiece(type){
  current={type:type, rot:0, x:3, y:0};
  gravityAcc=0; lockTimer=0; lockResets=0;
  grounded=collides({type:type,rot:0,x:3,y:1});
  if(collides(current)) endGame();
}
function nextPiece(){ ensureQueue(); newPiece(queue.shift()); ensureQueue(); }

function afterMove(){
  grounded=collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});
  if(grounded){ if(lockResets<15){ lockTimer=0; lockResets++; } }
  else { lockTimer=0; }
}
function move(dx){
  if(!current||paused||over||clearing) return;
  if(!collides({type:current.type,rot:current.rot,x:current.x+dx,y:current.y})){
    current.x+=dx; afterMove();
  }
}
function softStep(){
  return !collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});
}
function rotate(dir){
  if(!current||paused||over||clearing||current.type==='O') return;
  const from=current.rot, to=(from+dir+4)%4;
  const table=current.type==='I'?KICK_I:KICK_JLSTZ;
  const kicks=table[''+from+to];
  const cells=PIECES[current.type][to];
  for(let i=0;i<kicks.length;i++){
    const nx=current.x+kicks[i][0], ny=current.y+kicks[i][1];
    if(!collides({type:current.type,rot:to,x:nx,y:ny}, cells)){
      current.x=nx; current.y=ny; current.rot=to; afterMove(); return;
    }
  }
}
function ghostY(){
  let y=current.y;
  while(!collides({type:current.type,rot:current.rot,x:current.x,y:y+1})) y++;
  return y;
}
function hardDrop(){
  if(!current||paused||over||clearing) return;
  let d=0;
  while(!collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1})){ current.y++; d++; }
  score+=d*2;
  lockPiece();
}
function holdPiece(){
  if(!current||!canHold||paused||over||clearing) return;
  const cur=current.type;
  if(heldPiece){ const h=heldPiece; heldPiece=cur; newPiece(h); }
  else { heldPiece=cur; nextPiece(); }
  canHold=false;
}
function lockPiece(){
  const cs=PIECES[current.type][current.rot];
  let topOut=true;
  for(let i=0;i<cs.length;i++){
    const x=current.x+cs[i][0], y=current.y+cs[i][1];
    if(y>=0){ grid[y][x]=current.type; topOut=false; }
  }
  lockResets=0; lockTimer=0; grounded=false; canHold=true;
  const full=[];
  for(let y=0;y<ROWS;y++){
    let f=true;
    for(let x=0;x<COLS;x++){ if(!grid[y][x]){ f=false; break; } }
    if(f) full.push(y);
  }
  if(full.length){
    resolveClear(full);
  } else {
    combo=-1;
    if(topOut){ endGame(); return; }
    nextPiece();
  }
  updateHUD();
}
function resolveClear(rows){
  clearing={rows:rows, t:0};
  const n=rows.length;
  const base=[0,100,300,500,800][n];
  combo++;
  let gain=base*level;
  if(combo>0) gain+=50*combo*level;
  score+=gain;
  lines+=n;
  const nl=Math.floor(lines/10)+1;
  if(nl>level){ level=nl; recalcGravity(); }
  if(!reduceMotion){
    for(let r=0;r<rows.length;r++){
      const y=rows[r];
      for(let x=0;x<COLS;x++) spawnParticles(x,y,grid[y][x]);
    }
  }
  updateHUD();
}
function finishClear(){
  const set={};
  for(let i=0;i<clearing.rows.length;i++) set[clearing.rows[i]]=1;
  const kept=[];
  for(let y=0;y<ROWS;y++){ if(!set[y]) kept.push(grid[y]); }
  while(kept.length<ROWS) kept.unshift(new Array(COLS).fill(0));
  grid=kept;
  clearing=null;
  nextPiece();
}
function recalcGravity(){
  gravityInterval=Math.max(15, Math.pow(0.8-((level-1)*0.007), level-1)*1000);
}
function tick(dtMs){
  if(!current) return;
  const step = softHeld ? Math.min(gravityInterval, 45) : gravityInterval;
  gravityAcc+=dtMs;
  let moved=false;
  while(gravityAcc>=step){


<total_tokens>14909431 tokens left</total_tokens>


489	  while(gravityAcc>=step){
490	    gravityAcc-=step;
491	    if(softStep()){ current.y++; moved=true; if(softHeld) score+=1; }
492	    else { grounded=true; gravityAcc=0; break; }
493	  }
494	  if(moved){
495	    lockTimer=0;
496	    grounded=collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});
497	    if(softHeld) updateHUD();
498	  }
499	  if(grounded){
500	    lockTimer+=dtMs;
501	    if(lockTimer>=LOCK_DELAY) lockPiece();
502	  }
503	}
504	function endGame(){
505	  over=true; running=false;
506	  if(score>best){
507	    best=score;
508	    try{ localStorage.setItem('neondrop.best', String(best)); }catch(e){}
509	    elBest.textContent=best;
510	  }
511	  showOverlay('GAME OVER', 'Score '+score+'  ·  Lines '+lines+'\nLevel '+level, 'PLAY AGAIN');
512	}
513	function updateHUD(){
514	  elScore.textContent=score;
515	  elLines.textContent=lines;
516	  elLevel.textContent=level;
517	}
518	function rr(ctx,x,y,w,h,r){
519	  if(w<0)w=0; if(h<0)h=0;
520	  const k=Math.min(r, w/2, h/2);
521	  ctx.beginPath();
522	  ctx.moveTo(x+k,y);
523	  ctx.arcTo(x+w,y,x+w,y+h,k);
524	  ctx.arcTo(x+w,y+h,x,y+h,k);
525	  ctx.arcTo(x,y+h,x,y,k);
526	  ctx.arcTo(x,y,x+w,y,k);
527	  ctx.closePath();
528	}
529	function paintCell(ctx,px,py,s,color,glow){
530	  ctx.save();
531	  if(glow){ ctx.shadowColor=color; ctx.shadowBlur=13; }
532	  ctx.fillStyle=color;
533	  rr(ctx, px+2, py+2, s-4, s-4, 4); ctx.fill();
534	  ctx.shadowBlur=0;
535	  ctx.globalAlpha=0.32; ctx.fillStyle='#ffffff';
536	  rr(ctx, px+4, py+4, s-8, (s-8)*0.4, 3); ctx.fill();
537	  ctx.globalAlpha=0.22; ctx.fillStyle='#000000';
538	  rr(ctx, px+4, py+(s-4)-(s-8)*0.26, s-8, (s-8)*0.26, 3); ctx.fill();
539	  ctx.restore();
540	}
541	function drawPieceInBox(ctx,bx,by,bw,bh,type,dim){
542	  const cells=PIECES[type][0];
543	  let a=9,b=-9,c=9,d=-9;
544	  for(let i=0;i<cells.length;i++){
545	    const cx=cells[i][0], cy=cells[i][1];
546	    if(cx<a)a=cx; if(cx>b)b=cx; if(cy<c)c=cy; if(cy>d)d=cy;
547	  }
548	  const w=b-a+1, h=d-c+1;
549	  const s=Math.min(bw/(w+1), bh/(h+1), 20);
550	  const ox=bx+(bw-w*s)/2-a*s, oy=by+(bh-h*s)/2-c*s;
551	  const col=COLORS[type];
552	  for(let i=0;i<cells.length;i++){
553	    const px=ox+cells[i][0]*s, py=oy+cells[i][1]*s;
554	    ctx.save();
555	    ctx.globalAlpha=dim?0.4:1;
556	    ctx.shadowColor=col; ctx.shadowBlur=8;
557	    ctx.fillStyle=col;
558	    rr(ctx, px+1.5, py+1.5, s-3, s-3, 3); ctx.fill();
559	    ctx.restore();
560	  }
561	}
562	function spawnParticles(x,y,type){
563	  const col=COLORS[type]||'#ffffff';
564	  for(let i=0;i<3;i++){
565	    if(particles.length>240) return;
566	    particles.push({
567	      x:x*CELL+CELL/2+(Math.random()-0.5)*CELL,
568	      y:y*CELL+CELL/2+(Math.random()-0.5)*CELL,
569	      vx:(Math.random()-0.5)*0.22,
570	      vy:(Math.random()-0.9)*0.28,
571	      life:1, color:col
572	    });
573	  }
574	}
575	function updateParticles(dtMs){
576	  for(let i=particles.length-1;i>=0;i--){
577	    const p=particles[i];
578	    p.x+=p.vx*dtMs; p.y+=p.vy*dtMs; p.vy+=0.0018*dtMs;
579	    p.life-=dtMs/700;
580	    if(p.life<=0) particles.splice(i,1);
581	  }
582	}
583	function render(){
584	  bctx.clearRect(0,0,300,600);
585	  bctx.fillStyle='rgba(125,90,190,0.05)';
586	  bctx.fillRect(0,0,300,600);
587	  bctx.strokeStyle='rgba(130,100,200,0.10)';
588	  bctx.lineWidth=1;
589	  for(let x=1;x<COLS;x++){ bctx.beginPath(); bctx.moveTo(x*CELL+0.5,0); bctx.lineTo(x*CELL+0.5,600); bctx.stroke(); }
590	  for(let y=1;y<ROWS;y++){ bctx.beginPath(); bctx.moveTo(0,y*CELL+0.5); bctx.lineTo(300,y*CELL+0.5); bctx.stroke(); }
591
592	  if(grid){
593	    for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++){
594	      const v=grid[y][x]; if(!v) continue;
595	      if(clearing && clearing.rows.indexOf(y)>=0){
596	        const k=Math.max(0, 1-clearing.t/CLEAR_TIME);
597	        bctx.save();
598	        bctx.globalAlpha=0.35+0.65*k;
599	        bctx.shadowColor='#ffffff'; bctx.shadowBlur=24*k;
600	        bctx.fillStyle='#ffffff';
601	        rr(bctx, x*CELL+2, y*CELL+2, CELL-4, CELL-4, 4); bctx.fill();
602	        bctx.restore();
603	      } else {
604	        paintCell(bctx, x*CELL, y*CELL, CELL, COLORS[v], false);
605	      }
606	    }
607	  }
608

  while(gravityAcc>=step){
    gravityAcc-=step;
    if(softStep()){ current.y++; moved=true; if(softHeld) score+=1; }
    else { grounded=true; gravityAcc=0; break; }
  }
  if(moved){
    lockTimer=0;
    grounded=collides({type:current.type,rot:current.rot,x:current.x,y:current.y+1});
    if(softHeld) updateHUD();
  }
  if(grounded){
    lockTimer+=dtMs;
    if(lockTimer>=LOCK_DELAY) lockPiece();
  }
}
function endGame(){
  over=true; running=false;
  if(score>best){
    best=score;
    try{ localStorage.setItem('neondrop.best', String(best)); }catch(e){}
    elBest.textContent=best;
  }
  showOverlay('GAME OVER', 'Score '+score+'  ·  Lines '+lines+'\nLevel '+level, 'PLAY AGAIN');
}
function updateHUD(){
  elScore.textContent=score;
  elLines.textContent=lines;
  elLevel.textContent=level;
}
function rr(ctx,x,y,w,h,r){
  if(w<0)w=0; if(h<0)h=0;
  const k=Math.min(r, w/2, h/2);
  ctx.beginPath();
  ctx.moveTo(x+k,y);
  ctx.arcTo(x+w,y,x+w,y+h,k);
  ctx.arcTo(x+w,y+h,x,y+h,k);
  ctx.arcTo(x,y+h,x,y,k);
  ctx.arcTo(x,y,x+w,y,k);
  ctx.closePath();
}
function paintCell(ctx,px,py,s,color,glow){
  ctx.save();
  if(glow){ ctx.shadowColor=color; ctx.shadowBlur=13; }
  ctx.fillStyle=color;
  rr(ctx, px+2, py+2, s-4, s-4, 4); ctx.fill();
  ctx.shadowBlur=0;
  ctx.globalAlpha=0.32; ctx.fillStyle='#ffffff';
  rr(ctx, px+4, py+4, s-8, (s-8)*0.4, 3); ctx.fill();
  ctx.globalAlpha=0.22; ctx.fillStyle='#000000';
  rr(ctx, px+4, py+(s-4)-(s-8)*0.26, s-8, (s-8)*0.26, 3); ctx.fill();
  ctx.restore();
}
function drawPieceInBox(ctx,bx,by,bw,bh,type,dim){
  const cells=PIECES[type][0];
  let a=9,b=-9,c=9,d=-9;
  for(let i=0;i<cells.length;i++){
    const cx=cells[i][0], cy=cells[i][1];
    if(cx<a)a=cx; if(cx>b)b=cx; if(cy<c)c=cy; if(cy>d)d=cy;
  }
  const w=b-a+1, h=d-c+1;
  const s=Math.min(bw/(w+1), bh/(h+1), 20);
  const ox=bx+(bw-w*s)/2-a*s, oy=by+(bh-h*s)/2-c*s;
  const col=COLORS[type];
  for(let i=0;i<cells.length;i++){
    const px=ox+cells[i][0]*s, py=oy+cells[i][1]*s;
    ctx.save();
    ctx.globalAlpha=dim?0.4:1;
    ctx.shadowColor=col; ctx.shadowBlur=8;
    ctx.fillStyle=col;
    rr(ctx, px+1.5, py+1.5, s-3, s-3, 3); ctx.fill();
    ctx.restore();
  }
}
function spawnParticles(x,y,type){
  const col=COLORS[type]||'#ffffff';
  for(let i=0;i<3;i++){
    if(particles.length>240) return;
    particles.push({
      x:x*CELL+CELL/2+(Math.random()-0.5)*CELL,
      y:y*CELL+CELL/2+(Math.random()-0.5)*CELL,
      vx:(Math.random()-0.5)*0.22,
      vy:(Math.random()-0.9)*0.28,
      life:1, color:col
    });
  }
}
function updateParticles(dtMs){
  for(let i=particles.length-1;i>=0;i--){
    const p=particles[i];
    p.x+=p.vx*dtMs; p.y+=p.vy*dtMs; p.vy+=0.0018*dtMs;
    p.life-=dtMs/700;
    if(p.life<=0) particles.splice(i,1);
  }
}
function render(){
  bctx.clearRect(0,0,300,600);
  bctx.fillStyle='rgba(125,90,190,0.05)';
  bctx.fillRect(0,0,300,600);
  bctx.strokeStyle='rgba(130,100,200,0.10)';
  bctx.lineWidth=1;
  for(let x=1;x<COLS;x++){ bctx.beginPath(); bctx.moveTo(x*CELL+0.5,0); bctx.lineTo(x*CELL+0.5,600); bctx.stroke(); }
  for(let y=1;y<ROWS;y++){ bctx.beginPath(); bctx.moveTo(0,y*CELL+0.5); bctx.lineTo(300,y*CELL+0.5); bctx.stroke(); }

  if(grid){
    for(let y=0;y<ROWS;y++) for(let x=0;x<COLS;x++){
      const v=grid[y][x]; if(!v) continue;
      if(clearing && clearing.rows.indexOf(y)>=0){
        const k=Math.max(0, 1-clearing.t/CLEAR_TIME);
        bctx.save();
        bctx.globalAlpha=0.35+0.65*k;
        bctx.shadowColor='#ffffff'; bctx.shadowBlur=24*k;
        bctx.fillStyle='#ffffff';
        rr(bctx, x*CELL+2, y*CELL+2, CELL-4, CELL-4, 4); bctx.fill();
        bctx.restore();
      } else {
        paintCell(bctx, x*CELL, y*CELL, CELL, COLORS[v], false);
      }
    }
  }


<total_tokens>14905558 tokens left</total_tokens>


608
609	  if(current && !clearing){
610	    const gy=ghostY(), cells=PIECES[current.type][current.rot], col=COLORS[current.type];
611	    bctx.save();
612	    bctx.globalAlpha=0.30; bctx.strokeStyle=col; bctx.lineWidth=2;
613	    for(let i=0;i<cells.length;i++){
614	      const yy=gy+cells[i][1];
615	      if(yy<0) continue;
616	      rr(bctx, (current.x+cells[i][0])*CELL+3, yy*CELL+3, CELL-6, CELL-6, 3); bctx.stroke();
617	    }
618	    bctx.restore();
619	    for(let i=0;i<cells.length;i++){
620	      const yy=current.y+cells[i][1];
621	      if(yy<0) continue;
622	      paintCell(bctx, (current.x+cells[i][0])*CELL, yy*CELL, CELL, col, true);
623	    }
624	  }
625
626	  for(let i=0;i<particles.length;i++){
627	    const p=particles[i];
628	    bctx.globalAlpha=Math.max(0, p.life);
629	    bctx.fillStyle=p.color;
630	    bctx.fillRect(p.x-2, p.y-2, 4, 4);
631	  }
632	  bctx.globalAlpha=1;
633
634	  hctx.clearRect(0,0,120,120);
635	  if(heldPiece) drawPieceInBox(hctx, 0, 0, 120, 120, heldPiece, !canHold);
636	  nctx.clearRect(0,0,120,330);
637	  const q=queue.slice(0,5);
638	  for(let i=0;i<q.length;i++) drawPieceInBox(nctx, 0, i*66, 120, 66, q[i], false);
639	}
640	function showOverlay(title,text,btn){
641	  ovTitle.textContent=title;
642	  if(text){ ovText.innerHTML=text.replace(/\n/g,'<br>'); ovText.style.display=''; }
643	  else { ovText.textContent=''; ovText.style.display='none'; }
644	  startBtn.textContent=btn;
645	  overlay.classList.remove('hidden');
646	}
647	function hideOverlay(){ overlay.classList.add('hidden'); }
648
649	function startGame(){
650	  grid=[];
651	  for(let y=0;y<ROWS;y++) grid.push(new Array(COLS).fill(0));
652	  bag=[]; queue=[]; ensureQueue();
653	  score=0; lines=0; level=1; combo=-1;
654	  heldPiece=null; canHold=true;
655	  particles.length=0; clearing=null;
656	  gravityAcc=0; lockTimer=0; lockResets=0; grounded=false;
657	  over=false; paused=false;
658	  recalcGravity(); updateHUD();
659	  nextPiece();
660	  running=true; last=0;
661	  hideOverlay();
662	}
663	function togglePause(){
664	  if(over||!running) return;
665	  paused=!paused;
666	  if(paused) showOverlay('PAUSED', '', 'RESUME');
667	  else hideOverlay();
668	}
669
670	startBtn.addEventListener('click', function(){
671	  if(paused) togglePause();
672	  else startGame();
673	});
674
675	const repeats={};
676	function rep(id,fn){ fn(); repeats[id]={fn:fn, acc:0, go:false}; }
677	function stopRep(id){ delete repeats[id]; }
678	function updateRepeats(dtMs){
679	  for(const id in repeats){
680	    const r=repeats[id];
681	    r.acc+=dtMs;
682	    if(!r.go){ if(r.acc>=150){ r.go=true; r.acc-=150; r.fn(); } }
683	    else { while(r.acc>=40){ r.acc-=40; r.fn(); } }
684	  }
685	}
686
687	window.addEventListener('keydown', function(e){
688	  const k=e.key.toLowerCase();
689	  if(k==='arrowleft'||k==='arrowright'||k==='arrowup'||k==='arrowdown'||k===' ') e.preventDefault();
690	  if(!running && (k===' '||k==='enter')){ if(!paused) startGame(); return; }
691	  if(k==='p'){ togglePause(); return; }
692	  if(paused||over||!running||e.repeat) return;
693	  switch(k){
694	    case 'arrowleft':  rep('left', function(){ move(-1); }); break;
695	    case 'arrowright': rep('right', function(){ move(1); }); break;
696	    case 'arrowdown':  softHeld=true; break;
697	    case 'arrowup': case 'x': rotate(1); break;
698	    case 'z': rotate(3); break;
699	    case ' ': hardDrop(); break;
700	    case 'c': case 'shift': holdPiece(); break;
701	  }
702	});
703	window.addEventListener('keyup', function(e){
704	  const k=e.key.toLowerCase();
705	  if(k==='arrowleft') stopRep('left');
706	  else if(k==='arrowright') stopRep('right');
707	  else if(k==='arrowdown') softHeld=false;
708	});
709
710	const touch=document.getElementById('touch');
711	if(touch){
712	  touch.addEventListener('contextmenu', function(e){ e.preventDefault(); });
713	  const btns=touch.querySelectorAll('button');
714	  for(let i=0;i<btns.length;i++){
715	    (function(btn){
716	      const act=btn.getAttribute('data-act');
717	      btn.addEventListener('pointerdown', function(e){
718	        e.preventDefault();
719	        if(!running||paused||over) return;
720	        if(act==='left') rep('tl', function(){ move(-1); });
721	        else if(act==='right') rep('tr', function(){ move(1); });
722	        else if(act==='soft') softHeld=true;
723	        else if(act==='rotate') rotate(1);
724	        else if(act==='hard') hardDrop();
725	        else if(act==='hold') holdPiece();
726	      });
727	      const end=function(){
728	        if(act==='left') stopRep('tl');
729	        else if(act==='right') stopRep('tr');
730	        else if(act==='soft') softHeld=false;
731	      };
732	      btn.addEventListener('pointerup', end);
733	      btn.addEventListener('pointercancel', end);
734	      btn.addEventListener('pointerleave', end);
735	    })(btns[i]);
736	  }
737	}
738
739	document.addEventListener('visibilitychange', function(){
740	  if(document.hidden && running && !paused && !over) togglePause();
741	});
742
743	function frame(ts){
744	  requestAnimationFrame(frame);
745	  if(!last) last=ts;
746	  let dt=ts-last; last=ts;
747	  if(dt>100) dt=100;
748	  if(running && !paused){
749	    updateRepeats(dt);
750	    if(clearing){
751	      clearing.t+=dt;
752	      if(clearing.t>=CLEAR_TIME) finishClear();
753	    } else {
754	      tick(dt);
755	    }
756	    updateParticles(dt);
757	  }
758	  render();
759	}
760	requestAnimationFrame(frame);
761
762	showOverlay('NEON DROP', 'Fill a row to clear it.\nDon’t let the stack reach the top.', 'PRESS START');
763	render();
764	})();
765	</script>
766

  if(current && !clearing){
    const gy=ghostY(), cells=PIECES[current.type][current.rot], col=COLORS[current.type];
    bctx.save();
    bctx.globalAlpha=0.30; bctx.strokeStyle=col; bctx.lineWidth=2;
    for(let i=0;i<cells.length;i++){
      const yy=gy+cells[i][1];
      if(yy<0) continue;
      rr(bctx, (current.x+cells[i][0])*CELL+3, yy*CELL+3, CELL-6, CELL-6, 3); bctx.stroke();
    }
    bctx.restore();
    for(let i=0;i<cells.length;i++){
      const yy=current.y+cells[i][1];
      if(yy<0) continue;
      paintCell(bctx, (current.x+cells[i][0])*CELL, yy*CELL, CELL, col, true);
    }
  }

  for(let i=0;i<particles.length;i++){
    const p=particles[i];
    bctx.globalAlpha=Math.max(0, p.life);
    bctx.fillStyle=p.color;
    bctx.fillRect(p.x-2, p.y-2, 4, 4);
  }
  bctx.globalAlpha=1;

  hctx.clearRect(0,0,120,120);
  if(heldPiece) drawPieceInBox(hctx, 0, 0, 120, 120, heldPiece, !canHold);
  nctx.clearRect(0,0,120,330);
  const q=queue.slice(0,5);
  for(let i=0;i<q.length;i++) drawPieceInBox(nctx, 0, i*66, 120, 66, q[i], false);
}
function showOverlay(title,text,btn){
  ovTitle.textContent=title;
  if(text){ ovText.innerHTML=text.replace(/\n/g,'<br>'); ovText.style.display=''; }
  else { ovText.textContent=''; ovText.style.display='none'; }
  startBtn.textContent=btn;
  overlay.classList.remove('hidden');
}
function hideOverlay(){ overlay.classList.add('hidden'); }

function startGame(){
  grid=[];
  for(let y=0;y<ROWS;y++) grid.push(new Array(COLS).fill(0));
  bag=[]; queue=[]; ensureQueue();
  score=0; lines=0; level=1; combo=-1;
  heldPiece=null; canHold=true;
  particles.length=0; clearing=null;
  gravityAcc=0; lockTimer=0; lockResets=0; grounded=false;
  over=false; paused=false;
  recalcGravity(); updateHUD();
  nextPiece();
  running=true; last=0;
  hideOverlay();
}
function togglePause(){
  if(over||!running) return;
  paused=!paused;
  if(paused) showOverlay('PAUSED', '', 'RESUME');
  else hideOverlay();
}

startBtn.addEventListener('click', function(){
  if(paused) togglePause();
  else startGame();
});

const repeats={};
function rep(id,fn){ fn(); repeats[id]={fn:fn, acc:0, go:false}; }
function stopRep(id){ delete repeats[id]; }
function updateRepeats(dtMs){
  for(const id in repeats){
    const r=repeats[id];
    r.acc+=dtMs;
    if(!r.go){ if(r.acc>=150){ r.go=true; r.acc-=150; r.fn(); } }
    else { while(r.acc>=40){ r.acc-=40; r.fn(); } }
  }
}

window.addEventListener('keydown', function(e){
  const k=e.key.toLowerCase();
  if(k==='arrowleft'||k==='arrowright'||k==='arrowup'||k==='arrowdown'||k===' ') e.preventDefault();
  if(!running && (k===' '||k==='enter')){ if(!paused) startGame(); return; }
  if(k==='p'){ togglePause(); return; }
  if(paused||over||!running||e.repeat) return;
  switch(k){
    case 'arrowleft':  rep('left', function(){ move(-1); }); break;
    case 'arrowright': rep('right', function(){ move(1); }); break;
    case 'arrowdown':  softHeld=true; break;
    case 'arrowup': case 'x': rotate(1); break;
    case 'z': rotate(3); break;
    case ' ': hardDrop(); break;
    case 'c': case 'shift': holdPiece(); break;
  }
});
window.addEventListener('keyup', function(e){
  const k=e.key.toLowerCase();
  if(k==='arrowleft') stopRep('left');
  else if(k==='arrowright') stopRep('right');
  else if(k==='arrowdown') softHeld=false;
});

const touch=document.getElementById('touch');
if(touch){
  touch.addEventListener('contextmenu', function(e){ e.preventDefault(); });
  const btns=touch.querySelectorAll('button');
  for(let i=0;i<btns.length;i++){
    (function(btn){
      const act=btn.getAttribute('data-act');
      btn.addEventListener('pointerdown', function(e){
        e.preventDefault();
        if(!running||paused||over) return;
        if(act==='left') rep('tl', function(){ move(-1); });
        else if(act==='right') rep('tr', function(){ move(1); });
        else if(act==='soft') softHeld=true;
        else if(act==='rotate') rotate(1);
        else if(act==='hard') hardDrop();
        else if(act==='hold') holdPiece();
      });
      const end=function(){
        if(act==='left') stopRep('tl');
        else if(act==='right') stopRep('tr');
        else if(act==='soft') softHeld=false;
      };
      btn.addEventListener('pointerup', end);
      btn.addEventListener('pointercancel', end);
      btn.addEventListener('pointerleave', end);
    })(btns[i]);
  }
}

document.addEventListener('visibilitychange', function(){
  if(document.hidden && running && !paused && !over) togglePause();
});

function frame(ts){
  requestAnimationFrame(frame);
  if(!last) last=ts;
  let dt=ts-last; last=ts;
  if(dt>100) dt=100;
  if(running && !paused){
    updateRepeats(dt);
    if(clearing){
      clearing.t+=dt;
      if(clearing.t>=CLEAR_TIME) finishClear();
    } else {
      tick(dt);
    }
    updateParticles(dt);
  }
  render();
}
requestAnimationFrame(frame);

showOverlay('NEON DROP', 'Fill a row to clear it.\nDon’t let the stack reach the top.', 'PRESS START');
render();
})();
</script>


<total_tokens>14902850 tokens left</total_tokens>


function stopRep(id){ delete repeats[id]; }

function stopRep(id){ delete repeats[id]; }
function clearInputs(){ for(const id in repeats) delete repeats[id]; softHeld=false; }
window.addEventListener('blur', clearInputs);


function stopRep(id){ delete repeats[id]; }
function clearInputs(){ for(const id in repeats) delete repeats[id]; softHeld=false; }
window.addEventListener('blur', clearInputs);

function rep(id,fn){ fn(); repeats[id]={fn:fn, acc:0, go:false}; }

+function clearInputs(){ for(const id in repeats) delete repeats[id]; softHeld=false; }


<total_tokens>14897681 tokens left</total_tokens>


  over=false; paused=false;
  recalcGravity(); updateHUD();

  over=false; paused=false;
  clearInputs();
  recalcGravity(); updateHUD();


  over=false; paused=false;
  recalcGravity(); updateHUD();

  over=false; paused=false;
  clearInputs();
  recalcGravity(); updateHUD();

gravityAcc=0; lockTimer=0; lockResets=0; grounded=false;


<total_tokens>14897321 tokens left</total_tokens>


Bash

cd /tmp/claude-1000/-home-tonda/809a6534-5852-498c-9da5-0fcab3335c9a/scratchpad && node --check <(sed -n '/<script>/,/<\/script>/p' neon-drop.html | sed '1d;$d') && echo "SYNTAX OK"

Check JS syntax with node


Error: Exit code 127
/bin/bash: line 1: node: command not found


<total_tokens>14896758 tokens left</total_tokens>


which python3 deno bun 2>/dev/null; ls /usr/bin | grep -iE 'node|deno|bun|python' | head

Look for a JS runtime


/usr/bin/python3
bunzip2
python3
python3.13


<total_tokens>14896564 tokens left</total_tokens>


cd /tmp/claude-1000/-home-tonda/809a6534-5852-498c-9da5-0fcab3335c9a/scratchpad && grep -n '__DATA__\|__CORE__\|__RENDER__\|__INPUT__' neon-drop.html; echo "---"; grep -c '{' neon-drop.html; grep -c '}' neon-drop.html; echo "--- python brace/paren check ---"; python3 - <<'EOF'
import re
s=open('neon-drop.html').read()
i=s.find('<script>'); j=s.rfind('</script>')
js=s[i+8:j]
# crude balance check ignoring strings/comments
depth={'(':0,'{':0,'[':0}
pairs={')':'(',  '}':'{', ']':'['}
stack=[]
in_str=None
esc=False
in_line_c=False
in_block_c=False
prev=''
for idx,ch in enumerate(js):
    if in_line_c:
        if ch=='\n': in_line_c=False
        prev=ch; continue
    if in_block_c:
        if prev=='*' and ch=='/': in_block_c=False
        prev=ch; continue
    if in_str:
        if esc: esc=False
        elif ch=='\\': esc=True
        elif ch==in_str: in_str=None
        prev=ch; continue
    if ch=='/' and prev=='/':
        in_line_c=True; prev=ch; continue
    if ch=='*' and prev=='/':
        in_block_c=True; prev=ch; continue
    if ch in ('"',"'","`"):
        in_str=ch; prev=ch; continue
    if ch in '({[':
        stack.append((ch,idx))
    elif ch in ')}]':
        if not stack or stack[-1][0]!=pairs[ch]:
            print("MISMATCH at",idx, repr(js[max(0,idx-40):idx+10])); break
        stack.pop()
    prev=ch
else:
    print("balanced" if not stack else ("unclosed: "+str(stack[-3:])))
EOF

Check for markers and bracket balance


---
176
178
--- python brace/paren check ---
balanced
Shell cwd was reset to /home/tonda


<total_tokens>14895468 tokens left</total_tokens>


Launching skill: claude-in-chrome


Browser tools are not available in this session: the Claude in Chrome extension is not set up. The user can install or connect it from https://claude.ai/chrome and manage browser tools with /chrome. Continue the task without browser tools (WebFetch and WebSearch cover read-only web content), or ask the user to perform browser steps manually. Do not attempt mcp__claude-in-chrome__* tool calls.


<total_tokens>14895060 tokens left</total_tokens>


Neon Drop


Artifact

Neonová arkádová verze Tetrisu do prohlížeče – 7-bag dílky, hold, ghost piece, hard drop, levely a nejlepší skóre.


Published /tmp/claude-1000/-home-tonda/809a6534-5852-498c-9da5-0fcab3335c9a/scratchpad/neon-drop.html at https://claude.ai/code/artifact/2ef25de4-4bfd-4156-a770-7fad3dad0187

Live subscription: arming in the background — not connected yet, so this is not a watch until `status` shows it connected (you are told if it cannot connect, unless this turn is interrupted first). Once connected, this session is notified when this artifact is republished elsewhere; a comment on it sent to Claude also reaches this session while this artifact's status row says auto-replies armed, and plain comments never notify — read them with action "comments" when asked.

To update: republish the same file path in this conversation (keeps this URL), or pass the URL as `url` from any other conversation — publishing without `url` from a conversation that didn't publish this artifact creates a separate artifact rather than updating this one. Artifacts are private unless shared from the page's share menu. To get back to it later: in the Claude Code terminal, /artifacts lists the artifacts you own or were shared (o opens, c copies the link) and ctrl+] (by default) reopens the most recent artifact from this session; on the web, the gallery at claude.ai/code/artifacts lists them.

Neon Drop


<total_tokens>14894532 tokens left</total_tokens>