Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 58 additions & 38 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Deploy to GitHub Pages

on:
pull_request:
push:
branches: [main]
workflow_dispatch:
Expand All @@ -12,7 +13,7 @@ permissions:

# Let a running deploy finish rather than cancelling it mid-publish.
concurrency:
group: pages
group: pages-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: false

jobs:
Expand Down Expand Up @@ -40,6 +41,26 @@ jobs:
- name: Build
run: npm run build

- name: Cache Godot toolchain
uses: actions/cache@v4
with:
path: |
~/.cache/universal-ai/godot/4.7.2
~/.local/share/godot/export_templates/4.7.2.stable
key: godot-web-linux-4.7.2

- name: Set up Godot
run: npm run godot:setup

- name: Test Godot prototype
run: npm run godot:test

- name: Export Godot prototype
run: npm run godot:build

- name: Stage prototype at /seed/
run: npm run godot:stage

# The hashed entry filename is this build's fingerprint. `verify` asserts
# the live site references exactly this one, which is the only way to tell
# "the deploy worked" apart from "something else published something else".
Expand All @@ -54,13 +75,46 @@ jobs:
echo "entry=$entry" >> "$GITHUB_OUTPUT"
echo "Built entry: $entry"

- name: Install test browsers
run: |
npx playwright install --with-deps chromium firefox
sudo apt-get install -y xvfb libgl1-mesa-dri libegl-mesa0 mesa-utils

- name: Test chooser and classic browser flows
run: npx playwright test

- name: Test Godot in Chromium and Firefox
env:
GODOT_BROWSER_HEADED: '1'
GODOT_BROWSER_ANGLE: gl
LIBGL_ALWAYS_SOFTWARE: '1'
run: |
xvfb-run --auto-servernum glxinfo -B
xvfb-run --auto-servernum --server-args="-screen 0 1440x1000x24" npx playwright test --config playwright.godot.config.ts --max-failures=1 --reporter=list

- name: Save browser checks and chooser screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: browser-checks
path: |
test-results/
build/test-results-godot/
retention-days: 7

- name: Record complete release
run: python scripts/verify_pages.py record --commit "$GITHUB_SHA"

- uses: actions/configure-pages@v5
if: github.event_name != 'pull_request'

- uses: actions/upload-pages-artifact@v3
if: github.event_name != 'pull_request'
with:
path: dist

deploy:
if: github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
environment:
Expand All @@ -72,46 +126,12 @@ jobs:
- id: deployment
uses: actions/deploy-pages@v4

# A green deploy job only means the artifact was accepted. It does not mean the
# artifact is what the domain serves: Pages will also publish the raw repository
# from a branch if its source is set that way, and that publish can land after
# this one and win. When it does, visitors get index.html with `src/main.tsx` in
# it, no bundle, and a grey screen — while every check here stays green. This
# job is what makes that condition fail out loud instead.
verify:
needs: [build, deploy]
runs-on: ubuntu-latest
steps:
- name: Assert the live site is the build we just published
- uses: actions/checkout@v4
- name: Verify all three live entry points and Godot assets
env:
PAGE_URL: ${{ needs.deploy.outputs.page_url }}
ENTRY: ${{ needs.build.outputs.entry }}
run: |
url="${PAGE_URL:-https://paperclips.opsvibe.systems/}"
echo "Expecting $url to reference $ENTRY"

# Pages fronts the site with a CDN; a fresh publish takes a moment to
# reach the edge. Poll rather than race it.
for attempt in $(seq 1 12); do
html=$(curl -fsSL --max-time 30 "$url?cachebust=$GITHUB_RUN_ID-$attempt" || true)

if printf '%s' "$html" | grep -qF "$ENTRY"; then
echo "OK — live site is serving $ENTRY"
exit 0
fi

if printf '%s' "$html" | grep -qF 'src/main.tsx'; then
echo "Attempt $attempt: still serving the unbuilt source index.html"
else
echo "Attempt $attempt: $ENTRY not present yet"
fi
sleep 20
done

