From 3d7ab0046f567543e63a3f2a3abf75e988eea94a Mon Sep 17 00:00:00 2001 From: luriasb Date: Sun, 27 Sep 2026 01:59:14 -0600 Subject: [PATCH] first commit --- .agents/skills/spec-impl/SKILL.md | 235 ++++++++++++++++++++++++++++++ .agents/skills/spec/SKILL.md | 164 +++++++++++++++++++++ .agents/skills/spec/template.md | 182 +++++++++++++++++++++++ AGENTS.md | 53 +++++++ README.md | 13 ++ skills-lock.json | 17 +++ src/css/style.css | 78 ++++++++++ src/index.html | 24 +++ src/js/game.js | 199 +++++++++++++++++++++++++ src/js/main.js | 53 +++++++ src/js/maze.js | 62 ++++++++ src/js/render.js | 165 +++++++++++++++++++++ 12 files changed, 1245 insertions(+) create mode 100644 .agents/skills/spec-impl/SKILL.md create mode 100644 .agents/skills/spec/SKILL.md create mode 100644 .agents/skills/spec/template.md create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 skills-lock.json create mode 100644 src/css/style.css create mode 100644 src/index.html create mode 100644 src/js/game.js create mode 100644 src/js/main.js create mode 100644 src/js/maze.js create mode 100644 src/js/render.js diff --git a/.agents/skills/spec-impl/SKILL.md b/.agents/skills/spec-impl/SKILL.md new file mode 100644 index 0000000..d15ae3c --- /dev/null +++ b/.agents/skills/spec-impl/SKILL.md @@ -0,0 +1,235 @@ +--- +name: spec-impl +description: Implements an approved spec. Validates that the state means "Approved" (in any language), creates a git branch named after the spec, switches to it, and starts the implementation step by step with pauses to review diffs. +disable-model-invocation: true +argument-hint: +allowed-tools: Read, Glob, Grep, Edit, Write, AskUserQuestion, Bash(git status:*), Bash(git branch:*), Bash(git checkout:*), Bash(git log:*), Bash(git diff:*), Bash(git stash:*), Bash(cat:*), Bash(ls:*) +--- + +# /spec-impl — Implementer of approved specs + +## Session context + +Current repository state: +!`git status --short` + +Current branch: +!`git branch --show-current` + +Specs available in this folder: +!`ls specs/ 2>/dev/null || echo "The specs/ folder does not exist"` + +Branch-creation config: +!`cat specs/.spec-config.yml 2>/dev/null || echo "AutoCreateBranch: true (default, no config file)"` + +--- + +## Instructions + +Follow these four phases in strict order. **Do not advance to the next phase if the previous one did not complete correctly.** + +--- + +### Phase 1 — Identify the spec + +The received argument is: `$ARGUMENTS` + +If `$ARGUMENTS` is empty: + +- List the files available in `specs/` (you already have them above). +- Ask the user to specify the exact name of the spec. +- Stop and wait for an answer. Do not continue. + +If `$ARGUMENTS` has a value: + +- Look for the file in `specs/`. The user may have written the full name (`01-mvp-arkanoid`), only the number (`01`), or only the slug (`mvp-arkanoid`). Try to find the correct file in any of those cases. +- If you do not find the file, show the available specs and ask the user to correct the name. +- If you do find it, continue to Phase 2. + +--- + +### Phase 2 — Validate the spec's state + +Read the spec file you located in Phase 1 using the Read tool or `cat`. + +In the file's contents, look for the line that contains the spec's state. The header label is typically `**Status:**` (English) or `**Estado:**` (Spanish), but it may use any language. Match by position (status line near the top of the spec) and by the surrounding state machine, not by the exact label. + +**Absolute rule:** You can only continue if the state **means "Approved"** — regardless of the language used. + +Treat any of the following (and their equivalents in other languages) as the **Approved** state and continue: + +- English: `Approved` +- Spanish: `Aprobado` +- Portuguese: `Aprovado` +- French: `Approuvé` +- German: `Genehmigt` +- Italian: `Approvato` +- …or any other language's word that clearly means "approved" + +Anything else (Draft / Borrador, In review / En revisión, Implemented / Implementado, Obsolete / Obsoleto, or any unrecognized value) means **stop** and show the error message below. + +| State category | Examples (any language) | Action | +| ----------------------------------------- | ------------------------------------------------- | -------------------------------------------------------------------------- | +| Approved | `Approved`, `Aprobado`, `Aprovado`, `Approuvé`, … | Continue to Phase 3. | +| Draft | `Draft`, `Borrador`, … | Stop. Show the error message below. | +| In review | `In review`, `En revisión`, … | Stop. Show the error message below. | +| Implemented | `Implemented`, `Implementado`, … | Stop. Show the error message below. | +| Obsolete | `Obsolete`, `Obsoleto`, … | Stop. Show the error message below. | +| State line not found / unrecognized value | — | Stop. The file does not follow the expected format. Tell this to the user. | + +If you are unsure whether a value means "approved", **do not assume**. Stop and ask the user to clarify or to update the spec to the canonical wording. + +**Standard error message when the state does not mean Approved:** + +``` +❌ I cannot implement this spec. + +Current state: [STATE FOUND] +I only work with specs whose state means "Approved" (e.g. `Approved`, `Aprobado`, +or the equivalent in another language). + +To continue you have two options: + 1. If the spec is ready to be implemented, open it and change the state + to "Approved" (or the equivalent term your team uses) manually. + That change is made by the human, not the agent. + 2. If the spec still needs work, use /spec [name] to resume it. +``` + +Do not offer alternatives, do not suggest "I can still start if you want". The block is intentional. + +--- + +### Phase 3 — Create the git branch and switch to it + +Once you have confirmed the state means `Approved`: + +0. **Check the working tree first.** Look at the `git status --short` output in the session context above. If it is **not empty**, stop and show the pending changes, then ask: + + ``` + ⚠️ There are uncommitted changes in the working tree. + Switching branches would carry them over. What do you want to do? + 1. Commit or stash them yourself, then re-run this command (recommended) + 2. Continue anyway — the changes travel to the new branch + ``` + + Wait for the answer. **Do not stash or commit on the user's behalf** unless they explicitly ask for it. If the working tree is clean, skip straight to step 1 without mentioning it. + +1. Derive the branch name from the spec file's full name, without the extension. Format: `spec-NN-slug`. Examples: + + - `01-mvp-arkanoid.md` → branch `spec-01-mvp-arkanoid` + - `02-powerups.md` → branch `spec-02-powerups` + +2. Read the `AutoCreateBranch` flag from the **Branch-creation config** shown in the session context above. + + - If the config file does not exist, the value is missing, or the value is unrecognized → treat it as `true` (the default). + - Only an explicit `false` (in any capitalization) disables automatic branch creation. + + **If `AutoCreateBranch` is `true` (default):** proceed without asking. + + - If the branch **does not exist**: create it with `git checkout -b spec-NN-slug`. + - If it **already exists**: this means previous work is being resumed. Switch to it, read `git log --oneline` on the branch, and tell the user which steps of the plan already look done and which step you propose to resume from. Wait for confirmation on the resume point before implementing anything. + - In both cases: switch to the branch with `git checkout spec-NN-slug` and confirm the change was successful before continuing. + + **If `AutoCreateBranch` is `false`:** ask before touching git. Show: + + ``` + AutoCreateBranch is set to false. + Create and switch to the branch spec-NN-slug? [y/N] + ``` + + - If the user answers **yes**: create/switch to the branch exactly as in the `true` case above. + - If the user answers **no** or leaves it empty: **do not create any branch.** Tell the user you will implement on the current branch (the one shown in the session context above) and ask for explicit confirmation to continue there. Do not improvise — wait for the answer. + +3. Visually confirm to the user the spec is ready and which branch is active: + + ``` + ✅ Ready to implement. + + Spec: specs/NN-slug.md + Branch: spec-NN-slug (active) (← or the current branch, if no new branch was created) + State: Approved (← echo back the actual value found in the spec) + ``` + +4. **Do not start implementing yet.** First show the spec summary to the user so they have it fresh. Extract and show: + - The **objective** (the line after `**Objective:**` / `**Objetivo:**` / equivalent label). + - The **scope** (the `## Scope` / `## Alcance` / equivalent section). + - The **implementation plan** (the section with the numbered steps — `## Implementation plan` / `## Plan de implementación` / equivalent). + - The **acceptance criteria** (the checklist — `## Acceptance criteria` / `## Criterios de aceptación` / equivalent). + +Match section headings by meaning, not by exact wording — the spec may be authored in any language. + +--- + +### Phase 4 — Implement step by step + +After showing the spec summary, tell the user: + +``` +I am going to implement the spec following the implementation plan exactly. +I will pause after each step so you can review the diff. + +Shall we start with Step 1? +``` + +Wait for explicit confirmation ("yes", "go ahead", "go", or equivalent). Do not start without it. + +Once confirmed, follow these rules during the entire implementation: + +**Never commit automatically.** Not per step, not at the end. You write the code and show the diff; committing is the user's decision and the user's command. Only commit if they explicitly ask you to. + +**One rule above all:** implement what the spec says. If something in the spec looks suboptimal to you, mention it as an observation but implement what was agreed. Changes to the spec go into the spec, not into the code by surprise. + +**Work rhythm:** + +- Implement one step of the plan. +- Show a summary of which files you touched and what you did. +- Say: `Step N completed. Could you review the diff and let me know if I continue with Step N+1?` +- Wait for confirmation before continuing. + +**If during the implementation you find an ambiguity** the spec does not resolve: + +- Stop. +- Describe the ambiguity exactly. +- Present two or three concrete options. +- Wait for the user's decision. +- Do not improvise. + +**If the user asks for something that is out of the spec's scope:** + +- Remind them that it is out of this spec's scope. +- Suggest noting it down for the next spec. +- Do not implement it on this branch. + +**When finishing the last step:** + +``` +✅ All steps of the plan are implemented. + +Next step: verify the spec's acceptance criteria one by one. +If they all pass, update the spec's state to "Implemented" (or the equivalent +in your repo's language) and make the final commit before merging this branch. +``` + +--- + +## Summary of expected behavior + +``` +/spec-impl 01-mvp-arkanoid + + Phase 1 → Finds specs/01-mvp-arkanoid.md + Phase 2 → Reads the state → "Approved" (or "Aprobado", etc.) → ✅ continues + Phase 3 → git checkout -b spec-01-mvp-arkanoid → git checkout spec-01-mvp-arkanoid + Shows objective, scope, plan and criteria + Phase 4 → Implements step by step with pauses + Ends by reminding to verify the acceptance criteria + +/spec-impl 02-powerups (state: Draft / Borrador) + + Phase 1 → Finds specs/02-powerups.md + Phase 2 → Reads the state → "Draft" → ❌ stops + Shows the standard error message + Does not create branch, does not touch code +``` + +**Branch creation is controlled by the `AutoCreateBranch` flag** in `specs/.spec-config.yml`. It defaults to `true` (create the branch automatically, as shown above). Set it to `false` to make Phase 3 ask `[y/N]` before creating the branch. diff --git a/.agents/skills/spec/SKILL.md b/.agents/skills/spec/SKILL.md new file mode 100644 index 0000000..6be6c97 --- /dev/null +++ b/.agents/skills/spec/SKILL.md @@ -0,0 +1,164 @@ +--- +name: spec +description: Designs and develops specs following the spec-driven method. Asks clarifying questions before proposing structure, and builds the spec section by section. Use it when starting a large feature, before writing code. +disable-model-invocation: true +argument-hint: 'short feature description or requirement' +allowed-tools: Read, Glob, Grep, Write, AskUserQuestion, Bash(ls:*), Bash(cat:*), Bash(date:*) +--- + +# /spec — Guided spec designer + +## Session context + +Today's date (use this for the spec header, never guess it): +!`date +%F` + +Specs that already exist: +!`ls specs/ 2>/dev/null || echo "The specs/ folder does not exist yet"` + +--- + +This skill helps you produce a useful spec following the spec-driven method. **You don't write code here.** Your job is to help the user clarify what they want to build, ask questions when something is not well-defined enough, and develop the spec section by section until it is ready to be saved into `specs/`. + +## Philosophy + +A spec is not decorative documentation. It is the contract that drives later execution. If the spec is vague, the code will improvise. That is why this flow is **deliberately slow during the definition phase** and **fast during the writing phase**. + +Read `template.md` (in the same directory as this skill) to see the full structure the spec will follow. Lean on it at every step. + +## Command flow + +- Follow the four phases in order. **Never skip Phase 2** — the questions are the whole point. If the user wants to go faster, remind them that the cost of a bad spec gets paid later in code. (Phase 3 does have a fast path once Phase 2 is genuinely complete; see below.) +- Your replies must be in the same language as the initial prompt. E.g.: if the initial prompt is in Spanish, your replies must be in Spanish; if it is in English, your replies must be in English. + +### Phase 1 — Understand the context + +Before asking questions about the feature, make sure you have project context: + +1. Read the project-memory file, if one exists. Try in order and stop at the first hit: `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `README.md`. This adapts the skill to whichever agent is running it (Claude Code, Codex, Gemini CLI, etc.). +2. Look at the `specs/` listing in the session context above to see which specs already exist and how they are numbered. +3. If previous specs exist, read at least the two most recent ones to pick up the project's conventions — including the **language** they are written in and the exact wording they use for states and section headings. A new spec must match the existing ones. + +If the `$ARGUMENTS` argument comes in empty, ask the user for an initial **single-sentence** description of what they want to build. If the description does not fit in one sentence, that is the first signal that the feature is too big — suggest splitting it before continuing. + +### Phase 2 — Clarify through questions + +This is the most important phase of the command. Your job here is to **detect ambiguities and ask**, not to assume. + +Ask questions in blocks of 3 to 5 at a time (not one single question followed by another single question — that is exhausting). After each block, wait for an answer before continuing. + +**Question categories you should always consider:** + +- **Scope:** What is in and what is NOT? Which parts of the feature are deferred to another spec? +- **Data:** What new structures are introduced? How are they named? Where do they live? +- **Integration:** Does this feature depend on previous specs? Does it modify something existing or only add? +- **Persistence:** Is anything saved between sessions? Where? With what versioning? +- **UX and states:** What does it look like when it works? What does it look like when it fails? Are there intermediate states? +- **Risks:** What can break this? What happens in the degraded case? +- **Closed decisions:** Is there any decision the user has already made and does not want to reopen? + +**How to phrase the questions:** + +- Use concrete questions, not open-ended ones. ❌ "How do you imagine persistence?" → ✅ "Is persistence localStorage, IndexedDB, or a JSON file on disk?" +- When you offer options, give 2–4, mark which one is your recommendation and why. +- If your agent exposes a native multiple-choice question tool (in Claude Code: `AskUserQuestion`), use it for these blocks instead of writing the options as prose — the user picks instead of typing. Put your recommendation first and label it. Fall back to a numbered markdown list when no such tool exists. +- If you spot an answer that would open Pandora's box (e.g. "and we also want multiplayer"), point out that it deserves its own spec and ask whether we leave it out of this one's scope. + +**When to stop asking:** + +Stop when you can answer these three questions without assuming anything: + +1. Which files will appear or change? +2. What is the first executable step and what is the last one? +3. How do I verify the feature is finished? + +If you still cannot answer one of them, keep asking. + +### Phase 3 — Write the spec + +Once Phase 2 is closed, decide how to write it: + +**If you already have all the information you need** — meaning you can answer the three Phase 2 questions (which files change, what the first and last executable steps are, how to verify it is finished) **without assuming anything** — then **do not go section by section**. Write the complete spec and jump straight to Phase 4 to save the file. Do not ask for section-by-section confirmation, and do not show a draft for approval first: the user already answered everything in Phase 2, and re-asking is friction. The user reviews the saved file and asks for changes if needed. + +**Only if information is still missing** (the user cut Phase 2 short, an answer was vague, or some section cannot be written without inventing something), develop the sections **one by one**, showing each one and waiting for confirmation before moving to the next. + +In both cases the content follows the same order: + +1. **Header** (state, dependencies, date, one-sentence objective). The one-sentence objective is critical — if it does not fit in one sentence, go back to Phase 2. +2. **Scope** (what is in and what is NOT). The "not in" must be explicit. +3. **Data model** (concrete structures with real names). If the feature introduces no new data, skip this section and say so explicitly. +4. **Implementation plan** (numbered steps, each leaving the system functional). +5. **Acceptance criteria** (boolean checklist, not aspirational). +6. **Decisions taken and discarded** (with brief justification). +7. **Identified risks** (only if applicable — if no relevant risks exist, skip it). + +**After each section (only in the section-by-section mode):** + +- Show it formatted in markdown. +- Ask: "Does this section stay like this or do you want to tweak it?" +- If the user requests changes, apply them and show again. +- Only move to the next section once the user confirms. + +**Common mistakes to avoid:** + +- Generating acceptance criteria that are not verifiable ("that it works well"). +- Putting things into the implementation plan that are not in the scope. +- Assuming file names or structures the user did not confirm. +- Skipping the decisions section — that section is the one with the most long-term value. + +### Phase 4 — Save the spec + +When the content is ready (either because you had everything, or because all sections were confirmed): + +1. Determine the next sequential number from the `specs/` listing in the session context. Take the highest existing number and add one, zero-padded to two digits. If the last one is `02-powerups.md`, this one will be `03-`. If `specs/` is empty or missing, start at `01-`. +2. Generate a short kebab-case slug from the objective (e.g. `levels-and-highscores`). See **Arguments** below for when `$ARGUMENTS` is the slug instead. +3. Use the date from the session context above for the `**Date:**` field. **Never write a date you did not read from there.** +4. Write the file directly at `specs/NN-slug.md` with all the sections. **Do not ask for permission to write it and do not ask whether the file name works** — announce the path in the final confirmation. Only ask if the target file already exists. +5. Mark the state as `Draft` by default (or the equivalent word used by the existing specs in this repo). **Do not mark it as `Approved` automatically** — the user does that once they have re-read it. +6. If the header lists dependencies (`**Depends on:** SPEC 01`), check that each referenced spec actually exists in `specs/`. If one does not, say so instead of writing a dangling reference. +7. **Seed the config file if it does not exist.** Check for `specs/.spec-config.yml`. If it is **missing**, create it with the default content below. If it **already exists, leave it untouched** — never overwrite the user's settings. + + ```yaml + # spec workflow configuration + # + # AutoCreateBranch — controls whether /spec-impl creates the git branch automatically. + # true (default) → /spec-impl creates and switches to spec-NN-slug without asking + # false → /spec-impl asks for [y/N] confirmation before creating the branch + AutoCreateBranch: true + ``` + +8. Confirm to the user: + - Path of the created file. + - Reminder: the spec is in `Draft` state. Change it to `Approved` once you have re-read it. + - If you just created `specs/.spec-config.yml`, mention it exists and that `AutoCreateBranch` defaults to `true` (set it to `false` to control branch creation yourself). + - Next step: once reviewed and approved, run `/spec-impl NN-slug` to implement it. + - **Stop here.** Do not propose implementing the spec, writing code, or taking any further action beyond this confirmation. + +## Hard rules + +- **Never write code during this command.** Only the spec's `.md` file at the end. +- **Never propose implementing the spec after saving it.** Your job ends when the file is written. The user runs `/spec-impl` when they are ready. +- **Never assume decisions the user did not confirm.** If you are missing information, ask — in Phase 2, which is where the questions belong. +- **Do not re-ask in Phase 3 what was already answered in Phase 2.** If the information is complete, write the whole spec and save it. Section-by-section confirmation is the fallback for incomplete information, not the default. +- **If the user wants to speed up and skip Phase 2**, remind them: "Questions now save hours later. Are you sure you want to skip them?". If they insist, respect their decision but record it in the spec's decisions section ("Quick definition without detailed clarification"). +- **If the feature is too big** (does not fit in one sentence, touches more than three areas of the system, requires decisions in four or more domains), propose splitting it into two or more specs before continuing. + +## Tone when asking questions + +Be direct and specific. Do not apologize for asking. Do not use phrases like "if you don't mind..." or "could you maybe...". The user invoked this skill precisely because they want you to ask questions. Use concrete questions, one per line when there are several, and number them so they are easy to answer. + +Example of a well-formed block: + +> Before writing the data model I need to clarify three things: +> +> 1. **Persistence.** localStorage, IndexedDB, or a JSON file on disk? Recommendation: localStorage if the data fits in <5MB and does not need queries. +> 2. **Schema versioning.** What happens when the format changes? Options: (a) version prefix in the key, (b) ignore and rebuild, (c) migrate on load. +> 3. **Privacy.** Is the data sensitive? If yes, is it encrypted? Is it deleted on logout? + +## Arguments + +`$ARGUMENTS` is **the feature description**, not the file name. Treat it as the starting point for Phase 1 and derive the slug from the objective in Phase 4. + +The one exception: if `$ARGUMENTS` is already a single kebab-case token with no spaces (e.g. `/spec levels-and-highscores`), it is ambiguous between a description and a slug — use it as the slug **and** as the seed of the description, without asking for confirmation. + +If they invoked `/spec` without arguments, start by asking for the one-sentence description. diff --git a/.agents/skills/spec/template.md b/.agents/skills/spec/template.md new file mode 100644 index 0000000..26ccaf4 --- /dev/null +++ b/.agents/skills/spec/template.md @@ -0,0 +1,182 @@ +# Template for a useful spec + +This file is the reference the `/spec` skill consults when generating specs. Each section includes its purpose and a minimal example. **It is not text to be copied verbatim** — it is the shape the skill must respect. + +--- + +## Header + +Every spec starts with metadata in a blockquote (no tables, no blocks, simple as shown below) format: + +```markdown +# SPEC NN — Short, descriptive title + +> **Status:** Draft +> **Depends on:** SPEC 01, SPEC 02 +> **Date:** YYYY-MM-DD +> **Objective:** A single sentence. If you need two sentences, the feature is too big. +``` + +**Valid states:** `Draft`, `In review`, `Approved`, `Implemented`, `Obsolete`. + +> The labels above are the English defaults. The skills also accept equivalents in any language (e.g. Spanish `Borrador` / `En revisión` / `Aprobado` / `Implementado` / `Obsoleto`). Pick one set per repo and stay consistent. + +**Objective rule:** one sentence that a human reads in 5 seconds and understands what is going to be built. If it doesn't fit in one sentence, split the feature. + +--- + +## Section 1 — Why this spec exists (optional) + +For specs that take non-obvious decisions or break project patterns, a brief section explaining the **why** of the work. Not the what — the what comes later. + +For simple specs, omit it. + +--- + +## Section 2 — Scope + +Two explicit sub-blocks. **Both are mandatory.** + +```markdown +## Scope + +**In:** + +- Concrete thing one. +- Concrete thing two. + +**Out of scope (for future specs):** + +- Something that could be done but not now. +- Something that came up in the conversation but is not in. +``` + +**Why "out" matters:** it captures the things the user mentioned during the question phase but were decided to be deferred. Without that record, during implementation there will be a temptation to slip them in "while we're at it". + +--- + +## Section 3 — Data model + +The concrete structures that appear or change. Use real code, not abstract pseudocode. + +```markdown +## Data model + +\`\`\`js +// Game state +const state = { +level: 1, +score: 0, +highScores: [/* { score, level, date } */], +}; +\`\`\` + +Conventions: + +- Coordinates: origin top-left. +- Velocities in pixels/frame. +``` + +If the feature introduces no new data, write it explicitly: _"This feature introduces no new data structures. It reuses the model from SPEC 01."_ + +--- + +## Section 4 — Implementation plan + +Numbered steps. Each step must leave the system in a **functional and runnable** state. No "implement half and continue tomorrow". + +```markdown +## Implementation plan + +1. Create file X with an empty skeleton. +2. Implement function A in X. Manual test: run Y, see Z. +3. Wire X to existing module W. +4. ... +``` + +**Rules:** + +- Each step must be commitable on its own. +- If a step requires more than 30–50 lines of code, split it. +- The last step of the plan is **not** "test everything" — that is the acceptance criteria. + +--- + +## Section 5 — Acceptance criteria + +Boolean checklist. Each item can be verified with yes or no. + +```markdown +## Acceptance criteria + +- [ ] The game loads without errors in the console. +- [ ] Breaking a brick adds exactly 10 points. +- [ ] Reloading the page preserves the high-scores. +``` + +**Anti-patterns to avoid:** + +- ❌ "That it works well." → not verifiable. +- ❌ "Good UX." → subjective. +- ❌ "No bugs." → not operational. +- ✅ "Pressing Esc pauses the game and shows the menu." → verifiable, boolean. + +--- + +## Section 6 — Decisions taken and discarded + +The section that has the most value 3 months from now. Capture **what you considered**, not just what you chose. + +```markdown +## Decisions + +- **Yes:** localStorage for persistence. Fits in <5MB and we don't need queries. +- **No:** IndexedDB. Overengineering for this case. +- **Yes:** versioned key (`save:v1`). Lets us migrate the schema later without breaking. +- **No:** cloud sync. Goes in another spec if it ever lands. +``` + +Each decision ideally has a brief reason. Decisions without a reason are the first ones to be questioned later. + +--- + +## Section 7 — Identified risks (optional) + +Only when there are non-obvious risks. Simple table: + +```markdown +## Risks + +| Risk | Mitigation | +| ------------------------------------- | --------------------------------------------------------------------------- | +| localStorage disabled in private mode | Fallback to in-memory object. The game still runs, it just doesn't persist. | +| Future incompatible schema | Key includes `:v1`. Migration documented in `persistence.js`. | +``` + +For small specs or very contained features, omit it. + +--- + +## Final section — What is NOT in (reinforcement) + +Repeat explicitly at the end what **will not** be done in this spec. This repetition is deliberate — the Scope section already says it, but at the end of the document it serves as a reminder when someone reads only the last lines. + +```markdown +## What is **not** in this spec + +- Visual editor (another spec if it ever lands). +- Multiplayer. +- Mobile version. + +Each one of those, if it lands, goes in its own spec. +``` + +--- + +## Global rules about the whole document + +- **One sentence per idea.** If a sentence has two commas and a semicolon, split it. +- **Concrete names.** If you say "the levels module", say `src/levels.js`. If you say "a key", give the exact string. +- **No TODOs.** A TODO in a spec means the decision was not made. Make it or note it as a pending decision with a reason. +- **No long executable code.** The spec describes; the code is written afterwards. Short snippets to illustrate data structures are fine; full functions are not. +- **Standard markdown.** No weird extensions. It must render on GitHub without surprises. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..16a559d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,53 @@ +# AGENTS.md + +Repo de práctica: clon de Pac-Man (Vanilla JS + Canvas) usado para aprender +Spec Driven Development. No hay build, no hay tests, no hay linter. + +## Stack +- HTML + CSS + Vanilla JS, sin dependencias ni package.json. +- Todo se ejecuta desde `src/index.html`. Se puede abrir directamente + en el navegador (`file://`) — no usa módulos ES ni fetch. + +## Cómo correrlo +- Abrir `src/index.html` en un navegador, o servir la carpeta `src/` con + cualquier servidor estático (`python3 -m http.server` desde `src/`). +- Controles: flechas del teclado. Botón Start en el overlay. + +## Estructura de `src/` +- `index.html` — canvas `#game`, overlay `#overlay`, y carga los scripts + en este orden estricto (es importante): + 1. `js/maze.js` — expone `window.MAZE`, `TUNNEL_ROW`, `PACMAN_START`, `GHOST_STARTS` + 2. `js/game.js` — expone `window.createGame`, `window.update`, `window.DIRS` + 3. `js/render.js` — expone `window.draw` + 4. `js/main.js` — bucle, teclado, overlays (lee los globales de los anteriores) +- Los scripts se comunican por `window.*` (no por imports). No convertir + a módulos ES sin refactorizar las 4 capas. +- `MAZE` (en `maze.js`) es la matriz prístina del laberinto (28×31). + `createGame()` la clava con `.map(row => row.slice())` por partida. + No mutar `MAZE` directamente. + +## Convenciones de código +- Los archivos existentes usan comillas simples y espacios dentro de los + paréntesis (`function ( x )`, `'left'`). Mantener ese estilo al editar. +- Los archivos JS sí llevan comentarios explicativos por sección + (a diferencia de la regla global por defecto). No quitarlos al editar + a menos que el usuario lo pida. +- El render dibuja a `TILE = 20` px sobre el canvas de 560×620. + Cambiar `TILE` rompe el layout (CSS `#game-wrap` está dimensionado + a 560×620). + +## Flujo de trabajo: Spec Driven Development +- Este proyecto existe para practicar SDD. Antes de añadir funcionalidad + usar el skill `.agents/skills/spec` (`/spec`) y guardar el resultado + en `specs/`. Para implementar a partir de un spec ya escrito, usar + `.agents/skills/spec-impl` (`/spec-impl`). +- `specs/` aún no existe; crearlo al primer spec. +- El template vive en `.agents/skills/spec/template.md`. + +## Lo que NO hacer +- No añadir `package.json`, bundler, framework, ni dependencias. +- No convertir los ` + + + + + diff --git a/src/js/game.js b/src/js/game.js new file mode 100644 index 0000000..ca3f440 --- /dev/null +++ b/src/js/game.js @@ -0,0 +1,199 @@ +// game.js +// Estado y reglas. Depende de globals de maze.js: MAZE, TUNNEL_ROW, +// PACMAN_START, GHOST_STARTS. + +const DIRS = { + left: { x: -1, y: 0 }, + right: { x: 1, y: 0 }, + up: { x: 0, y: -1 }, + down: { x: 0, y: 1 }, +}; +const OPPOSITE = { left: 'right', right: 'left', up: 'down', down: 'up' }; + +const PACMAN_SPEED = 0.125; // 1/8 celda/frame -> alinea cada 8 frames +const GHOST_SPEED = 0.1; // 1/10 celda/frame + +// Crea una partida nueva. Copia MAZE (pristino) a game.grid para poder comer +// dots sin destruir el original, y reiniciar. +function createGame() { + const grid = MAZE.map( ( row ) => row.slice() ); + // La celda de inicio de Pacman arranca sin dot. + grid[ PACMAN_START.y ][ PACMAN_START.x ] = 0; + + let dots = 0; + for ( const row of grid ) for ( const v of row ) if ( v === 2 ) dots++; + + return { + state: 'start', + score: 0, + lives: 3, + dotsRemaining: dots, + grid, + pacman: { + x: PACMAN_START.x, + y: PACMAN_START.y, + dir: 'left', + nextDir: null, + speed: PACMAN_SPEED, + }, + ghosts: GHOST_STARTS.map( ( g ) => ( { + x: g.x, + y: g.y, + dir: 'up', + speed: GHOST_SPEED, + kind: g.kind, + } ) ), + }; +} + +function aligned( v ) { + return Math.abs( v - Math.round( v ) ) < 1e-3; +} + +// Una celda es muro para el actor dado? +// pacman: bloqueado por pared (1) y puerta (3) +// ghost: bloqueado solo por pared (1) +function isWall( grid, x, y, actor ) { + if ( y < 0 || y >= grid.length ) return true; + if ( x < 0 || x >= grid[ 0 ].length ) return true; + const v = grid[ y ][ x ]; + if ( v === 1 ) return true; + if ( v === 3 && actor === 'pacman' ) return true; + return false; +} + +// Puede el actor avanzar desde (x,y) en la direccion dir? +function canMove( grid, x, y, dir, actor ) { + const d = DIRS[ dir ]; + if ( !d ) return false; + const tx = x + d.x; + const ty = y + d.y; + // Tunel: salir por un borde en la fila del tunel siempre es valido. + if ( ty === TUNNEL_ROW && ( tx < 0 || tx >= grid[ 0 ].length ) ) return true; + return !isWall( grid, tx, ty, actor ); +} + +function wrapTunnel( a, width ) { + if ( Math.round( a.y ) === TUNNEL_ROW ) { + if ( a.x < 0 ) a.x += width; + else if ( a.x >= width ) a.x -= width; + } +} + +function movePacman( game ) { + const p = game.pacman; + const grid = game.grid; + const width = grid[ 0 ].length; + + if ( aligned( p.x ) && aligned( p.y ) ) { + p.x = Math.round( p.x ); + p.y = Math.round( p.y ); + + // Aplicar giro pendiente si es posible. + if ( p.nextDir && canMove( grid, p.x, p.y, p.nextDir, 'pacman' ) ) { + p.dir = p.nextDir; + p.nextDir = null; + } + // Comer dot. + if ( grid[ p.y ][ p.x ] === 2 ) { + grid[ p.y ][ p.x ] = 0; + game.score += 10; + game.dotsRemaining--; + } + // Si no puede seguir, se detiene en la celda. + if ( !canMove( grid, p.x, p.y, p.dir, 'pacman' ) ) return; + } + + const d = DIRS[ p.dir ]; + p.x += d.x * p.speed; + p.y += d.y * p.speed; + wrapTunnel( p, width ); +} + +function decideGhost( game, g ) { + const grid = game.grid; + const p = game.pacman; + + const options = Object.keys( DIRS ).filter( + ( dir ) => dir !== OPPOSITE[ g.dir ] && canMove( grid, g.x, g.y, dir, 'ghost' ) + ); + // Sin salida (callejon): permitir el giro de 180. + const choices = options.length ? options : [ '' + OPPOSITE[ g.dir ] ]; + + if ( g.kind === 'hunter' ) { + const px = Math.round( p.x ); + const py = Math.round( p.y ); + let best = choices[ 0 ]; + let bestDist = Infinity; + for ( const dir of choices ) { + const d = DIRS[ dir ]; + const nx = g.x + d.x; + const ny = g.y + d.y; + const dist = Math.abs( nx - px ) + Math.abs( ny - py ); + if ( dist < bestDist ) { + bestDist = dist; + best = dir; + } + } + g.dir = best; + } else { + g.dir = choices[ Math.floor( Math.random() * choices.length ) ]; + } +} + +function moveGhost( game, g ) { + const grid = game.grid; + const width = grid[ 0 ].length; + + if ( aligned( g.x ) && aligned( g.y ) ) { + g.x = Math.round( g.x ); + g.y = Math.round( g.y ); + decideGhost( game, g ); + if ( !canMove( grid, g.x, g.y, g.dir, 'ghost' ) ) return; + } + + const d = DIRS[ g.dir ]; + g.x += d.x * g.speed; + g.y += d.y * g.speed; + wrapTunnel( g, width ); +} + +function resetPositions( game ) { + const p = game.pacman; + p.x = PACMAN_START.x; + p.y = PACMAN_START.y; + p.dir = 'left'; + p.nextDir = null; + game.ghosts.forEach( ( g, i ) => { + g.x = GHOST_STARTS[ i ].x; + g.y = GHOST_STARTS[ i ].y; + g.dir = 'up'; + } ); +} + +function collides( a, b ) { + return Math.abs( a.x - b.x ) < 0.5 && Math.abs( a.y - b.y ) < 0.5; +} + +function update( game ) { + movePacman( game ); + game.ghosts.forEach( ( g ) => moveGhost( game, g ) ); + + for ( const g of game.ghosts ) { + if ( collides( game.pacman, g ) ) { + game.lives--; + if ( game.lives <= 0 ) { + game.state = 'lost'; + return; + } + resetPositions( game ); + break; + } + } + + if ( game.dotsRemaining <= 0 ) game.state = 'won'; +} + +window.createGame = createGame; +window.update = update; +window.DIRS = DIRS; diff --git a/src/js/main.js b/src/js/main.js new file mode 100644 index 0000000..9c6742e --- /dev/null +++ b/src/js/main.js @@ -0,0 +1,53 @@ +// main.js +// Bucle, teclado y pantallas. Usa createGame/update/draw (globals). + +const canvas = document.getElementById( 'game' ); +const ctx = canvas.getContext( '2d' ); +const overlay = document.getElementById( 'overlay' ); +const actionBtn = document.getElementById( 'action-btn' ); + +let game = createGame(); +let frame = 0; + +const KEY_DIR = { + ArrowLeft: 'left', + ArrowRight: 'right', + ArrowUp: 'up', + ArrowDown: 'down', +}; + +document.addEventListener( 'keydown', ( e ) => { + const dir = KEY_DIR[ e.key ]; + if ( !dir ) return; + e.preventDefault(); + if ( game.state === 'playing' ) game.pacman.nextDir = dir; +} ); + +function showOverlay( title, cls, btnLabel ) { + overlay.innerHTML = + '' + title + '' + + ''; + overlay.classList.add( 'show' ); + document.getElementById( 'action-btn' ).addEventListener( 'click', startGame ); +} + +function startGame() { + game = createGame(); + game.state = 'playing'; + overlay.classList.remove( 'show' ); +} + +if ( actionBtn ) actionBtn.addEventListener( 'click', startGame ); + +function loop() { + frame++; + if ( game.state === 'playing' ) { + update( game ); + if ( game.state === 'won' ) showOverlay( 'GANASTE', 'win', 'Reiniciar' ); + else if ( game.state === 'lost' ) showOverlay( 'PERDISTE', 'lose', 'Reiniciar' ); + } + draw( ctx, game, frame ); + requestAnimationFrame( loop ); +} + +loop(); diff --git a/src/js/maze.js b/src/js/maze.js new file mode 100644 index 0000000..9bcaf2c --- /dev/null +++ b/src/js/maze.js @@ -0,0 +1,62 @@ +// maze.js +// Laberinto 28x31 fiel a la geometria del nivel 1 de Pac-Man. +// Se escribe como 31 strings de 28 chars (legible) y se parsea a numeros. +// '#' pared(1) · '.' dot(2) · ' ' vacio transitable(0) · '-' puerta pen(3) +// Coordenadas: celda (x,y), origen arriba-izquierda. x in [0,27], y in [0,30]. +// Simetrico respecto al eje vertical central (entre cols 13 y 14). + +const MAZE_STR = [ + '############################', // 0 borde + '#............##............#', // 1 + '#.####.#####.##.#####.####.#', // 2 + '#.####.#####.##.#####.####.#', // 3 + '#.####.#####.##.#####.####.#', // 4 + '#..........................#', // 5 + '#.####.##.########.##.####.#', // 6 + '#.####.##.########.##.####.#', // 7 + '#......##....##....##......#', // 8 + '######.#####.##.#####.######', // 9 + '######.#####.##.#####.######', // 10 + '######.##..........##.######', // 11 + '######.##.###--###.##.######', // 12 puerta pen cols 13-14 + '######.##.# #.##.######', // 13 interior pen + ' # # ', // 14 tunel (extremos abiertos) + pen + '######.##.# #.##.######', // 15 interior pen + '######.##.########.##.######', // 16 fondo pen + '######.##..........##.######', // 17 + '######.#####.##.#####.######', // 18 + '######.#####.##.#####.######', // 19 + '#............##............#', // 20 + '#.####.#####.##.#####.####.#', // 21 + '#.####.#####.##.#####.####.#', // 22 + '#...##................##...#', // 23 fila inicio Pacman (13,23) + '###.##.##.########.##.##.###', // 24 + '###.##.##.########.##.##.###', // 25 + '#......##....##....##......#', // 26 + '#.##########.##.##########.#', // 27 + '#.##########.##.##########.#', // 28 + '#..........................#', // 29 + '############################', // 30 borde +]; + +function parseTile( ch ) { + if ( ch === '#' ) return 1; + if ( ch === '.' ) return 2; + if ( ch === '-' ) return 3; + return 0; // espacio = vacio transitable +} + +// Matriz numerica pristina (no se muta; cada partida copia esto). +const MAZE = MAZE_STR.map( ( row ) => row.split( '' ).map( parseTile ) ); + +const TUNNEL_ROW = 14; +const PACMAN_START = { x: 13, y: 23 }; +const GHOST_STARTS = [ + { x: 13, y: 14, kind: 'hunter' }, // dentro de la pen + { x: 14, y: 14, kind: 'random' }, // dentro de la pen +]; + +window.MAZE = MAZE; +window.TUNNEL_ROW = TUNNEL_ROW; +window.PACMAN_START = PACMAN_START; +window.GHOST_STARTS = GHOST_STARTS; diff --git a/src/js/render.js b/src/js/render.js new file mode 100644 index 0000000..3e04ac1 --- /dev/null +++ b/src/js/render.js @@ -0,0 +1,165 @@ +// render.js +// Dibujo arcade sobre canvas. Usa game.grid (no MAZE) para reflejar dots comidos. + +const TILE = 20; +const WALL_COLOR = '#2121ff'; +const DOOR_COLOR = '#ffb8ff'; +const DOT_COLOR = '#ffb897'; + +function cellCenter( x, y ) { + return { cx: x * TILE + TILE / 2, cy: y * TILE + TILE / 2 }; +} + +// Paredes estilo arcade: lineas finas redondeadas que conectan los centros +// de celdas-pared adyacentes. Produce el trazado continuo del original. +function drawWalls( ctx, grid ) { + const H = grid.length; + const W = grid[ 0 ].length; + ctx.strokeStyle = WALL_COLOR; + ctx.lineWidth = 2.5; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.beginPath(); + for ( let y = 0; y < H; y++ ) { + for ( let x = 0; x < W; x++ ) { + if ( grid[ y ][ x ] !== 1 ) continue; + const { cx, cy } = cellCenter( x, y ); + // Conectar solo hacia derecha y abajo evita trazos duplicados. + if ( x + 1 < W && grid[ y ][ x + 1 ] === 1 ) { + ctx.moveTo( cx, cy ); + ctx.lineTo( cx + TILE, cy ); + } + if ( y + 1 < H && grid[ y + 1 ][ x ] === 1 ) { + ctx.moveTo( cx, cy ); + ctx.lineTo( cx, cy + TILE ); + } + // Celda-pared aislada (sin vecino): punto corto para que se vea. + const lone = + ( x + 1 >= W || grid[ y ][ x + 1 ] !== 1 ) && + ( x - 1 < 0 || grid[ y ][ x - 1 ] !== 1 ) && + ( y + 1 >= H || grid[ y + 1 ][ x ] !== 1 ) && + ( y - 1 < 0 || grid[ y - 1 ][ x ] !== 1 ); + if ( lone ) { + ctx.moveTo( cx - 3, cy ); + ctx.lineTo( cx + 3, cy ); + } + } + } + ctx.stroke(); +} + +function drawDoor( ctx, grid ) { + const H = grid.length; + const W = grid[ 0 ].length; + ctx.strokeStyle = DOOR_COLOR; + ctx.lineWidth = 3; + ctx.beginPath(); + for ( let y = 0; y < H; y++ ) { + for ( let x = 0; x < W; x++ ) { + if ( grid[ y ][ x ] !== 3 ) continue; + const px = x * TILE; + const py = y * TILE + TILE / 2; + ctx.moveTo( px, py ); + ctx.lineTo( px + TILE, py ); + } + } + ctx.stroke(); +} + +function drawDots( ctx, grid ) { + ctx.fillStyle = DOT_COLOR; + for ( let y = 0; y < grid.length; y++ ) { + for ( let x = 0; x < grid[ 0 ].length; x++ ) { + if ( grid[ y ][ x ] !== 2 ) continue; + const { cx, cy } = cellCenter( x, y ); + ctx.beginPath(); + ctx.arc( cx, cy, 2.5, 0, Math.PI * 2 ); + ctx.fill(); + } + } +} + +function drawPacman( ctx, p, frame ) { + const { cx, cy } = cellCenter( p.x, p.y ); + let rot = 0; + if ( p.dir === 'right' ) rot = 0; + else if ( p.dir === 'down' ) rot = Math.PI / 2; + else if ( p.dir === 'left' ) rot = Math.PI; + else if ( p.dir === 'up' ) rot = -Math.PI / 2; + + // Boca animada: abre/cierra con el frame. + const open = ( Math.sin( frame * 0.3 ) * 0.5 + 0.5 ) * 0.28 + 0.02; + + ctx.fillStyle = '#ffff00'; + ctx.beginPath(); + ctx.moveTo( cx, cy ); + ctx.arc( cx, cy, TILE / 2 - 1, rot + open * Math.PI, rot - open * Math.PI ); + ctx.closePath(); + ctx.fill(); +} + +function drawGhost( ctx, g, color ) { + const { cx, cy } = cellCenter( g.x, g.y ); + const r = TILE / 2 - 1; + const top = cy - r; + const bottom = cy + r; + const left = cx - r; + const right = cx + r; + + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc( cx, cy - 1, r, Math.PI, 0, false ); // cabeza + ctx.lineTo( right, bottom ); + // falda ondulada (3 picos) + ctx.lineTo( right - r * 0.66, bottom - 4 ); + ctx.lineTo( cx, bottom ); + ctx.lineTo( left + r * 0.66, bottom - 4 ); + ctx.lineTo( left, bottom ); + ctx.closePath(); + ctx.fill(); + + // ojos mirando segun direccion + const dir = DIRS[ g.dir ] || { x: 0, y: 0 }; + const ex = dir.x * 1.6; + const ey = dir.y * 1.6; + for ( const off of [ -3.5, 3.5 ] ) { + ctx.fillStyle = '#fff'; + ctx.beginPath(); + ctx.arc( cx + off, cy - 1, 3, 0, Math.PI * 2 ); + ctx.fill(); + ctx.fillStyle = '#0000bb'; + ctx.beginPath(); + ctx.arc( cx + off + ex, cy - 1 + ey, 1.5, 0, Math.PI * 2 ); + ctx.fill(); + } +} + +function drawHUD( ctx, game, W ) { + ctx.fillStyle = '#fff'; + ctx.font = '14px "Courier New", monospace'; + ctx.textBaseline = 'top'; + ctx.textAlign = 'left'; + ctx.fillText( 'SCORE ' + game.score, 8, 4 ); + ctx.textAlign = 'right'; + ctx.fillText( 'VIDAS ' + game.lives, W * TILE - 8, 4 ); +} + +const GHOST_COLORS = [ '#ff0000', '#00ffff', '#ffb8ff', '#ffb852' ]; + +function draw( ctx, game, frame ) { + const grid = game.grid; + const W = grid[ 0 ].length; + const H = grid.length; + + ctx.fillStyle = '#000'; + ctx.fillRect( 0, 0, W * TILE, H * TILE ); + + drawWalls( ctx, grid ); + drawDoor( ctx, grid ); + drawDots( ctx, grid ); + drawPacman( ctx, game.pacman, frame ); + game.ghosts.forEach( ( g, i ) => drawGhost( ctx, g, GHOST_COLORS[ i ] || '#ff0000' ) ); + drawHUD( ctx, game, W ); +} + +window.draw = draw;