('ready');
@@ -765,48 +757,6 @@ export function BlockRunner() {
};
}, [apply, setFeed]);
- // Onchain high scores: poll the board, know our own best.
- useEffect(() => {
- let cancelled = false;
- const wallet = scoreWallet();
- // Deferred so the effect body itself does not set state synchronously.
- const t = window.setTimeout(() => setMyAddress(wallet.address), 0);
- const load = async () => {
- try {
- const [entries, mine] = await Promise.all([fetchBoard(), fetchBest(wallet.address)]);
- if (!cancelled) {
- setBoard(entries);
- setChainBest(mine);
- }
- } catch {
- /* Board is decoration; the next poll retries (or the chain regenesised). */
- }
- };
- void load();
- const id = window.setInterval(() => {
- if (!document.hidden) void load();
- }, 12_000);
- return () => {
- cancelled = true;
- window.clearTimeout(t);
- window.clearInterval(id);
- };
- }, []);
-
- const postScore = useCallback(async () => {
- const s = gameRef.current.score;
- if (s <= 0 || submitState === 'funding' || submitState === 'submitting' || submitState === 'confirming') return;
- setSubmitState('funding');
- try {
- await submitScore(s, (st) => setSubmitState(st));
- setSubmitState('done');
- setChainBest((b) => Math.max(b, s));
- setBoard(await fetchBoard());
- } catch {
- setSubmitState('error');
- }
- }, [submitState]);
-
const shareOnX = useCallback(() => {
trackBlockRunnerShare(shareVariant, score);
window.open(xShareUrl(shareText(score, shareVariant)), '_blank', 'noopener,noreferrer');
@@ -821,6 +771,28 @@ export function BlockRunner() {
else if (stage.requestFullscreen) void stage.requestFullscreen();
}, []);
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ const stage = stageRef.current;
+ if (!canvas || !stage) return;
+ const measure = () => {
+ const c = canvas.getBoundingClientRect();
+ const st = stage.getBoundingClientRect();
+ const scale = Math.min(c.width / WIDTH, c.height / HEIGHT);
+ if (!(scale > 0)) return;
+ setCanvasBox({
+ x: c.left - st.left + (c.width - WIDTH * scale) / 2,
+ y: c.top - st.top + (c.height - HEIGHT * scale) / 2,
+ scale,
+ });
+ };
+ measure();
+ const ro = new ResizeObserver(measure);
+ ro.observe(canvas);
+ ro.observe(stage);
+ return () => ro.disconnect();
+ }, [immersive, isFullscreen]);
+
useEffect(() => {
const onChange = () => setIsFullscreen(Boolean(document.fullscreenElement));
document.addEventListener('fullscreenchange', onChange);
@@ -830,7 +802,6 @@ export function BlockRunner() {
// Input.
const doTap = useCallback(() => apply(tap(gameRef.current)), [apply]);
const doRestart = useCallback(() => {
- setSubmitState((st) => (st === 'done' || st === 'error' ? '' : st));
apply(restart(gameRef.current));
}, [apply]);
@@ -1024,9 +995,6 @@ export function BlockRunner() {
aria-label={`Block Runner. ${phase === 'running' ? `Score ${score}.` : 'Tap space or the screen to start eating.'}`}
role="img"
/>
- {/* Once a submit starts, the board's own poll can raise chainBest past
- the score mid-flight — keep the prompt mounted on submitState so the
- progress and the success line survive their own success. */}
Latest block
@@ -1041,40 +1009,34 @@ export function BlockRunner() {
- {phase === 'dead' && score > 0 ? (
-
-
-
-
+ );
+ })()
) : null}
{immersive && !isFullscreen ? (
@@ -1099,89 +1061,6 @@ export function BlockRunner() {
) : null}
-
-
-
Onchain high scores
-
- {/* Once a submit starts, the board's own poll can raise chainBest past
- the score mid-flight — keep the prompt mounted on submitState so the
- progress and the success line survive their own success. */}
-
-
-
Latest block
-
- {head ? head.number.toLocaleString() : '———'}
- {slot ? {slot} : null}
-
-
-
-
-
-
-
-
- {phase === 'dead' && score > 0 && (score > chainBest || submitState !== '') ? (
-
void postScore()}
- disabled={submitState === 'funding' || submitState === 'submitting' || submitState === 'confirming'}
- >
- {submitState === 'funding'
- ? 'Funding wallet…'
- : submitState === 'submitting'
- ? 'Submitting…'
- : submitState === 'confirming'
- ? 'Confirming…'
- : `Post ${score} onchain`}
-
- ) : null}
- {submitState === 'error' ? (
-
- Submission failed — try again.
-
- ) : null}
-
-
-
- {board.length === 0 ? (
-
- No scores onchain yet — die proudly and post yours.
-
- ) : (
-
- {board.map((e, i) => (
- -
- {i + 1}
-
- {shortAddr(e.player)}
-
- {myAddress && e.player.toLowerCase() === myAddress.toLowerCase() ? (
- you
- ) : null}
- {e.score}
-
- ))}
-
- )}
-
-
- Your browser plays as{' '}
- {myAddress ? {shortAddr(myAddress as `0x${string}`)} : '…'} and the
- faucet funds its first submission.
-
-
-
How to play
@@ -1203,7 +1082,6 @@ export function BlockRunner() {
'Block height follows gas used.',
`Swallow a block to read its number and slot${head ? `, like ${blockLabel(head)}` : ''}.`,
'The beat runs at one note per block.',
- 'Scores live in a contract on vibenet, keyed by the wallet that posted them. A devnet regenesis wipes the board.',
]}
/>
diff --git a/app/vibenet/demos/200/contract/BlockRunnerScores.json b/app/vibenet/demos/200/contract/BlockRunnerScores.json
deleted file mode 100644
index 1457b03..0000000
--- a/app/vibenet/demos/200/contract/BlockRunnerScores.json
+++ /dev/null
@@ -1,107 +0,0 @@
-{
- "abi": [
- {
- "anonymous": false,
- "inputs": [
- {
- "indexed": true,
- "internalType": "address",
- "name": "player",
- "type": "address"
- },
- {
- "indexed": false,
- "internalType": "uint96",
- "name": "score",
- "type": "uint96"
- }
- ],
- "name": "NewScore",
- "type": "event"
- },
- {
- "inputs": [
- {
- "internalType": "address",
- "name": "",
- "type": "address"
- }
- ],
- "name": "best",
- "outputs": [
- {
- "internalType": "uint96",
- "name": "",
- "type": "uint96"
- }
- ],
- "stateMutability": "view",
- "type": "function"
- },
- {
- "inputs": [
- {
- "internalType": "uint256",
- "name": "",
- "type": "uint256"
- }
- ],
- "name": "board",
- "outputs": [
- {
- "internalType": "address",
- "name": "player",
- "type": "address"
- },
- {
- "internalType": "uint96",
- "name": "score",
- "type": "uint96"
- }
- ],
- "stateMutability": "view",
- "type": "function"
- },
- {
- "inputs": [
- {
- "internalType": "uint96",
- "name": "score",
- "type": "uint96"
- }
- ],
- "name": "submit",
- "outputs": [],
- "stateMutability": "nonpayable",
- "type": "function"
- },
- {
- "inputs": [],
- "name": "top",
- "outputs": [
- {
- "components": [
- {
- "internalType": "address",
- "name": "player",
- "type": "address"
- },
- {
- "internalType": "uint96",
- "name": "score",
- "type": "uint96"
- }
- ],
- "internalType": "struct BlockRunnerScores.Entry[10]",
- "name": "",
- "type": "tuple[10]"
- }
- ],
- "stateMutability": "view",
- "type": "function"
- }
- ],
- "bytecode": "0x6080604052348015600e575f5ffd5b506106608061001c5f395ff3fe608060405234801561000f575f5ffd5b506004361061004a575f3560e01c806312ca564f1461004e57806328ed6e3f14610063578063d53bb092146100a2578063fe6dcdba146100e2575b5f5ffd5b61006161005c36600461051b565b6100f7565b005b610076610071366004610548565b610446565b604080516001600160a01b0390931683526001600160601b039091166020830152015b60405180910390f35b6100ca6100b036600461055f565b600a6020525f90815260409020546001600160601b031681565b6040516001600160601b039091168152602001610099565b6100ea610475565b6040516100999190610585565b5f816001600160601b0316116101415760405162461bcd60e51b815260206004820152600a6024820152697a65726f2073636f726560b01b60448201526064015b60405180910390fd5b335f908152600a60205260409020546001600160601b039081169082161161019b5760405162461bcd60e51b815260206004820152600d60248201526c1b9bdd081e5bdd5c8818995cdd609a1b6044820152606401610138565b335f818152600a602090815260409182902080546bffffffffffffffffffffffff19166001600160601b03861690811790915591519182527f41962b14da4c981f83dac8e262111e9ad37614600f0ea9f119b03af61c544162910160405180910390a2600a5f5b600a81101561023e57335f82600a811061021e5761021e6105d4565b01546001600160a01b0316036102365780915061023e565b600101610202565b5080600a03610284576009546001600160a01b0316158015906102775750600954600160a01b90046001600160601b0390811690831611155b15610280575050565b5060095b604080518082019091523381526001600160601b03831660208201525f82600a81106102b2576102b26105d4565b82516020909301516001600160601b0316600160a01b026001600160a01b03909316929092179101555b5f8111801561033a57505f6102f26001836105fc565b600a8110610302576103026105d4565b0154600160a01b90046001600160601b03165f82600a8110610326576103266105d4565b0154600160a01b90046001600160601b0316115b15610442575f8061034c6001846105fc565b600a811061035c5761035c6105d4565b604080518082019091529101546001600160a01b0381168252600160a01b90046001600160601b0316602082015290505f82600a811061039e5761039e6105d4565b015f6103ab6001856105fc565b600a81106103bb576103bb6105d4565b8254910180546001600160a01b0319166001600160a01b03909216918217815591546001600160601b03600160a01b918290041602179055805f83600a8110610406576104066105d4565b82516020909301516001600160601b0316600160a01b026001600160a01b03909316929092179101558161043981610615565b925050506102dc565b5050565b5f81600a8110610454575f80fd5b01546001600160a01b0381169150600160a01b90046001600160601b031682565b61047d6104e2565b6040805161014081019091525f600a81835b828210156104d95760408051808201909152848301546001600160a01b0381168252600160a01b90046001600160601b03166020808301919091529082526001909201910161048f565b50505050905090565b604051806101400160405280600a905b604080518082019091525f80825260208201528152602001906001900390816104f25790505090565b5f6020828403121561052b575f5ffd5b81356001600160601b0381168114610541575f5ffd5b9392505050565b5f60208284031215610558575f5ffd5b5035919050565b5f6020828403121561056f575f5ffd5b81356001600160a01b0381168114610541575f5ffd5b610280810181835f5b600a8110156105cb57815180516001600160a01b031684526020908101516001600160601b0316818501526040909301929091019060010161058e565b50505092915050565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b8181038181111561060f5761060f6105e8565b92915050565b5f81610623576106236105e8565b505f19019056fea264697066735822122065c9501d2b15d374c83f39a8c53052366e0254c856e9a850d97efc826538ddcc64736f6c634300081c0033",
- "solc": "0.8.28",
- "optimizer": 200
-}
diff --git a/app/vibenet/demos/200/contract/BlockRunnerScores.sol b/app/vibenet/demos/200/contract/BlockRunnerScores.sol
deleted file mode 100644
index bfe512d..0000000
--- a/app/vibenet/demos/200/contract/BlockRunnerScores.sol
+++ /dev/null
@@ -1,51 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.24;
-
-/// Block Runner's onchain high-score board (vibenet devnet).
-/// No names — a score belongs to the wallet that submitted it, and only a
-/// wallet's best is kept. The top ten live in a fixed array, sorted on write
-/// so reading is one call. NOTE: vibenet is regenesised periodically; when
-/// that happens, redeploy with deploy.mjs and update the address constant in
-/// ../lib/leaderboard.ts.
-contract BlockRunnerScores {
- struct Entry {
- address player;
- uint96 score;
- }
-
- Entry[10] public board;
- mapping(address => uint96) public best;
-
- event NewScore(address indexed player, uint96 score);
-
- function submit(uint96 score) external {
- require(score > 0, "zero score");
- require(score > best[msg.sender], "not your best");
- best[msg.sender] = score;
- emit NewScore(msg.sender, score);
-
- // Find the sender's existing slot, or take the last one if they beat it.
- uint256 idx = 10;
- for (uint256 i = 0; i < 10; i++) {
- if (board[i].player == msg.sender) {
- idx = i;
- break;
- }
- }
- if (idx == 10) {
- if (board[9].player != address(0) && score <= board[9].score) return;
- idx = 9;
- }
- board[idx] = Entry(msg.sender, score);
- while (idx > 0 && board[idx].score > board[idx - 1].score) {
- Entry memory t = board[idx - 1];
- board[idx - 1] = board[idx];
- board[idx] = t;
- idx--;
- }
- }
-
- function top() external view returns (Entry[10] memory) {
- return board;
- }
-}
diff --git a/app/vibenet/demos/200/contract/deploy.mjs b/app/vibenet/demos/200/contract/deploy.mjs
deleted file mode 100644
index e236c82..0000000
--- a/app/vibenet/demos/200/contract/deploy.mjs
+++ /dev/null
@@ -1,31 +0,0 @@
-// Deploy BlockRunnerScores to vibenet and print the address.
-//
-// node app/vibenet/demos/200/contract/deploy.mjs
-//
-// Uses DEPLOYER_PK if set; otherwise generates a throwaway key and funds it
-// from the vibenet faucet. After a vibenet regenesis, run this again and put
-// the printed address into app/vibenet/demos/200/lib/leaderboard.ts.
-import { readFileSync } from 'node:fs';
-import { createPublicClient, createWalletClient, http } from 'viem';
-import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
-
-const RPC = process.env.VIBENET_RPC_URL || 'https://rpc.vibes.base.org';
-const FAUCET = 'https://api.vibes.base.org/api/vibenet/faucet/drip';
-const chain = { id: 84538453, name: 'Vibenet', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: [RPC] } } };
-const artifact = JSON.parse(readFileSync(new URL('./BlockRunnerScores.json', import.meta.url), 'utf8'));
-
-const pk = process.env.DEPLOYER_PK ?? generatePrivateKey();
-const account = privateKeyToAccount(pk);
-const pub = createPublicClient({ chain, transport: http(RPC) });
-const wallet = createWalletClient({ account, chain, transport: http(RPC) });
-
-if ((await pub.getBalance({ address: account.address })) === 0n) {
- console.log('funding deployer', account.address, 'from faucet…');
- const res = await fetch(FAUCET, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ address: account.address }) });
- if (!res.ok) throw new Error(`faucet: ${res.status} ${await res.text()}`);
- while ((await pub.getBalance({ address: account.address })) === 0n) await new Promise((r) => setTimeout(r, 500));
-}
-
-const hash = await wallet.deployContract({ abi: artifact.abi, bytecode: artifact.bytecode });
-const receipt = await pub.waitForTransactionReceipt({ hash, pollingInterval: 200 });
-console.log('BlockRunnerScores deployed at:', receipt.contractAddress);
diff --git a/app/vibenet/demos/200/lib/leaderboard.test.ts b/app/vibenet/demos/200/lib/leaderboard.test.ts
deleted file mode 100644
index 4d7641f..0000000
--- a/app/vibenet/demos/200/lib/leaderboard.test.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { parseBoard, shortAddr } from './leaderboard';
-
-const A = '0x1111111111111111111111111111111111111111' as const;
-const B = '0x2222222222222222222222222222222222222222' as const;
-const ZERO = '0x0000000000000000000000000000000000000000' as const;
-
-describe('shortAddr', () => {
- it('keeps only the ends of the address — that IS the name on the board', () => {
- expect(shortAddr('0x24411613aab0b4f551942f50af090941bd57e07d')).toBe('0x2441…e07d');
- });
-});
-
-describe('parseBoard', () => {
- it('drops empty slots from the fixed-size board', () => {
- const board = parseBoard([
- { player: A, score: 42n },
- { player: B, score: 7n },
- ...Array.from({ length: 8 }, () => ({ player: ZERO, score: 0n })),
- ]);
- expect(board).toEqual([
- { player: A, score: 42 },
- { player: B, score: 7 },
- ]);
- });
-});
diff --git a/app/vibenet/demos/200/lib/leaderboard.ts b/app/vibenet/demos/200/lib/leaderboard.ts
deleted file mode 100644
index 41cb655..0000000
--- a/app/vibenet/demos/200/lib/leaderboard.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-// Block Runner's onchain high-score board. No names: a score belongs to the
-// wallet address that submitted it, and only your best is kept. Each browser
-// gets a throwaway devnet key (localStorage); the first submission funds it
-// from the vibenet faucet. The contract keeps the top ten sorted, so reading
-// is one call.
-//
-// Vibenet is regenesised periodically. When the board vanishes, redeploy with
-// contract/deploy.mjs and update SCORES_ADDRESS below.
-import { createPublicClient, createWalletClient, http, type Address, type Hex } from 'viem';
-import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
-
-import { VIBENET_RPC_URL } from '../../../library/config';
-import { vibenetApi } from '../../../library/client';
-import artifact from '../contract/BlockRunnerScores.json';
-
-export const SCORES_ADDRESS: Address = '0x24411613aab0b4f551942f50af090941bd57e07d';
-
-const KEY_STORAGE = 'block-runner:pk';
-
-const chain = {
- id: 84538453,
- name: 'Vibenet',
- nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
- rpcUrls: { default: { http: [VIBENET_RPC_URL] } },
-} as const;
-
-export type BoardEntry = { player: Address; score: number };
-
-const ZERO: Address = '0x0000000000000000000000000000000000000000';
-
-/** Drop empty slots and normalize the fixed-size board the contract returns. */
-export function parseBoard(raw: readonly { player: Address; score: bigint }[]): BoardEntry[] {
- return raw
- .filter((e) => e.player !== ZERO && e.score > 0n)
- .map((e) => ({ player: e.player, score: Number(e.score) }));
-}
-
-/** `0x1234…abcd` — the whole identity on the board. */
-export function shortAddr(a: Address): string {
- return `${a.slice(0, 6)}…${a.slice(-4)}`;
-}
-
-function pub() {
- return createPublicClient({ chain, transport: http(VIBENET_RPC_URL) });
-}
-
-/** The browser's throwaway score wallet (created on first use). */
-export function scoreWallet(): { address: Address; pk: Hex } {
- let pk: Hex | null = null;
- try {
- pk = window.localStorage.getItem(KEY_STORAGE) as Hex | null;
- } catch {
- pk = null;
- }
- if (!pk || !/^0x[0-9a-fA-F]{64}$/.test(pk)) {
- pk = generatePrivateKey();
- try {
- window.localStorage.setItem(KEY_STORAGE, pk);
- } catch {
- /* Ephemeral key: scores just will not persist across reloads. */
- }
- }
- return { address: privateKeyToAccount(pk).address, pk };
-}
-
-export async function fetchBoard(): Promise {
- const raw = (await pub().readContract({
- address: SCORES_ADDRESS,
- abi: artifact.abi,
- functionName: 'top',
- })) as readonly { player: Address; score: bigint }[];
- return parseBoard(raw);
-}
-
-export async function fetchBest(address: Address): Promise {
- const raw = (await pub().readContract({
- address: SCORES_ADDRESS,
- abi: artifact.abi,
- functionName: 'best',
- args: [address],
- })) as bigint;
- return Number(raw);
-}
-
-/**
- * Submit a score from the browser's throwaway wallet. Funds it from the
- * faucet on first use. Resolves to the tx hash once mined.
- */
-export async function submitScore(score: number, onStatus?: (s: 'funding' | 'submitting' | 'confirming') => void): Promise {
- const { pk } = scoreWallet();
- const account = privateKeyToAccount(pk);
- const client = pub();
- if ((await client.getBalance({ address: account.address })) === 0n) {
- onStatus?.('funding');
- await vibenetApi.faucet.drip({ address: account.address });
- const until = Date.now() + 20_000;
- while ((await client.getBalance({ address: account.address })) === 0n) {
- if (Date.now() > until) throw new Error('The faucet did not fund the score wallet in time.');
- await new Promise((r) => setTimeout(r, 500));
- }
- }
- onStatus?.('submitting');
- const wallet = createWalletClient({ account, chain, transport: http(VIBENET_RPC_URL) });
- const hash = await wallet.writeContract({
- address: SCORES_ADDRESS,
- abi: artifact.abi,
- functionName: 'submit',
- args: [BigInt(score)],
- });
- onStatus?.('confirming');
- // Replacement detection would cost an extra round trip per poll; a fresh
- // throwaway key never replaces anything.
- await client.waitForTransactionReceipt({ hash, pollingInterval: 200, timeout: 30_000, checkReplacement: false });
- return hash;
-}