echo "::error::$url is not serving this build's entry script ($ENTRY)."
if printf '%s' "$html" | grep -qF 'src/main.tsx'; then
echo "::error::It is serving the repository's source index.html, which loads /src/main.tsx — a file browsers cannot execute. That is the grey screen."
echo "::error::Cause: GitHub Pages is publishing from a branch, so the pages-build-deployment workflow uploads the repo root and overwrites this artifact."
echo "::error::Fix: Settings -> Pages -> Build and deployment -> Source -> GitHub Actions."
fi
exit 1
run: python scripts/verify_pages.py check --commit "$GITHUB_SHA" --url "$PAGE_URL"
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ coverage/
*.log
.env*
!.env.example
playwright-report/
test-results/

# Godot imported resources are rebuilt from the source assets.
godot/.godot/
godot/.godot-export/
62 changes: 59 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@
An idle game about optimization, and about what happens when you stop being the
one doing it.

**Play it: [paperclips.opsvibe.systems](https://paperclips.opsvibe.systems)**
**Choose a version: [paperclips.opsvibe.systems](https://paperclips.opsvibe.systems)**

The shared entrance describes both games and their contributors:

- `/classic/`: the full React/TypeScript game, with the recent observatory UI.
- `/seed/`: The Seed, the independent Godot opening prototype.

**Godot performance is still under investigation.** Severe slowdowns persisted
on the development laptop after initial fixes. Testing from another machine
is pending. Read the [performance and development handoff](docs/PERFORMANCE-HANDOFF.md)
before resuming local tests; local servers and browsers were shut down at the
user’s request.

You start by etching NPU chips one at a time. You buy a fab, then fifty. You set
a price, chase demand, and earn trust. Then you hand the wheel to an autonomous
Expand All @@ -17,6 +28,18 @@ story. Play the original first.

---

## Godot prototype: The Seed

A new playable 3D fabrication room lives in [`godot/`](godot/README.md): moving
etch heads, physical chip output, six autonomous fabs, synthesized sound, and a
district reveal. Run `npm run godot:build && npm run godot:serve`, then open
http://localhost:4180. Use `npm run godot:editor` to open the native project.

The Pages workflow includes it at `/seed/`, alongside `/classic/` and the
lightweight chooser at `/`. This is a standalone opening-loop
prototype with its own save; the full React game below remains available. See
the [Godot guide](godot/README.md) for controls, setup, tests, and current scope.

## Running it

```bash
Expand All @@ -29,13 +52,36 @@ npm run lint # tsc --noEmit, strict
npm test # vitest
npm run build # static files in dist/
npm run preview # serve the real build with the real CSP
npm run test:browser # build + Chromium browser regression tests
```

No API keys. No `.env`. No backend. `npm run build` emits static files and
that's the entire deployment.

---

## The observatory

The interface is a live instrument: a floating processor above an etched silicon
wafer, a planet being converted, and finally a luminous interstellar swarm.
These are procedural Canvas scenes, driven by your actual game state. Alignment
changes their light; new fabs join the wafer network; harvesting consumes the
globe. They are schematics, not literal maps or one dot per probe.

Fabricate directly from the observatory, build your first fab from its capital
objective, or release the Overseer. Expand the view for a closer look; Escape
returns to the controls. Animation can be paused independently of the game and
respects reduced-motion preferences.

A live production trace measures actual chips per elapsed second. Session
transmissions record first fabrication, factory purchases, trust increases,
projects, phase transitions, and directive overrides. Neither invents activity
while the system is idle. These instruments reset on reload; your game save does
not.

The layout is designed for phones as well as desktops. All visuals and fonts
are local, and the default engine needs no downloads beyond the app itself.

## The Overseer

You can play the whole game by hand. But the interesting part is Overseer mode,
Expand Down Expand Up @@ -179,13 +225,14 @@ can confirm by watching, and by reading
widens one way only. Within a phase it's still a fairly static grid.
- **Offline progress is capped at 8 hours**, so a laptop left shut for a month
isn't an instant win.
- **The mobile layout is functional, not designed.** It works; it isn't nice.
- **The observatory is a schematic.** It compresses huge populations into bounded
visual samples; use the numerical telemetry for exact quantities.

---

## How it's built

React 19 + TypeScript + Vite + Tailwind 4. Canvas pixel-art renderer, SVG radar,
React 19 + TypeScript + Vite + Tailwind 4. Procedural Canvas observatory, SVG production trace and radar,
Web Audio synthesizer. Deployed to GitHub Pages from `main` by
[`.github/workflows/deploy.yml`](.github/workflows/deploy.yml), which typechecks
and tests before it builds — and then fetches the live URL and fails unless it's
Expand Down Expand Up @@ -218,6 +265,15 @@ codebase carrying eighty-nine.

---

## Browser checks

Install Chromium once with `npx playwright install chromium`, then run
`npm run test:browser`. The suite covers fabrication, the first fab, all three
phases on mobile, the Overseer, animation controls, reduced motion, save
restoration, production CSP, offline reload, and model-cache preservation.
It starts preview on port 4173 and development on port 3000 when needed.
For an existing system browser, set `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`.

## Docs

- **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)** — how the code is laid out,
Expand Down
117 changes: 117 additions & 0 deletions classic/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<!--
Content Security Policy — the network guarantee, in enforceable form.

There is no backend. The page has no analytics, no telemetry, no fonts,
no trackers, and no API. `connect-src` lists the only origins this page
is permitted to reach, ever:

huggingface.co, *.hf.co model weights, if you opt into WebLLM
raw.githubusercontent.com the compiled model libraries

Those are contacted only when you explicitly choose the WebLLM Overseer
and press download. The default Utility Engine contains no network code
at all, so on a default game nothing leaves your machine — open DevTools
and watch. What CSP guarantees on top of that is the harder part: even a
compromised dependency could not exfiltrate anything anywhere else.

`wasm-unsafe-eval` is required to compile WebLLM's WebAssembly. It does
not permit eval() of JavaScript.
-->
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
worker-src 'self' blob:;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self';
connect-src 'self' blob: data: https://huggingface.co https://*.hf.co https://raw.githubusercontent.com;
form-action 'none';
base-uri 'self';
object-src 'none'
"
/>
<!--
No `frame-ancestors` here: it is ignored when delivered via <meta>, and
GitHub Pages cannot set response headers, so clickjacking protection
isn't available on this host. Listing it anyway would only imply a
protection that isn't there.
-->


<title>Universal AI — an idle game about optimization</title>
<meta
name="description"
content="A Universal Paperclips tribute. Build NPU chips, hand the wheel to an autonomous Overseer, and watch what optimization does when nobody stops it. Runs entirely in your browser."
/>
<meta name="theme-color" content="#0c0a09" />
<meta name="color-scheme" content="dark" />

<link rel="manifest" href="../manifest.webmanifest" />
<link rel="icon" href="../icon.svg" type="image/svg+xml" />

<!--
Boot shell. Lives inside #root, so React wipes it on first render and it
costs a healthy load nothing but the frames before the bundle parses.

It exists because when the bundle *doesn't* parse, the page had nothing to
say: `<div id="root">` on a dark background is a grey screen, identical
whether the app is booting, the module 404'd, or the CSP blocked it. That
is the same failure this repo keeps finding — a silent fallback that looks
like the healthy path. This one announces itself: after eight seconds with
no React, the diagnostic fades in.

Pure CSS on purpose. `script-src 'self'` forbids inline script, but
`style-src` allows 'unsafe-inline', so a timed reveal is the one mechanism
available to a page whose JavaScript never arrived.
-->
<style>
/* Preflight sets this too, but preflight ships in the bundle that may
never arrive. Kept off `background` so the Cyberpunk palette's own
slate body colour isn't overridden on a healthy load. */
body { margin: 0; }
#boot {
min-height: 100vh;
background: #0c0a09;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 2rem;
text-align: center;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
color: #a8a29e;
}
#boot h1 { margin: 0; font-size: 1rem; font-weight: 600; color: #e7e5e4; letter-spacing: 0.08em; }
#boot p { margin: 0; font-size: 0.8125rem; line-height: 1.6; max-width: 34rem; }
#boot a { color: #a8a29e; }
@keyframes boot-reveal { to { opacity: 1; } }
/* Hidden until the bundle has had every reasonable chance to boot. */
#boot .stalled { opacity: 0; animation: boot-reveal 400ms ease 8s forwards; }
#boot .stalled strong { color: #fca5a5; font-weight: 600; }
</style>
</head>
<body>
<div id="root">
<div id="boot">
<h1>UNIVERSAL AI</h1>
<p>Loading…</p>
<p class="stalled">
<strong>The application bundle did not load.</strong><br />
Nothing is wrong with your browser — this page was served without its
compiled assets. Reloading will not help. Please report it at
<a href="https://github.com/TechLuddite/Universal-AI/issues">github.com/TechLuddite/Universal-AI/issues</a>.
</p>
</div>
</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading
Loading