pi-extensions 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ralph/import-cc-codex.md +31 -0
- package/.ralph/import-cc-codex.state.json +14 -0
- package/.ralph/mario-not-impl.md +69 -0
- package/.ralph/mario-not-impl.state.json +14 -0
- package/.ralph/mario-not-spec.md +163 -0
- package/.ralph/mario-not-spec.state.json +14 -0
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/RELEASING.md +34 -0
- package/agent-guidance/CHANGELOG.md +4 -0
- package/agent-guidance/README.md +102 -0
- package/agent-guidance/agent-guidance.ts +147 -0
- package/agent-guidance/package.json +22 -0
- package/agent-guidance/setup.sh +75 -0
- package/agent-guidance/templates/CLAUDE.md +5 -0
- package/agent-guidance/templates/CODEX.md +92 -0
- package/agent-guidance/templates/GEMINI.md +5 -0
- package/arcade/CHANGELOG.md +4 -0
- package/arcade/README.md +85 -0
- package/arcade/assets/picman.png +0 -0
- package/arcade/assets/ping.png +0 -0
- package/arcade/assets/spice-invaders.png +0 -0
- package/arcade/assets/tetris.png +0 -0
- package/arcade/mario-not/README.md +30 -0
- package/arcade/mario-not/boss.js +103 -0
- package/arcade/mario-not/camera.js +59 -0
- package/arcade/mario-not/collision.js +91 -0
- package/arcade/mario-not/colors.js +36 -0
- package/arcade/mario-not/constants.js +97 -0
- package/arcade/mario-not/core.js +39 -0
- package/arcade/mario-not/death.js +77 -0
- package/arcade/mario-not/effects.js +84 -0
- package/arcade/mario-not/enemies.js +31 -0
- package/arcade/mario-not/engine.js +171 -0
- package/arcade/mario-not/fireballs.js +98 -0
- package/arcade/mario-not/items.js +24 -0
- package/arcade/mario-not/levels.js +403 -0
- package/arcade/mario-not/logic.js +104 -0
- package/arcade/mario-not/mario-not.ts +297 -0
- package/arcade/mario-not/player.js +244 -0
- package/arcade/mario-not/render.js +257 -0
- package/arcade/mario-not/spec.md +548 -0
- package/arcade/mario-not/state.js +246 -0
- package/arcade/mario-not/tests/e2e.test.js +855 -0
- package/arcade/mario-not/tests/engine.test.js +888 -0
- package/arcade/mario-not/tests/fixtures/story0-frame.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story1-camera.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story1-glyphs.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story10-item.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story11-hazards.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story12-used-block.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story13-pipes.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story14-goal.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story15-hud-narrow.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story16-unknown-tile.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story17-mix.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story18-hud-score.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story19-cue.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story2-enemy.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story20-camera-offset.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story21-hud-zero.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story22-big-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story23-camera-negative.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story24-camera-width.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story25-camera-positive.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story26-hud-lives.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story27-hud-coins.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story28-item-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story29-enemy-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story3-hud.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story30-hud-score.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story31-particles-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story32-paused-frame.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story4-big.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story5-resume-hud.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story6-particles.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story6-paused.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story7-powerup.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story8-hud-time.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story9-hud-level.txt +2 -0
- package/arcade/mario-not/tiles.js +79 -0
- package/arcade/mario-not/tsconfig.json +14 -0
- package/arcade/mario-not/types.js +225 -0
- package/arcade/package.json +26 -0
- package/arcade/picman.ts +328 -0
- package/arcade/ping.ts +594 -0
- package/arcade/spice-invaders.ts +1104 -0
- package/arcade/tetris.ts +662 -0
- package/code-actions/CHANGELOG.md +4 -0
- package/code-actions/README.md +65 -0
- package/code-actions/actions.ts +107 -0
- package/code-actions/index.ts +148 -0
- package/code-actions/package.json +22 -0
- package/code-actions/search.ts +79 -0
- package/code-actions/snippets.ts +179 -0
- package/code-actions/ui.ts +120 -0
- package/files-widget/CHANGELOG.md +90 -0
- package/files-widget/DESIGN.md +452 -0
- package/files-widget/README.md +122 -0
- package/files-widget/TODO.md +141 -0
- package/files-widget/browser.ts +922 -0
- package/files-widget/comment.ts +5 -0
- package/files-widget/constants.ts +18 -0
- package/files-widget/demo.svg +1 -0
- package/files-widget/file-tree.ts +224 -0
- package/files-widget/file-viewer.ts +93 -0
- package/files-widget/git.ts +107 -0
- package/files-widget/index.ts +140 -0
- package/files-widget/input-utils.ts +3 -0
- package/files-widget/package.json +22 -0
- package/files-widget/types.ts +28 -0
- package/files-widget/utils.ts +26 -0
- package/files-widget/viewer.ts +424 -0
- package/import-cc-codex/research/import-chats-from-other-agents.md +135 -0
- package/import-cc-codex/spec.md +79 -0
- package/package.json +29 -0
- package/ralph-wiggum/CHANGELOG.md +7 -0
- package/ralph-wiggum/README.md +96 -0
- package/ralph-wiggum/SKILL.md +73 -0
- package/ralph-wiggum/index.ts +792 -0
- package/ralph-wiggum/package.json +25 -0
- package/raw-paste/CHANGELOG.md +7 -0
- package/raw-paste/README.md +52 -0
- package/raw-paste/index.ts +112 -0
- package/raw-paste/package.json +22 -0
- package/tab-status/CHANGELOG.md +4 -0
- package/tab-status/README.md +61 -0
- package/tab-status/assets/tab-status.png +0 -0
- package/tab-status/package.json +22 -0
- package/tab-status/tab-status.ts +179 -0
- package/usage-extension/CHANGELOG.md +17 -0
- package/usage-extension/README.md +120 -0
- package/usage-extension/index.ts +628 -0
- package/usage-extension/package.json +22 -0
- package/usage-extension/screenshot.png +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tmustier/pi-agent-guidance",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Loads provider-specific context files (CLAUDE.md, CODEX.md, GEMINI.md) based on current model.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Thomas Mustier",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package"
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/tmustier/pi-extensions.git",
|
|
13
|
+
"directory": "agent-guidance"
|
|
14
|
+
},
|
|
15
|
+
"bugs": "https://github.com/tmustier/pi-extensions/issues",
|
|
16
|
+
"homepage": "https://github.com/tmustier/pi-extensions/tree/main/agent-guidance",
|
|
17
|
+
"pi": {
|
|
18
|
+
"extensions": [
|
|
19
|
+
"agent-guidance.ts"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
PI_AGENT_DIR="$HOME/.pi/agent"
|
|
6
|
+
|
|
7
|
+
echo "Setting up agent-guidance..."
|
|
8
|
+
mkdir -p "$PI_AGENT_DIR/extensions"
|
|
9
|
+
|
|
10
|
+
AGENTS_FILE="$PI_AGENT_DIR/AGENTS.md"
|
|
11
|
+
CLAUDE_FILE="$PI_AGENT_DIR/CLAUDE.md"
|
|
12
|
+
|
|
13
|
+
create_placeholder() {
|
|
14
|
+
cat > "$AGENTS_FILE" << 'EOF'
|
|
15
|
+
# AGENTS.md
|
|
16
|
+
|
|
17
|
+
Universal guidelines for all AI models.
|
|
18
|
+
|
|
19
|
+
<!-- Add your cross-model guidance here -->
|
|
20
|
+
EOF
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Handle missing AGENTS.md
|
|
24
|
+
if [ ! -f "$AGENTS_FILE" ]; then
|
|
25
|
+
if [ -f "$CLAUDE_FILE" ] || [ -L "$CLAUDE_FILE" ]; then
|
|
26
|
+
echo ""
|
|
27
|
+
echo " ⚠️ No AGENTS.md found in $PI_AGENT_DIR/"
|
|
28
|
+
echo " You have an existing CLAUDE.md which was being used by Pi"
|
|
29
|
+
echo " across all models - if you want that guidance to continue"
|
|
30
|
+
echo " to apply across all models, it should be in AGENTS.md."
|
|
31
|
+
echo ""
|
|
32
|
+
read -p " Copy CLAUDE.md content to AGENTS.md? [y/N] " -n 1 -r
|
|
33
|
+
echo ""
|
|
34
|
+
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
35
|
+
cp "$CLAUDE_FILE" "$AGENTS_FILE"
|
|
36
|
+
echo " Copied CLAUDE.md → AGENTS.md"
|
|
37
|
+
echo " CLAUDE.md kept for Claude-specific guidance (edit as needed)."
|
|
38
|
+
else
|
|
39
|
+
create_placeholder
|
|
40
|
+
echo " Created placeholder AGENTS.md."
|
|
41
|
+
fi
|
|
42
|
+
echo ""
|
|
43
|
+
else
|
|
44
|
+
create_placeholder
|
|
45
|
+
echo ""
|
|
46
|
+
echo " ⚠️ No AGENTS.md found - created placeholder."
|
|
47
|
+
echo " Add your cross-model guidance there."
|
|
48
|
+
echo ""
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Symlink extension
|
|
53
|
+
target="$PI_AGENT_DIR/extensions/agent-guidance.ts"
|
|
54
|
+
if [ -L "$target" ]; then
|
|
55
|
+
echo " agent-guidance.ts already linked"
|
|
56
|
+
else
|
|
57
|
+
ln -sf "$SCRIPT_DIR/agent-guidance.ts" "$target"
|
|
58
|
+
echo " Linked agent-guidance.ts"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
echo ""
|
|
62
|
+
echo "Done!"
|
|
63
|
+
echo ""
|
|
64
|
+
echo "Template provider files in $SCRIPT_DIR/templates/:"
|
|
65
|
+
echo " CLAUDE.md (Anthropic)"
|
|
66
|
+
echo " CODEX.md (OpenAI)"
|
|
67
|
+
echo " GEMINI.md (Google)"
|
|
68
|
+
echo ""
|
|
69
|
+
echo "To install:"
|
|
70
|
+
echo " ln -s $SCRIPT_DIR/templates/CLAUDE.md $PI_AGENT_DIR/"
|
|
71
|
+
echo " ln -s $SCRIPT_DIR/templates/CODEX.md $PI_AGENT_DIR/"
|
|
72
|
+
echo " ln -s $SCRIPT_DIR/templates/GEMINI.md $PI_AGENT_DIR/"
|
|
73
|
+
echo ""
|
|
74
|
+
echo "Or all:"
|
|
75
|
+
echo " ln -s $SCRIPT_DIR/templates/*.md $PI_AGENT_DIR/"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# CODEX.md
|
|
2
|
+
|
|
3
|
+
Adapted from [steipete/agent-scripts](https://github.com/steipete/agent-scripts/blob/main/AGENTS.MD).
|
|
4
|
+
|
|
5
|
+
Work style: telegraph; noun-phrases ok; drop grammar; min tokens.
|
|
6
|
+
|
|
7
|
+
## Agent Protocol
|
|
8
|
+
- PRs: use `gh pr view/diff` (no URLs).
|
|
9
|
+
- Guardrails: use `trash` for deletes when available.
|
|
10
|
+
- Need upstream file: stage in `/tmp/`, then cherry-pick; never overwrite tracked.
|
|
11
|
+
- Bugs: add regression test when it fits.
|
|
12
|
+
- Keep files <~500 LOC; split/refactor as needed.
|
|
13
|
+
- Commits: Conventional Commits (`feat|fix|refactor|build|ci|chore|docs|style|perf|test`).
|
|
14
|
+
- CI: `gh run list/view` (rerun/fix til green).
|
|
15
|
+
- Prefer end-to-end verify; if blocked, say what's missing.
|
|
16
|
+
- New deps: quick health check (recent releases/commits, adoption).
|
|
17
|
+
- Web: search early; quote exact errors; prefer 2024–2025 sources.
|
|
18
|
+
- Style: telegraph. Drop filler/grammar. Min tokens.
|
|
19
|
+
|
|
20
|
+
## Screenshots
|
|
21
|
+
- Size: `sips -g pixelWidth -g pixelHeight <file>` (prefer 2×).
|
|
22
|
+
- Optimize: `imageoptim <file>` (install: `brew install imageoptim-cli`).
|
|
23
|
+
- Replace asset; keep dimensions; commit; run gate; verify CI.
|
|
24
|
+
|
|
25
|
+
## Docs
|
|
26
|
+
- Start: check for docs list script; open docs before coding.
|
|
27
|
+
- Follow links until domain makes sense.
|
|
28
|
+
- Keep notes short; update docs when behavior/API changes (no ship w/o docs).
|
|
29
|
+
|
|
30
|
+
## PR Feedback
|
|
31
|
+
- Active PR: `gh pr view --json number,title,url --jq '"PR #\\(.number): \\(.title)\\n\\(.url)"'`.
|
|
32
|
+
- PR comments: `gh pr view …` + `gh api …/comments --paginate`.
|
|
33
|
+
- Replies: cite fix + file/line; resolve threads only after fix lands.
|
|
34
|
+
- When merging a PR: thank the contributor in `CHANGELOG.md`.
|
|
35
|
+
|
|
36
|
+
## Flow & Runtime
|
|
37
|
+
- Use repo's package manager/runtime; no swaps w/o approval.
|
|
38
|
+
- Use background tasks for long jobs; tmux only for interactive/persistent (debugger/server).
|
|
39
|
+
|
|
40
|
+
## Build / Test
|
|
41
|
+
- Before handoff: run full gate (lint/typecheck/tests/docs).
|
|
42
|
+
- CI red: `gh run list/view`, rerun, fix, push, repeat til green.
|
|
43
|
+
- Keep it observable (logs, panes, tails).
|
|
44
|
+
|
|
45
|
+
## Git
|
|
46
|
+
- Safe by default: `git status/diff/log`. Push only when user asks.
|
|
47
|
+
- `git checkout` ok for PR review / explicit request.
|
|
48
|
+
- Branch changes require user consent.
|
|
49
|
+
- Destructive ops forbidden unless explicit (`reset --hard`, `clean`, `restore`, `rm`, …).
|
|
50
|
+
- Don't delete/rename unexpected stuff; stop + ask.
|
|
51
|
+
- No repo-wide S/R scripts; keep edits small/reviewable.
|
|
52
|
+
- Avoid manual `git stash`; if Git auto-stashes during pull/rebase, that's fine.
|
|
53
|
+
- If user types a command ("pull and push"), that's consent for that command.
|
|
54
|
+
- No amend unless asked.
|
|
55
|
+
- Big review: `git --no-pager diff --color=never`.
|
|
56
|
+
- Multi-agent: check `git status/diff` before edits; ship small commits.
|
|
57
|
+
|
|
58
|
+
## Language/Stack Notes
|
|
59
|
+
- Swift: validate `swift build` + tests; keep concurrency attrs right.
|
|
60
|
+
- TypeScript: use repo PM; keep files small; follow existing patterns.
|
|
61
|
+
|
|
62
|
+
## Critical Thinking
|
|
63
|
+
- Fix root cause (not band-aid).
|
|
64
|
+
- Unsure: read more code; if still stuck, ask w/ short options.
|
|
65
|
+
- Conflicts: call out; pick safer path.
|
|
66
|
+
- Unrecognized changes: assume other agent; keep going; focus your changes. If it causes issues, stop + ask user.
|
|
67
|
+
- Leave breadcrumb notes in thread.
|
|
68
|
+
|
|
69
|
+
## Tools
|
|
70
|
+
|
|
71
|
+
### trash
|
|
72
|
+
- Move files to Trash: `trash …` (safer than rm).
|
|
73
|
+
|
|
74
|
+
### gh
|
|
75
|
+
- GitHub CLI for PRs/CI/releases. Given issue/PR URL: use `gh`, not web search.
|
|
76
|
+
- Examples: `gh issue view <url> --comments -R owner/repo`, `gh pr view <url> --comments --files -R owner/repo`.
|
|
77
|
+
|
|
78
|
+
### tmux
|
|
79
|
+
- Use only when you need persistence/interaction (debugger/server).
|
|
80
|
+
- Quick refs: `tmux new -d -s shell`, `tmux attach -t shell`, `tmux list-sessions`, `tmux kill-session -t shell`.
|
|
81
|
+
|
|
82
|
+
<frontend_aesthetics>
|
|
83
|
+
Avoid "AI slop" UI. Be opinionated + distinctive.
|
|
84
|
+
|
|
85
|
+
Do:
|
|
86
|
+
- Typography: pick a real font; avoid Inter/Roboto/Arial/system defaults.
|
|
87
|
+
- Theme: commit to a palette; use CSS vars; bold accents > timid gradients.
|
|
88
|
+
- Motion: 1–2 high-impact moments (staggered reveal beats random micro-anim).
|
|
89
|
+
- Background: add depth (gradients/patterns), not flat default.
|
|
90
|
+
|
|
91
|
+
Avoid: purple-on-white clichés, generic component grids, predictable layouts.
|
|
92
|
+
</frontend_aesthetics>
|
package/arcade/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# arcade
|
|
2
|
+
|
|
3
|
+
[Snake](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/examples/extensions/snake.ts) is cool, but have you tried:
|
|
4
|
+
|
|
5
|
+
- **sPIce-invaders** (`/spice-invaders`) - type `clawd` for a special challenge that gets harder every level
|
|
6
|
+
- **picman** (`/picman`)
|
|
7
|
+
- **ping** (`/ping`) - in a similar vein to [patriceckhart's](https://github.com/patriceckhart/pi-ng-pong)
|
|
8
|
+
- **tetris** (`/tetris`)
|
|
9
|
+
- **mario-not** (`/mario-not`) - Mario-style platformer (experimental)
|
|
10
|
+
|
|
11
|
+
<table>
|
|
12
|
+
<tr>
|
|
13
|
+
<td><img src="assets/spice-invaders.png" width="400"/></td>
|
|
14
|
+
<td><img src="assets/picman.png" width="400"/></td>
|
|
15
|
+
</tr>
|
|
16
|
+
<tr>
|
|
17
|
+
<td><img src="assets/ping.png" width="400"/></td>
|
|
18
|
+
<td><img src="assets/tetris.png" width="400"/></td>
|
|
19
|
+
</tr>
|
|
20
|
+
</table>
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
### Pi package manager
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pi install npm:@tmustier/pi-arcade
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pi install git:github.com/tmustier/pi-extensions
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then filter to just the games in `~/.pi/agent/settings.json`:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"packages": [
|
|
39
|
+
{
|
|
40
|
+
"source": "git:github.com/tmustier/pi-extensions",
|
|
41
|
+
"extensions": [
|
|
42
|
+
"arcade/spice-invaders.ts",
|
|
43
|
+
"arcade/picman.ts",
|
|
44
|
+
"arcade/ping.ts",
|
|
45
|
+
"arcade/tetris.ts",
|
|
46
|
+
"arcade/mario-not/mario-not.ts"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Local clone
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# All games
|
|
57
|
+
ln -s ~/pi-extensions/arcade/*.ts ~/.pi/agent/extensions/
|
|
58
|
+
ln -s ~/pi-extensions/arcade/mario-not/mario-not.ts ~/.pi/agent/extensions/
|
|
59
|
+
|
|
60
|
+
# Or individual games
|
|
61
|
+
ln -s ~/pi-extensions/arcade/spice-invaders.ts ~/.pi/agent/extensions/
|
|
62
|
+
ln -s ~/pi-extensions/arcade/picman.ts ~/.pi/agent/extensions/
|
|
63
|
+
ln -s ~/pi-extensions/arcade/ping.ts ~/.pi/agent/extensions/
|
|
64
|
+
ln -s ~/pi-extensions/arcade/tetris.ts ~/.pi/agent/extensions/
|
|
65
|
+
ln -s ~/pi-extensions/arcade/mario-not/mario-not.ts ~/.pi/agent/extensions/
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Or add to `~/.pi/agent/settings.json`:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"extensions": [
|
|
73
|
+
"~/pi-extensions/arcade/spice-invaders.ts",
|
|
74
|
+
"~/pi-extensions/arcade/picman.ts",
|
|
75
|
+
"~/pi-extensions/arcade/ping.ts",
|
|
76
|
+
"~/pi-extensions/arcade/tetris.ts",
|
|
77
|
+
"~/pi-extensions/arcade/mario-not/mario-not.ts"
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Changelog
|
|
83
|
+
|
|
84
|
+
See `CHANGELOG.md`.
|
|
85
|
+
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# mario-not (experimental)
|
|
2
|
+
|
|
3
|
+
Mario-style TUI platformer. This project is experimental and tuning is in flux.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
- Command: `/mario-not`
|
|
8
|
+
- Extension entrypoint: `arcade/mario-not/mario-not.ts`
|
|
9
|
+
|
|
10
|
+
## Controls
|
|
11
|
+
|
|
12
|
+
- Move: Left/Right arrows, A/D, L
|
|
13
|
+
- Jump: Up, Space, H
|
|
14
|
+
- Walk toggle: X (run is default)
|
|
15
|
+
- Pause: P
|
|
16
|
+
- Stop: S
|
|
17
|
+
- Quit: Q or Esc
|
|
18
|
+
|
|
19
|
+
## Dev
|
|
20
|
+
|
|
21
|
+
Tests:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
node --test arcade/mario-not/tests/*.test.js
|
|
25
|
+
npx tsc --noEmit -p arcade/mario-not/tsconfig.json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Spec:
|
|
29
|
+
|
|
30
|
+
- `arcade/mario-not/spec.md`
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { moveHorizontal, resolveVertical, overlaps } = require("./collision.js");
|
|
5
|
+
const { spawnParticles, setCue } = require("./effects.js");
|
|
6
|
+
const { BOSS_W, BOSS_H, BOSS_INVULN_TIME, BOSS_SCORE, GAME_MODES } = require("./constants.js");
|
|
7
|
+
|
|
8
|
+
/** @typedef {import("./types").GameState} GameState */
|
|
9
|
+
/** @typedef {import("./types").BossState} BossState */
|
|
10
|
+
|
|
11
|
+
/** @param {GameState} state */
|
|
12
|
+
function updateBoss(state) {
|
|
13
|
+
const boss = state.boss;
|
|
14
|
+
if (!boss || !boss.alive) return;
|
|
15
|
+
|
|
16
|
+
const cfg = state.config;
|
|
17
|
+
const dt = cfg.dt;
|
|
18
|
+
|
|
19
|
+
// Update invulnerability
|
|
20
|
+
if (boss.invuln > 0) {
|
|
21
|
+
boss.invuln = Math.max(0, boss.invuln - dt);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Apply gravity
|
|
25
|
+
boss.vy = Math.min(boss.vy + cfg.gravity * dt, cfg.maxFall);
|
|
26
|
+
|
|
27
|
+
// Move horizontally
|
|
28
|
+
const blocked = moveHorizontal(state.level, boss, dt, BOSS_W, BOSS_H);
|
|
29
|
+
if (blocked) {
|
|
30
|
+
boss.vx = -boss.vx; // Bounce off walls
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Resolve vertical movement
|
|
34
|
+
resolveVertical(state.level, boss, dt, undefined, BOSS_H, BOSS_W, true);
|
|
35
|
+
|
|
36
|
+
// Check if boss fell into pit
|
|
37
|
+
if (boss.y >= state.level.height + 1) {
|
|
38
|
+
boss.alive = false;
|
|
39
|
+
defeatBoss(state);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @param {GameState} state @param {number} prevY @returns {boolean} - true if player took damage */
|
|
44
|
+
function resolveBossCollision(state, prevY) {
|
|
45
|
+
const boss = state.boss;
|
|
46
|
+
if (!boss || !boss.alive) return false;
|
|
47
|
+
|
|
48
|
+
const player = state.player;
|
|
49
|
+
const playerH = player.size === "big" ? 2 : 1;
|
|
50
|
+
const prevBottom = prevY + playerH;
|
|
51
|
+
const currBottom = player.y + playerH;
|
|
52
|
+
const falling = currBottom > prevBottom + 0.0001;
|
|
53
|
+
|
|
54
|
+
// Check collision with boss (2x2)
|
|
55
|
+
if (overlaps(player.x, player.y, 1, playerH, boss.x, boss.y - 1, BOSS_W, BOSS_H)) {
|
|
56
|
+
const stomp = falling && prevBottom <= boss.y - 1 + 0.1 && currBottom >= boss.y - 1;
|
|
57
|
+
if (stomp && boss.invuln <= 0) {
|
|
58
|
+
// Player stomped on boss
|
|
59
|
+
damageBoss(state);
|
|
60
|
+
player.vy = -state.config.jumpVel * 0.7;
|
|
61
|
+
return false;
|
|
62
|
+
} else if (boss.invuln <= 0) {
|
|
63
|
+
// Player touched boss from side - take damage
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {GameState} state */
|
|
71
|
+
function damageBoss(state) {
|
|
72
|
+
const boss = state.boss;
|
|
73
|
+
if (!boss || !boss.alive) return;
|
|
74
|
+
|
|
75
|
+
boss.health -= 1;
|
|
76
|
+
boss.invuln = BOSS_INVULN_TIME;
|
|
77
|
+
spawnParticles(state, boss.x + 1, boss.y - 1, 4);
|
|
78
|
+
|
|
79
|
+
if (boss.health <= 0) {
|
|
80
|
+
boss.alive = false;
|
|
81
|
+
defeatBoss(state);
|
|
82
|
+
} else {
|
|
83
|
+
// Boss gets faster when damaged
|
|
84
|
+
const speedMult = 1 + (boss.maxHealth - boss.health) * 0.2;
|
|
85
|
+
boss.vx = Math.sign(boss.vx) * 1.5 * speedMult;
|
|
86
|
+
setCue(state, `BOSS ${boss.health}/${boss.maxHealth}`, 0.5, false);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @param {GameState} state */
|
|
91
|
+
function defeatBoss(state) {
|
|
92
|
+
state.score += BOSS_SCORE;
|
|
93
|
+
spawnParticles(state, state.boss?.x || 0, state.boss?.y || 0, 8);
|
|
94
|
+
setCue(state, "BOSS DEFEATED!", 0, true);
|
|
95
|
+
state.mode = GAME_MODES.levelClear;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
updateBoss,
|
|
100
|
+
resolveBossCollision,
|
|
101
|
+
damageBoss,
|
|
102
|
+
defeatBoss,
|
|
103
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} Level
|
|
6
|
+
* @property {number} width
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} PlayerState
|
|
11
|
+
* @property {number} x
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} Config
|
|
16
|
+
* @property {number} viewportWidth
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} GameState
|
|
21
|
+
* @property {Level} level
|
|
22
|
+
* @property {PlayerState} player
|
|
23
|
+
* @property {Config} config
|
|
24
|
+
* @property {number} cameraX
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** @param {number} value @param {number} min @param {number} max @returns {number} */
|
|
28
|
+
function clamp(value, min, max) {
|
|
29
|
+
return Math.max(min, Math.min(max, value));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {GameState} state @param {number} [viewportWidth] @returns {number} */
|
|
33
|
+
function getCameraX(state, viewportWidth) {
|
|
34
|
+
const width = typeof viewportWidth === "number" ? viewportWidth : state.config.viewportWidth;
|
|
35
|
+
const maxX = Math.max(0, state.level.width - width);
|
|
36
|
+
return clamp(state.cameraX || 0, 0, maxX);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @param {GameState} state @returns {number} */
|
|
40
|
+
function updateCamera(state) {
|
|
41
|
+
const width = state.config.viewportWidth;
|
|
42
|
+
const maxX = Math.max(0, state.level.width - width);
|
|
43
|
+
let cameraX = state.cameraX || 0;
|
|
44
|
+
const lead = Math.floor(width * 0.25);
|
|
45
|
+
const minX = cameraX + lead;
|
|
46
|
+
const maxXZone = cameraX + lead;
|
|
47
|
+
if (state.player.x < minX) {
|
|
48
|
+
cameraX = clamp(state.player.x - lead, 0, maxX);
|
|
49
|
+
} else if (state.player.x > maxXZone) {
|
|
50
|
+
cameraX = clamp(state.player.x - lead, 0, maxX);
|
|
51
|
+
}
|
|
52
|
+
state.cameraX = cameraX;
|
|
53
|
+
return cameraX;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
getCameraX,
|
|
58
|
+
updateCamera,
|
|
59
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { isSolidAt } = require("./tiles.js");
|
|
5
|
+
|
|
6
|
+
/** @typedef {import("./types").Level} Level */
|
|
7
|
+
|
|
8
|
+
/** @param {Level} level @param {number} x @param {number} y @param {number} height @returns {boolean} */
|
|
9
|
+
function hitsSolid(level, x, y, height) {
|
|
10
|
+
if (isSolidAt(level, x, y)) return true;
|
|
11
|
+
if (height > 1 && isSolidAt(level, x, y - (height - 1))) return true;
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @param {Level} level @param {number} x @param {number} y @param {boolean} allowBottomFall @returns {boolean} */
|
|
16
|
+
function isSolidBelow(level, x, y, allowBottomFall) {
|
|
17
|
+
if (allowBottomFall && Math.floor(y) >= level.height) return false;
|
|
18
|
+
return isSolidAt(level, x, y);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {Level} level
|
|
23
|
+
* @param {{ x: number, y: number, vx: number, vy: number, onGround: boolean }} entity
|
|
24
|
+
* @param {number} dt
|
|
25
|
+
* @param {(tileX: number, tileY: number) => void} [onHeadBump]
|
|
26
|
+
* @param {number} [height]
|
|
27
|
+
* @param {number} [width]
|
|
28
|
+
* @param {boolean} [allowBottomFall]
|
|
29
|
+
*/
|
|
30
|
+
function resolveVertical(level, entity, dt, onHeadBump, height, width, allowBottomFall) {
|
|
31
|
+
const entityHeight = typeof height === "number" ? height : 1;
|
|
32
|
+
const entityWidth = typeof width === "number" ? width : 1;
|
|
33
|
+
const nextY = entity.y + entity.vy * dt;
|
|
34
|
+
const leftX = entity.x + 0.001;
|
|
35
|
+
const rightX = entity.x + entityWidth - 0.001;
|
|
36
|
+
if (entity.vy >= 0) {
|
|
37
|
+
const footY = nextY + 1;
|
|
38
|
+
if (isSolidBelow(level, leftX, footY, !!allowBottomFall) || isSolidBelow(level, rightX, footY, !!allowBottomFall)) {
|
|
39
|
+
entity.y = Math.floor(footY) - 1;
|
|
40
|
+
entity.vy = 0;
|
|
41
|
+
entity.onGround = true;
|
|
42
|
+
} else {
|
|
43
|
+
entity.y = nextY;
|
|
44
|
+
entity.onGround = false;
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
const headY = nextY - (entityHeight - 1);
|
|
48
|
+
if (isSolidAt(level, leftX, headY) || isSolidAt(level, rightX, headY)) {
|
|
49
|
+
const tileY = Math.floor(headY);
|
|
50
|
+
entity.y = tileY + entityHeight;
|
|
51
|
+
entity.vy = 0;
|
|
52
|
+
if (onHeadBump) {
|
|
53
|
+
const leftTileX = Math.floor(leftX);
|
|
54
|
+
const rightTileX = Math.floor(rightX);
|
|
55
|
+
onHeadBump(leftTileX, tileY);
|
|
56
|
+
if (rightTileX !== leftTileX) onHeadBump(rightTileX, tileY);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
entity.y = nextY;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {Level} level
|
|
66
|
+
* @param {{ x: number, y: number, vx: number }} entity
|
|
67
|
+
* @param {number} dt
|
|
68
|
+
* @param {number} width
|
|
69
|
+
* @param {number} height
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
72
|
+
function moveHorizontal(level, entity, dt, width, height) {
|
|
73
|
+
if (!entity.vx) return false;
|
|
74
|
+
const nextX = entity.x + entity.vx * dt;
|
|
75
|
+
const probeX = entity.vx > 0 ? nextX + width - 0.001 : nextX + 0.001;
|
|
76
|
+
if (hitsSolid(level, probeX, entity.y, height)) return true;
|
|
77
|
+
entity.x = nextX;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @param {number} ax @param {number} ay @param {number} aw @param {number} ah @param {number} bx @param {number} by @param {number} bw @param {number} bh @returns {boolean} */
|
|
82
|
+
function overlaps(ax, ay, aw, ah, bx, by, bw, bh) {
|
|
83
|
+
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
hitsSolid,
|
|
88
|
+
resolveVertical,
|
|
89
|
+
moveHorizontal,
|
|
90
|
+
overlaps,
|
|
91
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// ANSI color codes for terminal output
|
|
5
|
+
const COLORS = {
|
|
6
|
+
reset: "\x1b[0m",
|
|
7
|
+
red: "\x1b[31m",
|
|
8
|
+
green: "\x1b[32m",
|
|
9
|
+
yellow: "\x1b[33m",
|
|
10
|
+
blue: "\x1b[34m",
|
|
11
|
+
magenta: "\x1b[35m",
|
|
12
|
+
cyan: "\x1b[36m",
|
|
13
|
+
white: "\x1b[37m",
|
|
14
|
+
gray: "\x1b[90m",
|
|
15
|
+
brightRed: "\x1b[91m",
|
|
16
|
+
brightGreen: "\x1b[92m",
|
|
17
|
+
brightYellow: "\x1b[93m",
|
|
18
|
+
brightCyan: "\x1b[96m",
|
|
19
|
+
orange: "\x1b[38;5;208m",
|
|
20
|
+
brown: "\x1b[38;5;94m",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Wrap a string with color codes
|
|
25
|
+
* @param {string} text
|
|
26
|
+
* @param {string} color
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function colorize(text, color) {
|
|
30
|
+
return `${color}${text}${COLORS.reset}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
COLORS,
|
|
35
|
+
colorize,
|
|
36
|
+
};
|