Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Samples

arcade

A collection of small arcade games — the demo that pushes the UI engine hardest.

21 source files1 test file

The arcade sample, running.
Running, signed in, compiled from the source below.

Get it

$ osy init arcade
$ osy launch

The app

app.osy7 lines
// A collection of small arcade games — the demo that pushes the UI engine hardest.
// Built to find where the engine stops.
app Arcade {
  model "model/**/*.osy";
  tests "tests/**/*.osy";
}
model/auth.osy36 lines
// SIGNING IN — two functions and the bootstrap that arms them.

/// Verify the password against the stored hash, then issue a signed ticket. No match → no ticket, so the client
/// stores nothing and stays anonymous — which on this app means "can still play, cannot post a score".
[AuthMethod]
string Login(string handle, string password) {
  var p = Player.Where(x => x.Handle == handle).FirstOrDefault();
  if (p == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, p.PasswordHash)) { StampSeen(p); return Security.IssueJwt(p.Id, p.Handle); }
  return "";
}

/// Stamp the last-seen timestamp on a SUCCESSFUL authentication. Runs as the armed Authenticator; on signup the row
/// is being created, so this writes under the create grant instead.
void StampSeen(Player p) { p.LastSeenAt = DateTime.UtcNow; }

/// Sign up and sign in. Every signup gets the `Player` floor so no account is a role-less dead end — a principal
/// with NO grant is denied by RLS everywhere and cannot even read its own row.
[AuthMethod]
string Signup(string handle, string password) {
  var p = new Player {
    Handle = handle,
    PasswordHash = Security.HashPassword(password),
  };
  var grant = new RoleGrant { Player = p, Role = Role.Player };
  StampSeen(p);                      // signing up IS signing in, so it counts as the first
  return Security.IssueJwt(p.Id, p.Handle);
}

app.AuthBootstrap = new AuthBootstrap {
  Role      = Role.Authenticator,    // the ephemeral principal login/signup run as
  Login     = Login,
  Signup    = Signup,
  LoginPage = LoginPage,
};
model/chrome.osy127 lines
// THE ROOM'S FURNITURE — the pieces the lobby is assembled from.

// ── A MACHINE'S LOOK ────────────────────────────────────────────────────────────────────────────────────
class Look {
  public string Accent;      // the neon this machine burns
  public string Glow;        // the same colour at low alpha, for wash and bloom
}

/// A single bulb on a marquee rail. `phase` is WHEN in the chase it lights — one animation, offset per bulb.
[Composable] component Bulb(int phase) {
  render {
    Box(w: 9, h: 9, rounded: Radius.Pill, bg: Colors.Bulb,
        shrink: 0,
        shadow: "0 0 10px 3px rgba(255,233,168,0.45)",
        animation: BulbChase, animationDelay: (phase * -400) + "ms");
  }
}

/// A rail of chasing bulbs, as it runs along the top and bottom of a sign.
[Composable] component BulbRail() {
  var phases = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3];
  render {
    Row(gap: 0, justify: Justify.SpaceBetween, w: "100%", px: 2) {
      foreach (var p in phases) { Bulb(p); }
    }
  }
}

/// A word burning in neon.
[Composable] component NeonWord(string word, string glow, string size) {
  render {
    Text(word,
         fontFamily: Font.Display, fontSize: size, fontWeight: FontWeight.Black, color: Colors.OnBg,
         letterSpacing: "0.08em", textTransform: TextTransform.Uppercase, whiteSpace: WhiteSpace.Nowrap,
         animation: NeonFlicker,
         textShadow: "0 0 4px " + glow + ", 0 0 14px " + glow + ", 0 0 38px " + glow);
  }
}

/// The hall's own sign, over the entrance: a bulb-framed board with the name burning inside it.
[Composable] component Marquee() {
  render {
    Stack(gap: 3, align: Align.Center, w: "100%", maxW: 760, mx: "auto",
          px: 5, py: 4, rounded: Radius.MarqueeR,
          bg: Colors.Carpet, borderW: 2, border: Colors.Pink,
          bgImage: "radial-gradient(120% 160% at 50% 0%, rgba(255,45,149,0.28) 0%, rgba(26,11,46,0) 62%)",
          shadow: "0 0 40px rgba(255,45,149,0.35), inset 0 0 60px rgba(255,45,149,0.14)") {
      BulbRail();
      NeonWord("Arcade", "#FF2D95", "58px");
      Text("· OSY# · PLAYER ONE READY ·",
           fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Cyan, letterSpacing: "0.42em",
           textTransform: TextTransform.Uppercase, textAlign: TextAlign.Center);
      BulbRail();
    }
  }
}

[Composable] component Cabinet(string title, string tagline, Look look, string route, bool playable) {
  action Play() { if (playable) { Navigation.Go(route); } }

  render {
    Pressable(onClick: Play) {
      Stack(gap: 0, w: "100%",
            rounded: Radius.Cab, overflow: Overflow.Hidden,
            bg: Colors.CabinetBody, borderW: 1, border: Colors.CabinetLip,
            cursor: playable ? Cursor.Pointer : Cursor.NotAllowed,
            opacity: playable ? 1 : 0.62,
            bgImage: "linear-gradient(160deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0) 38%), linear-gradient(0deg, " + look.Glow + " 0%, rgba(0,0,0,0) 55%)",
            shadow: "0 18px 44px rgba(0,0,0,0.65), inset 0 0 0 1px rgba(255,255,255,0.03)") {

        Box(w: "100%", px: 4, py: 3,
            bg: Colors.Ink,
            bgImage: "linear-gradient(180deg, " + look.Glow + " 0%, rgba(0,0,0,0.85) 100%)",
            borderBW: 2, border: look.Accent) {
          NeonWord(title, look.Accent, "19px");
        }

        Box(position: Position.Relative, w: "100%", h: 150, bg: Colors.Screen, overflow: Overflow.Hidden,
            shadow: "inset 0 0 34px rgba(0,0,0,0.95), inset 0 0 70px " + look.Glow) {

          Box(position: Position.Absolute, inset: 0, animation: ScreenBreathe) { Slot; }

          Box(position: Position.Absolute, inset: 0, pointerEvents: PointerEvents.None, z: ZIndex.Above,
              bgImage: "repeating-linear-gradient(180deg, rgba(0,0,0,0.42) 0px, rgba(0,0,0,0.42) 1px, rgba(0,0,0,0) 2px, rgba(0,0,0,0) 3px)");

          Box(position: Position.Absolute, left: 0, right: 0, top: 0, h: 18,
              pointerEvents: PointerEvents.None, z: ZIndex.Above, animation: ScanSweep,
              bgImage: "linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.09) 50%, rgba(255,255,255,0) 100%)");
        }

        Row(gap: 3, align: Align.Center, justify: Justify.SpaceBetween, px: 4, py: 3,
            bg: Colors.CabinetLip,
            bgImage: "linear-gradient(180deg, rgba(255,255,255,0.07) 0%, rgba(0,0,0,0.35) 100%)") {
          Row(gap: 2, align: Align.Center) {
            Box(w: 22, h: 10, rounded: Radius.Pill, bg: Colors.Ink, shadow: "inset 0 1px 2px rgba(255,255,255,0.12)");
            Box(w: 13, h: 13, rounded: Radius.Pill, bg: look.Accent,
                shadow: "0 0 10px " + look.Accent + ", inset 0 -2px 3px rgba(0,0,0,0.5)");
            Box(w: 13, h: 13, rounded: Radius.Pill, bg: Colors.Ink, borderW: 1, border: look.Accent,
                shadow: "0 0 6px " + look.Glow);
          }
          Text(playable ? "Insert Coin" : "Coming Soon",
               fontFamily: Font.Mono, fontSize: FontSize.Micro, fontWeight: FontWeight.Bold,
               color: playable ? look.Accent : Colors.Dim,
               letterSpacing: "0.24em", textTransform: TextTransform.Uppercase, whiteSpace: WhiteSpace.Nowrap,
               animation: playable ? CoinBlink : ScreenBreathe);
        }

        Box(px: 4, py: 3, bg: Colors.Ink) {
          Text(tagline, fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, lineHeight: 1.6);
        }
      }
    }
  }
}

/// A CABINET BUTTON — the arcade's own look: the glow, the display face and the letter-spacing are the cabinet.
/// The kit's `Button` is the general control; this is a different one, not a replacement for it.
[Composable] component ArcadeButton(string label, string glow) {
  render {
    Box(px: 5, py: 3, rounded: Radius.Chip, bg: Colors.CabinetLip, borderW: 2, border: glow,
        shadow: "0 0 18px " + glow + "55") {
      Text(label, fontFamily: Font.Display, fontSize: FontSize.Small, fontWeight: FontWeight.Black, color: Colors.OnBg,
           letterSpacing: "0.16em", textTransform: TextTransform.Uppercase, textShadow: "0 0 10px " + glow);
    }
  }
}
model/identity.osy51 lines
// WHO IS PLAYING — the principal, the role vocabulary, and the grant table.

/// The role vocabulary.
[Role] enum Role {
  Authenticator,
  Player,
}

/// The BY-SHAPE grant table: an entity with both a reference to the `[Principal]` and a `[Role]`-typed property IS
/// the grant table — no marker, no grammar. The platform discovers it structurally.
entity RoleGrant {
  [Required] Player Player;
  [Required] Role Role = Role.Player;
  security {
    allow read where Player == user;              // you can see your own
    allow create when IsAuthenticator;            // signup seeds the Player floor, leashed by Signup's body
  }
}

/// The one predicate the rules below compose from. A security rule cannot call a parameterized policy, so the named
/// form is what exists.
policy IsAuthenticator => RoleGrant.Any(g => g.Player == user && g.Role == Role.Authenticator);

[Principal]
entity Player {
  /// The LOGIN field, so it must be unique — otherwise `Signup` mints a second account for a handle that already
  /// has one and the duplicate is a dead end. The database refuses it and the create raises with this sentence.
  [Required, MaxLength(40), Unique("That handle is taken — pick another.")] string Handle;

  /// Salted hash, never the plaintext. Readable ONLY by the armed authenticator (the field mask below).
  [MaxLength(200)] string PasswordHash;
  /// Stamped on every successful authentication. Null = never signed in.
  DateTime? LastSeenAt;

  [ForeignKey(Player)] RoleGrant[] RoleGrants;

  security {
    allow read when IsAuthenticated || IsAnonymous;
    allow read when IsAuthenticator;               // login/signup verify a credential
    deny read PasswordHash when !IsAuthenticator;  // ONLY the auth flow ever reads the hash (field mask)
    deny read LastSeenAt when !IsAuthenticated;
    allow create when IsAuthenticator;             // signup
    allow update where Id == user.Id;              // you may touch your own row
    // `StampSeen` on the LOGIN path writes this column on a row that already exists, so the create grant above does
    // not cover it and the `where` filter cannot: the armed principal has no user, so `Id == user.Id` matches
    // nothing and the commit was refused — signing up worked and signing back in did not. Property-scoped, so the
    // auth flow gets this one column and nothing else.
    allow update LastSeenAt when IsAuthenticator;
  }
}
model/rules.osy48 lines
// THE RULES OF BLOCKFALL, AS PLAIN FUNCTIONS — the part of a game that is worth testing, separated from the part
// that can only be watched.

/// How many rows are completely full — the count the score table is paid on.
int FullRowCount(int[] well) {
  var full = 0;
  for (var r = 0; r < 20; r = r + 1) {
    var complete = true;
    for (var c = 0; c < 10; c = c + 1) { if (well[r * 10 + c] == 0) { complete = false; } }
    if (complete) { full = full + 1; }
  }
  return full;
}

/// The well with every full row removed and everything above it shifted down, empty rows added at the top.
int[] CollapseRows(int[] well) {
  var kept = new List<int>();
  var cleared = 0;
  for (var r = 19; r >= 0; r = r - 1) {
    var complete = true;
    for (var c = 0; c < 10; c = c + 1) { if (well[r * 10 + c] == 0) { complete = false; } }
    if (complete) { cleared = cleared + 1; }
    else { for (var c = 0; c < 10; c = c + 1) { kept.Add(well[r * 10 + c]); } }
  }
  if (cleared == 0) { return well; }

  var next = new List<int>();
  for (var i = 0; i < cleared * 10; i = i + 1) { next.Add(0); }
  for (var r = 19 - cleared; r >= 0; r = r - 1) {
    for (var c = 0; c < 10; c = c + 1) { next.Add(kept[r * 10 + c]); }
  }
  return next;
}

/// The classic table: a tetris is worth far more than four singles, which IS the risk/reward the game is about.
/// Flat per-line scoring removes the only interesting decision it has.
int LineScore(int cleared, int level) {
  if (cleared <= 0) { return 0; }
  if (cleared == 1) { return 100 * level; }
  if (cleared == 2) { return 300 * level; }
  if (cleared == 3) { return 500 * level; }
  return 800 * level;
}

/// One level per ten lines. Level multiplies the score AND shortens the drop interval, so it is the difficulty
/// curve and the reward curve at once.
int LevelFor(int lines) { return 1 + lines / 10; }
model/scores.osy26 lines
// THE HIGH-SCORE TABLE — the reason to sign in.

/// Which cabinet a score came off. An ENUM rather than a string: a typo in a game name would silently open a second
/// leaderboard nobody can find, and the platform's own guidance is that a constrained value is never a bare string.
enum Game {
  Blockfall,
  OsyBirds,
}

entity Score {
  /// Whose it is. The create rule below pins this to the signed-in player, so a score cannot be posted in somebody
  /// else's name.
  [Required] Player Player;
  [Required] Game Game = Game.Blockfall;
  [Required] int Points;
  int Lines;
  int Level;
  /// When it was set — the tie-breaker, so an equal score that came first ranks first.
  [Required] DateTime AchievedAt = DateTime.UtcNow;

  security {
    allow read when IsAuthenticated || IsAnonymous;
    allow create where Player == user;
  }
}
model/textures.osy13 lines
// THE CORRIDOR'S WALL TEXTURES — Kenney "Prototype Textures", CC0 (public domain).

/// The wall material for a map cell value. A top-level function — the platform's own idiom for a shared helper
/// (`demo/agent-expenses/model/auth.osy` declares `string Login(…)` the same way), and it keeps the ~7KB of base64
/// out of the page that uses it.
string WallTexture(int kind) {
  if (kind == 1) { return "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAABGdBTUEAALGPC/xhBQAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABGUUKwAAAETklEQVRoBe1aS2/UMBAeZ9N22yKVt0Q5w4UT54q/wa/hyK/hxI8AjpzKAaniQKGCQ9/V9rG7Md94kjTxTFDLYzepYq2yzpfP8/DYY6837ufHbOe9T1KKiyPyMUYAUTQOUPM9DYZMn56Fp1wtSpOc3+BKaTah9Q2X7rzzH15NB8uFWSKCKEnIqzaw0jnyWWFH9Vs5ANrwLjPO9sglVWrhj5IPGivVOJQqcDyijdeDNFmgdJmGD4iD4Nk4eAZ2dlFXKXfXdACSUdKV/+IAJLPx+JqO6eFz9/RlMvpBySIdfvEXh37rref+ZgNCuawVSDu+2QH0/e6m3zzK/JTcgNIhZVNauk3ZOI81wsLjuJUlTF70rqOTbx4jZ7BIk2ArvEKdXUrYn1Y7ACtXHzlYvLDKEwC2Lq6x9WHC+pPvtPvJY7S1s3AE0Mfn+zzVzg8pGSAC/nSPbq1zWkEehD8yqNrqQMhQw3v0+IU7P6CFFTraduf7HpPh/jM3PqU7T2h87D6/yeCbpNHK1K44pWd5SFncQUXlkq3J4RmYZjFxBh2ykOeUf7DlEQSHu4wmI39xzDP7+CvnKRCA5Bk6iNcpOddaT9WgCZOv9Ucmn0FpYpG1UhGbT2KMk+NtnsTiFq5+TCcnFSRaiXIT5v8VHIAZiPKgPjQ0Mn9rDQva2rGGqTbUO2D3y+zQy5U41mlmtJC56nOlaKf5QsbzslJwbQnCLDnVihZekFPIQorUiVYjLFAyerO4qtKcjEbSqvrMlBCYVVZZN42B2VDB6wC2Ekj/cbG24GiAj0FGYzyq52/QhGnwxYE6XwywFw3LGJgNjf0kjgM36/s+ArPu8VjfzYhAQ1KLnW3bfTCb1wEu2geNCA1406Mg6fJSMstK+cyUIPJLTrXSzOd1IP9UG0jdytP85Iq4SBa+2USD0kTjptJAvhlzgHu1q6WPwLwjFzZzxa6rZoy1f+L8057NHDagnlKc/OAMWU5hqw5gB6sPAmA943rripbwrZ490HxpLYhs2k7X+SzjOqfTkyEf33d+DqQ4UMH5PY7lovIPIhBCDLFnOHGKOkoWpr+MwCicGvKowC+ySAGiqYYEe6gHQ+m3xRexhvxmB8xV1TSGhbvuDyHV82WPdqTSOzDvQHX+XKjzQyhEIGysjbGg8nTOuSJeii0rkQ4tR5gaR0MNBqTzEegdiEbFzG/7CMy8yyOFxfG6ioS5f2rhZi78Iiv+D42cM37QBEYTHmU60ITJV50EIUqDf/g369UViItasYmXYsuK0MqrliNMjaOJBgOihk4pvSOV3oF5B6qPwLwjENYB66c6rwO6CLPhUUwvxEJULM2UICcJsRS+j5sHDoP536xFwo7aNuX7JjzKdKAJk686CUKZBq+/DvRzIIrazG/7CMy8yyOFnY9Aitdz8Ra1LkhSRrYJCfGqx+t4fzC8vT4ZqbNXSaMqC+G400xZpjEwG8a7rr9+/wtkJLlJW2SLLwAAAABJRU5ErkJggg==)"; }
  if (kind == 2) { return "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAABGdBTUEAALGPC/xhBQAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABGUUKwAAAEzElEQVRoBe1aTY8TRxB9Y88sNiFBSSQOSIHc4cSJYy6bH5IfkkN+Cdf8Bi65ceKEiHIBARJIfGg/CGBn7fXwqmuwd6qqV14peGxpWt5Z9+vqVx/dXd1uu6hfPcDzvzCAKwVQRyDxRYQTc/LlWCTnk3XlxQ6SOB5ExtCKm7+UeH4f939H6VQolYNDKpHyKmjGeCRNk6m0rlM8ifYK8Rmw/0eJQYUKuHwFAzpRY3GKeo56gVO2R5EoBqhPnTFFcqAtz1p1WSTn3gF1qC1PyWIoqr3eokDthCk/qFLkac/1u7j9Gz68xPASDv7Bf4d4/CeKet3IiZndlOQAZ8vrh5geSew5Dpy4iznGV7E4aYxiVIJ53I3FRuty7hd4/0yGqbyE2ScRGlYY7sls4ZyhP1vtAGfXdzck9tWVZOsUo6sYjqAz7/gZ3jzG0Hi+LdU0AlxRk3eovsH0UJbR/BMmb/HtT2JjOcLpFNXeajpti+WNHXSAC7zA+Bp+/hXTA3Hj+CkmhxgUuHYHs4/48RZO3uPRPUEozGEJdo2UhWz2YA5I2YZPfbNyPuFWnvRK7hKO4B4kUtCBGoMaB39j+k5SKtPo7ANO/pUZdfQEgz0RoBvMSJLIkgLJdKYkg0ymo7gi8uTrbEnyFqQAoxmlUcENg1LWXxYxU/XRi0YDyfliCjr+2ELOGrA17784QIOYTFulkJHZ+mKt3nqDrYG9AzYim67rGtCcYHTrWg5BL+8R7ai4b/XIUj5sCkHpUkrq1ZcyLJ88QQSFwsT94g73AWazRMKnNUDrjqdR6nHKe1D2mVLyK1Ova5UOcb4nHh2n6a1J1eRUyZx8pPVi+0Bdh2EOIr+1UO9A10PTj0DXI7A8C9k8lw50OTDEQ1dU8kLyXpjMISipkw7oPuDUh11yVNr7nC65JqM2Jxbi8iFBPw/IVmCYWCUYoFmcbEZeeicGg69UeX5V6nFH3nD3+8Aqmh2969NoR4FfqmUWSis4WDaZEx6vpgPhBBmcVT0OGlyUB1CyieRhU5RmRJAf6nmROP5B7ndN4ck2PI0KHp1Gwyw0+j6xrn+cvsjlLoNfjnd+DZRy6Tk5wNzEP+194WBy+whHWUagTSLVdIM0OYp30ra41NYnpzDv/+cT3YmTuYZOPhxFF1iCZ6aQ9YC0aYRlx7TsqW48VvnoYks2XS9MDlVgyHequvNroHeg6/mWFrFbYsmqYOmlxZjDQ1d08erzrIBHtDUkZ1NWnlOIq9st8LO6tve9WM4R0OJ9yDmWw8kTkoT4+Uo9T5akX8Qayu6e/Qh0F3vVvPMjoNfrPK+7RCvHJAcS8ZISivB6ndffKUB82l7K7LJNo9TjlPfg6no9c4MSfqDhl4ExThVtHawpIs92UxMaA6ZACHmIO5BA3ewDrk2CKu3y35YcTjkvr4jHldTjRPRltUbkIrPza6B3wA/1ZpF+BDYbb69NP9RrFm+3SubO7QMh3u6uNU3/fNoett50jpXSkEhesP/xep1s5+0DjYXtf1EaDTaN1MmQizr569dAO6Sbr/UjsPmYtzXu/AiUWMzkltSXIvM9QMEfV3tpnmzdcYtiZfoNLfltGgwohDQmJx59P0DaxazEzX3sV9GhzlskGtIruvQV/c6sr//z+8+QsJ8sSstGSAAAAABJRU5ErkJggg==)"; }
  if (kind == 3) { return "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAABGdBTUEAALGPC/xhBQAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABGUUKwAAAEuElEQVRoBe1ay24TMRS1ZyZpHpVaVAGLSuwqVqwQW8QvsONT+AW+hB38A1uEkBC7sqA8VaEmtCXNYzIejn0zzsy1EyWCZDLSjKLp9fHxffhx7biR74ZnbwanDRkK/kghUo4JIYVMfbiuKeJo3Aoa0DBSMeqKDydT7erKwY/T5HH3JIL3z89ft4OGdZaMBSJwHUUVbKiio9BlmvDAQDsKu6i9SAYBUchNowTRWosZLECDURf3BjZQkxd3n0bo+3bQvB3tRyJEY1hNUoX3JJ1avVZYNwBoRttOGm8iAGiG8xH+xOn0Yeves4NHP6eXezL6NPl1mQxfXb2XNBba/Zyoizv06AAiGX4Yf7/qj6apCmXQlg0Ih2FnKhLyVKXpKI13yOucKzoAmhhf4z6mUFNGQzUBFslgT4SJSDH6iVSjZIcDmAp1HB1gHLpBE30/TOPDoI1IaOl8i/sfxz98aSrXD+WJegTQx73kphM0fyfDUMqhinticNw4RNWebIzSqZ5UQpXn5DLL6GadFI/C/Sfdk35yg0E4i3sQQhE8aB0P1Ph+8851On55+RYITbb52s40G4TDRAYFI4lPxtV/WdFWrYUbtSKC96GQp5PzvhogkyqhkF+v1Qjy5/iiKZFbxY2aUIYmS+7+4MVNRtc53QjWSSt48j3YC8gaty1JICYtYokk82XSA0V3Gx4hY5H8iUcWYYmc6SqxqAPAA48x+0mmt0GCPLKbcgVcXN5xdQDL+2fztXYn5qYKCyKr1EvcfDJg9ncJGQy3iZdPTKaZil4+gXq7xckZb9bSRcgAcE6dhcRhzE5SggzGtLGiNb0WTm5jH0gToc/PVgsJcMdNvQDx8ZLRivFBI6Y5UBXUm1g9+wAp4a4Yo0w5mHAbYL2ICz1bQqEegRI6vWCy8iPwf/YB9IlJLIW+sQgEKxODFW2ztXAi6+8DRr/b1kUWWs+UWE9mQpbXUc+0saJtuBaudyT9fQDZ1M2yUOkFV8etWiNYF63g2QcyZyxnLrjOkNrKr4E6gPkglyNVfgT0IranrnwfYoG76wYgPuse5hbwPYsYDnhXvNcZqIXzUUtGuEOmW1gngDyg5SwADw4I6vIPDNwKOoSwOwHowcP4QDAfALq4CcC0yb1wWwXnqz+FcPGG+3vcgOdi0yL2oAVTaI3/D5DO3mb+P4DbKjivd2KMLxviLAByYP42U4jGfw4asn65Q09qXf20Q3v5C6aQX7lxvuBJ9QqVXwN1AGVPutnlbnbunbvjIqijReyuYoNwmMim1UauVchDutz1Ln2YdvMERbgibtV6+V5wLaNagw7A2iHv7PsfLVi1VrCaSXD1E9PFwXdBQupFzHp168V6BLbe5cxg5UeArtere5iT+nrdJC83TSFzeUCM4Iq40aw1kAk29IBd7UAWkD1GiUz7gEeXMewYNYBrmHgMNwZ0jRWIZt+Mb5kuTlW2IQnUj5VfA3UAbFy3XqxHYOtdzgzqL/U4WLunfxdBy4zMlGjcVBbwLXwfgIn6er3Q6SUU6kVcQqcXTNYjUOiOEgoRfgOOX/m5lpFG6bSUr6LMuOC+n/NBww04mtNvBot6oMlzgsQtqvc06nUGbsN5WfWf3/8FHdAM6TXOx+MAAAAASUVORK5CYII=)"; }
  if (kind == 4) { return "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAABGdBTUEAALGPC/xhBQAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABGUUKwAAAFD0lEQVRoBe1aPY/bRhB9pKg7XezYuBiI3SSAqxRJlSJx6cpd/kR+RaoU6fwv0rp1ZcCNuwAJUgUJjAD5RA4ubFjn88k6+06k38yQsrgzNKIAOYkAF4JEPr6dndnZnZ1dKqu++wkPvgfGSEoGVAkEEOSnjHBiKb/C3q5Q5y+12kotCmFJ+UCuoMdDZXCKm58VuP8Dvr4NTLSqCbaWTJhdL79J4MdbQIJvpMRkX2qeTFW1pRAj89trupYFc3zzVYFxIdpfvIJiJAIXJc4WKEucngbyswx5JpykiOEZqkShEu+wX2jAXmoA5bCkfGCUo6wCnHxPpoRxQe1ZznDjE3z5BQ4eY3eMh39h+hx37qHSZpSRjoEa3PyPGVDgx4c4PJa+px84cHlx+RJendV60yEyjrexmAHUM8Ofj8RN9MCLE9GbQ2tnB4sF8hxn227AAh9eBYfTxT3p+5OXuPwuJmNkuZhEw37+FWhM3TI3mFo5njzDhT1MjzAaiQceT/HBNVF1siv27Ozi1WLLNK/VKWTwZBXe38etz/H0SMz4/QDTZ8hG+PQjzOb4+DqOZvj2riBC1kCUWFNHoTbKGEAyi9VafWh4ErSMGUdvRqHV+nqt8gsZJFWOX/4QJ4xHEiKP53h+LCPqt3+wMxbC7EQ4vKA+bICRLikWrkholSYgShPtRzGfplYdYTSKubSpqmwIZTJO/j5oWlc1GYJmsxbS3GzV73JqUulRSzN6uWojrcfbcsMB0e8yGLBp/+kckKBmcWFFHYl9K7d2aaDw20UABxIxzMv3Ekwe+eGjEFTptg40AXtVK2advlAQ8aorG00qkKxDVES1pZlC7dAqlYXP4JjIUas8+GYdYK7mU3w+DuI9tV8nnbbEO0i/1Z5kcTC110inS64DwyR23j5nYPDAOXe4a673HmhyoXaUEztd6HsDhmTXNzXfRCVPvQQjhI2GEghqYOU6YGuHF+kRk/TvG1kylxemZpdGa+JcTDJuFGmHxGO3TtT4ssnmogvn8zSuN9uAUH7AVwkkp3K06QCUJa/3c2AwoBlZm/rtvQd0Uy+ZnEswS4YOP7OZ4RF3ZCF6ftmkg55PMouTT2I445lBejLVkE09DxLf4xmynsKq1PqLOXCYjRIPsktWYqKaKFRi/5JIy4m3XV2n0wl/3cPdCZVvy61179NPIae2T3l+zxPwpIRe062P39BIVc/nENKhcniYekDILM4Ddpjp8UA4q8+pvO7IxL/OFW8bQtr66pfo44cQpZqiTn7XEOKObI0NTc79p9N7Va0+XA8GbNpLvfdAdzqtyWravwJGZzA2iR27PuSRKWuzuWEIEhUR3oEHsJB5aB5Es4C8hZBqbjuyKAWXxy5O04wuXB4l/EZsmB0EfJVAcipHOy8Apbnez4HBAHXvBr8GD2yw87VpTeaYQvmkiHkY3xkmJVwEyJHg7ZI51q6P1518Wwd8YBFNoihEvifzZDrL7DVrVIcqBRsajVshTn7aRiOWePLIVqsElE74b69ZRZDrbMEcKFp24G83wMsP+CqZjcbtemUEGSYxO2GjZfDARrufjffeA7oOSFR2abiEfNe9AuoneSLMgP3/7weW64APc13hsgunCWn4awKi4BLy0pLy1w+jw2vWtE/P/773k3gw4PwHTbvFAqf8e+68Depd1/uBBcOlP+9nFeJJqCnxwk7tKb/tajnvZ0n4/Oc2aVHIit8PzKl81ve/378GGJL2S8932n8AAAAASUVORK5CYII=)"; }
  return "none";
}
model/theme.osy91 lines
// THE ARCADE'S DESIGN VALUES — a dark room lit by its own machines.

theme Arcade {
  Mode = Dark;

  Colors {
    Bg          = "#07030D";
    OnBg        = "#F2E9FF";
    Floor       = "#0E0719";
    Carpet      = "#1A0B2E";

    CabinetBody = "#150B22";
    CabinetLip  = "#251340";
    Screen      = "#04020B";

    Pink        = "#FF2D95";
    Cyan        = "#22E5FF";
    Amber       = "#FFC24D";
    Lime        = "#4DFF6E";
    Violet      = "#B45CFF";

    Bulb        = "#FFE9A8";
    BulbOff     = "#5A431C";

    Dim         = "#8B79A8";
    Ink         = "#050209";
  }

  Radius   { Cab = "16px"; MarqueeR = "10px"; Chip = "6px"; Pill = "999px"; }

  FontSize { Micro = "10px"; Tiny = "12px"; Small = "13px"; Body = "15px";
             Head = "19px"; Big = "30px"; Huge = "58px"; }

  FontWeight { Bold = "700"; Black = "900"; }

  Font     { Display = "Impact, 'Arial Black', 'Haettenschweiler', system-ui, sans-serif";
             Mono    = "'Courier New', ui-monospace, SFMono-Regular, monospace"; }

  ZIndex   { Above = "5"; Overlay = "20"; }
}

/// A NEON TUBE. An even sine reads as "loading indicator", not "sign".
animation NeonFlicker {
  Duration = "6s";
  Easing   = Linear;
  Repeat   = Infinite;
  0%   { Opacity = 1; }
  8%   { Opacity = 1; }
  9%   { Opacity = 0.55; }
  10%  { Opacity = 1; }
  30%  { Opacity = 1; }
  31%  { Opacity = 0.4; }
  32%  { Opacity = 0.9; }
  33%  { Opacity = 0.5; }
  34%  { Opacity = 1; }
  75%  { Opacity = 1; }
  76%  { Opacity = 0.65; }
  77%  { Opacity = 1; }
  100% { Opacity = 1; }
}

animation BulbChase {
  Duration = "1.6s"; Easing = Linear; Repeat = Infinite;
  0% { Opacity = 1; } 24% { Opacity = 1; } 26% { Opacity = 0.2; } 98% { Opacity = 0.2; } 100% { Opacity = 1; }
}

/// INSERT COIN — a hard on/off blink, not a fade. Every arcade attract screen in history blinks this way, and the
/// squareness is the whole character: stops one percent apart give an instant edge out of a linear interpolation.
animation CoinBlink {
  Duration = "1.1s"; Easing = Linear; Repeat = Infinite;
  0% { Opacity = 1; } 49% { Opacity = 1; } 50% { Opacity = 0; } 99% { Opacity = 0; } 100% { Opacity = 1; }
}

/// A CRT's slow brightness breathing. Very shallow — this should be felt rather than seen.
animation ScreenBreathe {
  Duration = "5s"; Easing = EaseInOut; Repeat = Infinite;
  0% { Opacity = 0.72; } 50% { Opacity = 1; } 100% { Opacity = 0.72; }
}

/// THE SCANLINE SWEEP — one bright band crawling down the glass, over the top of the static scanline texture.
animation ScanSweep {
  Duration = "7s"; Easing = Linear; Repeat = Infinite;
  0% { TranslateY = "-120%"; } 100% { TranslateY = "820%"; }
}

/// ATTRACT-MODE BOB — the gentle rise and fall a title screen does while nobody is playing.
animation AttractBob {
  Duration = "3.2s"; Easing = EaseInOut; Repeat = Infinite;
  0% { TranslateY = "0px"; } 50% { TranslateY = "-6px"; } 100% { TranslateY = "0px"; }
}
model/pages/birds.osy416 lines
// OSY BIRDS — a 3D flappy in a sunlit valley. A side-on camera over rolling low-poly hills, pipes as real lit
// cylinders, a modelled bird that pitches with its velocity and casts a real shadow, trees and clouds drifting
// past at their own parallax, fog at the horizon — every frame a few hundred `Draw.Mesh` calls on the same
// `Canvas` the 2D sky and HUD are painted on. The question this cabinet answers: can the canvas carry a lit,
// shadowed 3D scene at 60fps with the whole game loop still in Osy#.

[Page("/birds")]
[AllowAnonymous]
component Birds() {
  meta { title = "Osy Birds"; }

  // ── the screen ────────────────────────────────────────────────────────────────────────────────────────
  int W = 1280;
  int H = 800;
  int CX = 640;
  int HORIZON = 430;      // where the 2D sky gradient ends — the scene's own ground takes over below it

  // ── the world ─────────────────────────────────────────────────────────────────────────────────────────
  // Right-handed, y up, ground at y = 0. The bird holds x = 0 and flies toward +x; the pipes stream toward it
  // from the right. The camera sits in front (+z) and a little above, looking slightly down the valley.
  double birdY = 2.6;
  double vy = 0;
  double birdR = 0.34;    // the bird's collision radius
  double PIPER = 0.72;    // a pipe's cylinder radius
  double GAPH = 1.22;     // half the gap — the door is 2.44 units tall
  double CEIL = 9.0;      // where the upper pipes end, above the frame
  double camY = 3.05;     // the camera's rest height for the attract bob
  double scroll = 0;      // how far the valley has slid past — the scenery's clock

  int state = 0;          // 0 ready · 1 flying · 2 down
  int score = 0;
  int best = 0;
  bool posted = false;
  double deadFor = 0;     // seconds since the crash — gates the restart so it can't be flapped through
  double t = 0;           // world clock, drives the flap cycle, the attract bob and the clouds

  int seed = 4211;
  List<Pipe> pipes = new List<Pipe>();
  List<Prop> trees = new List<Prop>();
  List<Prop> hills = new List<Prop>();
  List<Prop> clouds = new List<Prop>();
  List<Prop> tufts = new List<Prop>();

  // ── the meshes: built once, drawn many times ──────────────────────────────────────────────────────────
  Mesh ground;
  Mesh water;
  Mesh ridge;             // the far skyline, a heightfield
  Mesh unitCyl;           // pipes, lips and trunks are ONE cylinder scaled per axis
  Mesh crown;
  Mesh ball;              // hills, clouds, tufts and the bird's body all start as this sphere
  Mesh beak;
  Mesh wing;

  double fps = 0;
  int frames = 0;
  double window = 0;

  /// ⚑ HOISTED, same reason as Blockfall's: a server read cannot sit inline in a 60fps body.
  var me = Session.CurrentUser;

  on mount {
    ground = Mesh.Plane(160, 40);
    water = Mesh.Plane(160, 14);
    ridge = BuildRidge();
    unitCyl = Mesh.Cylinder(1, 1, 1, 18);
    crown = Mesh.Cone(1, 1, 9);
    ball = Mesh.Sphere(1, 2);
    beak = Mesh.Cone(1, 1, 8);
    wing = Mesh.Box(1, 1, 1);

    // The scenery is rolled once: three bands of trees, a row of hills, a sky of clouds, tufts on the grass.
    // Each prop carries its own parallax, so the bands slide past at different speeds and the valley gets deep.
    for (var i = 0; i < 14; i = i + 1) {
      trees.Add(new Prop { X = Roll() % 100, Z = -3.5 - (Roll() % 100) * 0.03, S = 0.8 + (Roll() % 100) * 0.006, Par = 1.0, Tone = Roll() % 3 });
    }
    for (var i = 0; i < 16; i = i + 1) {
      trees.Add(new Prop { X = Roll() % 100, Z = -12.0 - (Roll() % 100) * 0.05, S = 1.0 + (Roll() % 100) * 0.009, Par = 0.55, Tone = Roll() % 3 });
    }
    for (var i = 0; i < 8; i = i + 1) {
      hills.Add(new Prop { X = i * 13.0 + (Roll() % 60) * 0.1, Z = -16.0 - (Roll() % 100) * 0.06, S = 5.0 + (Roll() % 100) * 0.05, Par = 0.4, Tone = Roll() % 3 });
    }
    for (var i = 0; i < 7; i = i + 1) {
      clouds.Add(new Prop { X = Roll() % 100, Z = -22.0 - (Roll() % 100) * 0.1, S = 1.2 + (Roll() % 100) * 0.012, Par = 0.12, Tone = 7 + (Roll() % 30) * 0.1 });
    }
    for (var i = 0; i < 26; i = i + 1) {
      tufts.Add(new Prop { X = Roll() % 100, Z = 2.5 - (Roll() % 100) * 0.09, S = 0.18 + (Roll() % 100) * 0.003, Par = 1.0, Tone = Roll() % 3 });
    }
    Reset();
  }

  int Roll() {
    seed = (seed * 75 + 74) % 65537;
    return seed;
  }

  /// The skyline: a strip of rolling heights, wound counter-clockwise seen from above so every face is lit
  /// from the sky side. 100 units wide so it can tile as the valley scrolls (see DrawRidge).
  Mesh BuildRidge() {
    var verts = new List<double>();
    var idx = new List<int>();
    var cols = 50;
    var rows = 5;
    for (var r = 0; r <= rows; r = r + 1) {
      for (var c = 0; c <= cols; c = c + 1) {
        var x = c * 2.0 - 50.0;
        var z = r * 3.0;
        // Four full waves across the 100-unit strip, so column 0 and column 50 land at the SAME height and the two
        // tiled copies meet without a step. (`c * 0.5` did not divide: 25 rad is not a multiple of 2π, and the
        // mismatch showed as a thin sliver of sky at the seam.) Every term below is periodic in 8π.
        var a = c * (Math.PI * 8.0 / 50.0);
        var h = 3.2 + Math.Sin(a) * 2.2 + Math.Sin(a * 2.0 + 1.3) * 0.9 + Math.Cos(a * 0.5) * 1.4;
        var y = r == 0 || r == rows ? 0.0 : h * (r == 1 || r == rows - 1 ? 0.75 : 1.0);
        verts.Add(x); verts.Add(y); verts.Add(z);
      }
    }
    for (var r = 0; r < rows; r = r + 1) {
      for (var c = 0; c < cols; c = c + 1) {
        var a = r * (cols + 1) + c;
        var b = a + 1;
        var d = a + cols + 1;
        var e = d + 1;
        idx.Add(a); idx.Add(d); idx.Add(b);
        idx.Add(b); idx.Add(d); idx.Add(e);
      }
    }
    return Mesh.From(verts, idx);
  }

  void Reset() {
    birdY = 2.6;
    vy = 0;
    score = 0;
    posted = false;
    deadFor = 0;
    var next = new List<Pipe>();
    for (var i = 0; i < 5; i = i + 1) {
      next.Add(new Pipe { X = 15.0 + i * 9.0, GapY = 1.9 + (Roll() % 1000) / 1000.0 * 2.6, Scored = false });
    }
    pipes = next;
  }

  void Flap() {
    if (state == 0) { state = 1; }
    if (state == 1) {
      vy = 4.7;
      return;
    }
    if (state == 2 && deadFor > 0.6) { Reset(); state = 1; vy = 4.7; }
  }

  action Tap() { Flap(); }
  /// The key surface's `onKeyDown` fires once per PRESS, however short — a tap is an edge, not a level, so it is
  /// wired here rather than polled with `Keyboard.Down` in the frame (a press shorter than a frame was invisible there).
  action Flapped(string key) { Flap(); }

  on frame (double dt) {
    frames = frames + 1;
    window = window + dt;
    if (window >= 0.5) { fps = Math.Round(frames / window); frames = 0; window = 0; }
    if (dt > 0.05) { dt = 0.05; }     // a background tab's first frame back must not teleport the bird
    t = t + dt;

    var speed = 6.0 + score * 0.08;
    if (speed > 11.0) { speed = 11.0; }
    if (state == 0) { speed = 1.6; }  // the valley idles past on the attract screen
    if (state == 2) { speed = 0.0; }

    if (state == 1) {
      vy = vy - 13.5 * dt;
      if (vy < -9.0) { vy = -9.0; }
      birdY = birdY + vy * dt;
      if (birdY > 6.6) { birdY = 6.6; vy = 0; }
      if (birdY < birdR + 0.04) { birdY = birdR + 0.04; Crash(); }

      var far = 0.0;
      foreach (var p in pipes) { if (p.X > far) { far = p.X; } }

      foreach (var p in pipes) {
        p.X = p.X - speed * dt;

        // The plane-crossing test: while the cylinder overlaps the bird's x, the bird must be inside the door.
        if (p.X > 0.0 - PIPER - birdR * 0.7 && p.X < PIPER + birdR * 0.7) {
          if (birdY > p.GapY + GAPH - birdR || birdY < p.GapY - GAPH + birdR) { Crash(); }
        }
        if (!p.Scored && p.X < 0.0 - PIPER) {
          p.Scored = true;
          score = score + 1;
          if (score > best) { best = score; }
        }
      }
      // A pipe that slid off the left edge respawns at the back of the train — the list never grows.
      foreach (var p in pipes) {
        if (p.X < -11.0) {
          p.X = far + 9.0;
          far = p.X;
          p.GapY = 1.9 + (Roll() % 1000) / 1000.0 * 2.6;
          p.Scored = false;
        }
      }
    }
    scroll = scroll + speed * dt;

    if (state == 0) { birdY = 2.6 + Math.Sin(t * 2.2) * 0.16; }
    if (state == 2) {
      deadFor = deadFor + dt;
      vy = vy - 13.5 * dt;
      birdY = birdY + vy * dt;
      if (birdY < birdR + 0.04) { birdY = birdR + 0.04; vy = 0; }
    }

    // The camera breathes after the bird instead of being bolted to it.
    camY = camY + (2.4 + birdY * 0.25 - camY) * 5.0 * dt;

    DrawSky();
    Draw.Camera(3.4, camY + 1.3, 17.5, 3.4, camY, 0, 42);
    Draw.Light(-0.35, -1.0, 0.45, "#fff1d0", "#a8ccff");       // a late-afternoon sun, front-right and high
    Draw.Fog("#d6ecff", 26, 74);
    DrawValley();
    DrawPipes();
    DrawBird();
    DrawHud();
  }

  void Crash() {
    if (state == 1) { state = 2; deadFor = 0; if (vy > 0) { vy = 0; } }
  }

  /// A prop's x on screen: its band slides past at its own parallax and wraps every 100 units, so the world
  /// is endless without ever being longer than one list.
  double PropX(Prop p) {
    var v = (p.X - scroll * p.Par) % 100.0;
    if (v < 0) { v = v + 100.0; }
    return v - 44.0;                                                 // the visible strip is about −8 … 15
  }

  // ── the sky, in 2D: a gradient the scene sits on, and a sun ──────────────────────────────────────────
  void DrawSky() {
    for (var i = 0; i < 20; i = i + 1) {
      var k = i / 19.0;
      var r = (int)(96 + 118 * k);
      var g = (int)(170 + 66 * k);
      var b = (int)(255 - 4 * k);
      Draw.Rect(0, i * HORIZON / 20, W, HORIZON / 20 + 1, "rgb(" + r + "," + g + "," + b + ")");
    }
    Draw.Rect(0, HORIZON, W, H - HORIZON, "#d6ecff");                // below the horizon, the fog's own colour
    Draw.Circle(1040, 118, 92, "rgba(255,244,200,0.35)");            // bloom
    Draw.Circle(1040, 118, 58, "#fff6d2");
  }

  // ── the valley: ground, water, the far ridge, hills, trees, tufts, clouds ─────────────────────────────
  void DrawValley() {
    Draw.Mesh(ground, 3.4, 0, -14, "#79b85c");
    Draw.Mesh(water, 3.4, -0.06, 11, "#5aa9e6");
    DrawRidge();

    foreach (var h in hills) {
      var x = PropX(h);
      var c = h.Tone == 0 ? "#6faf58" : (h.Tone == 1 ? "#7fbe64" : "#62a04f");
      Draw.Mesh(ball, x, -0.4, h.Z, 0, 0, 0, h.S, h.S * 0.42, h.S * 0.7, c);
    }
    foreach (var tr in trees) {
      var x = PropX(tr);
      var s = tr.S;
      var c = tr.Tone == 0 ? "#3f8f4a" : (tr.Tone == 1 ? "#4b9f52" : "#357d42");
      Draw.Mesh(unitCyl, x, 0.55 * s, tr.Z, 0, 0, 0, 0.16 * s, 1.1 * s, 0.16 * s, "#7a5236");
      Draw.Mesh(crown, x, 1.1 * s + 1.3 * s, tr.Z, 0, 0, 0, 0.95 * s, 2.6 * s, 0.95 * s, c);
      Draw.Mesh(crown, x, 1.1 * s + 2.4 * s, tr.Z, 0, 0, 0, 0.7 * s, 1.9 * s, 0.7 * s, c);
    }
    foreach (var g in tufts) {
      var x = PropX(g);
      var c = g.Tone == 0 ? "#5f9e47" : (g.Tone == 1 ? "#6cad52" : "#8a6a48");
      Draw.Mesh(ball, x, g.S * 0.35, g.Z, 0, 0, 0, g.S * 1.6, g.S * 0.7, g.S, c);
    }
    foreach (var cl in clouds) {
      var x = PropX(cl) + Math.Sin(t * 0.07 + cl.Z) * 0.6;
      var y = cl.Tone;
      var s = cl.S;
      Draw.Mesh(ball, x, y, cl.Z, 0, 0, 0, s * 1.5, s * 0.8, s, "#ffffff");
      Draw.Mesh(ball, x - s * 1.3, y - s * 0.2, cl.Z + 0.3, 0, 0, 0, s * 1.0, s * 0.6, s * 0.8, "#f4f9ff");
      Draw.Mesh(ball, x + s * 1.4, y - s * 0.15, cl.Z - 0.2, 0, 0, 0, s * 1.1, s * 0.65, s * 0.85, "#f4f9ff");
    }
  }

  /// The ridge is 100 wide and the band wraps every 100, so two copies always cover the frame.
  void DrawRidge() {
    var v = (0.0 - scroll * 0.22) % 100.0;
    if (v < 0) { v = v + 100.0; }
    var x = v - 44.0;
    Draw.Mesh(ridge, x, 0, -36, "#5f9a55");
    Draw.Mesh(ridge, x > 3.4 ? x - 100.0 : x + 100.0, 0, -36, "#5f9a55");
  }

  // ── the pipes: lit cylinders with a wider mouth ring, from one unit cylinder ─────────────────────────
  void DrawPipes() {
    foreach (var p in pipes) {
      if (p.X > -12.0 && p.X < 20.0) {
        var top = p.GapY + GAPH;
        var bot = p.GapY - GAPH;
        Draw.Mesh(unitCyl, p.X, (top + CEIL) / 2, 0, 0, 0, 0, PIPER, CEIL - top, PIPER, "#3fae4c");
        Draw.Mesh(unitCyl, p.X, bot / 2, 0, 0, 0, 0, PIPER, bot, PIPER, "#3fae4c");
        Draw.Mesh(unitCyl, p.X, top + 0.28, 0, 0, 0, 0, PIPER * 1.18, 0.56, PIPER * 1.18, "#46bd54");
        Draw.Mesh(unitCyl, p.X, bot - 0.28, 0, 0, 0, 0, PIPER * 1.18, 0.56, PIPER * 1.18, "#46bd54");
      }
    }
  }

  // ── the bird: a body, a head, a beak, an eye and two wings on the flap clock ────────────────────────
  void DrawBird() {
    var y = birdY;
    var pitch = vy * 0.09;                                            // the nose follows the velocity
    if (pitch > 0.6) { pitch = 0.6; }
    if (pitch < -0.7) { pitch = -0.7; }
    if (state == 2) { pitch = -1.2; }                                 // down, and it shows
    var flap = state == 2 ? 0.9 : Math.Sin(t * 16.0) * 0.8;          // wings stop rowing when the bird is down
    var cp = Math.Cos(pitch);
    var sp = Math.Sin(pitch);

    // Everything hangs off the body's centre, rotated by the pitch — a point (dx, dy) in the bird's own
    // frame lands at (dx·cos − dy·sin, dx·sin + dy·cos).
    Draw.Mesh(ball, 0, y, 0, 0, 0, pitch, 0.48, 0.4, 0.38, "#f2b632");                        // body
    Draw.Mesh(ball, 0.05 * cp - 0.36 * sp, y + 0.05 * sp + 0.36 * cp, 0, 0, 0, pitch, 0.34, 0.32, 0.3, "#f7c23a");   // head
    Draw.Mesh(ball, 0.02 * cp + 0.1 * sp, y + 0.02 * sp - 0.1 * cp, 0.02, 0, 0, pitch, 0.4, 0.28, 0.34, "#ffe2a0");  // belly
    Draw.Mesh(beak, 0.42 * cp - 0.36 * sp, y + 0.42 * sp + 0.36 * cp, 0, 0, 0, pitch - 1.5708, 0.11, 0.3, 0.11, "#f06a2b");  // beak
    Draw.Mesh(ball, 0.22 * cp - 0.48 * sp, y + 0.22 * sp + 0.48 * cp, 0.24, 0, 0, pitch, 0.1, 0.1, 0.08, "#ffffff");        // eye
    Draw.Mesh(ball, 0.26 * cp - 0.49 * sp, y + 0.26 * sp + 0.49 * cp, 0.3, 0, 0, pitch, 0.05, 0.05, 0.04, "#1a1020");
    Draw.Mesh(ball, -0.02 * cp - 0.66 * sp, y - 0.02 * sp + 0.66 * cp, 0, 0, 0, pitch, 0.08, 0.1, 0.06, "#e6402f");       // comb

    // The wings pivot at the body's sides: rotated about x by the stroke, placed half their span out along it.
    var a = flap;
    var wx = -0.12 * cp;
    var wy = y - 0.12 * sp + 0.06;
    Draw.Mesh(wing, wx, wy + Math.Sin(a) * 0.32, 0.3 + Math.Cos(a) * 0.32, 0.0 - a, 0, pitch, 0.42, 0.06, 0.64, "#d9981f");
    Draw.Mesh(wing, wx, wy + Math.Sin(a) * 0.32, -0.3 - Math.Cos(a) * 0.32, a, 0, pitch, 0.42, 0.06, 0.64, "#c98a1c");
    // The tail feathers, trailing.
    Draw.Mesh(wing, -0.44 * cp + 0.02 * sp, y - 0.44 * sp - 0.02 * cp, 0, 0, 0, pitch + 0.35, 0.3, 0.05, 0.22, "#d9981f");
  }

  // ── the HUD, drawn on the same surface ────────────────────────────────────────────────────────────────
  void DrawHud() {
    Draw.Text("" + score, CX - (score > 9 ? 34 : 17), 30, "rgba(0,0,0,0.35)", 64);
    Draw.Text("" + score, CX - (score > 9 ? 37 : 20), 26, "#FFFFFF", 64);
    Draw.Text($"BEST {best}", 24, 24, "#1d4f8a", 20);
    Draw.Text($"{fps} FPS", W - 110, 24, "#c2452a", 18);

    if (state == 0) {
      Draw.Text("O S Y   B I R D S", CX - 190, 112, "#1d4f8a", 40);
      Draw.Text("space / click to flap", CX - 130, 166, "#FFFFFF", 24);
    }
    if (state == 2 && deadFor > 0.4) {
      Draw.Rect(CX - 260, 290, 520, 150, "rgba(14,30,52,0.82)");
      Draw.Rect(CX - 260, 290, 520, 3, "#FF2D95");
      Draw.Text("G A M E   O V E R", CX - 150, 320, "#FF2D95", 34);
      Draw.Text($"score {score}   ·   best {best}", CX - 110, 372, "#FFFFFF", 22);
      if (deadFor > 0.6) { Draw.Text("space to fly again", CX - 90, 406, "#8fd0ff", 17); }
    }
  }

  /// Posted once, when the bird is down — same contract as Blockfall's board: anonymous play posts nothing.
  action PostScore() {
    if (me == null) { return; }
    if (score <= 0 || state != 2 || posted) { return; }
    posted = true;
    var landed = new Score {
      Player = me,
      Game = Game.OsyBirds,
      Points = score,
    };
    UnitOfWork.Commit();
  }

  render {
    Box(minH: "100vh", w: "100%", bg: Colors.Bg, color: Colors.OnBg, p: 6) {
      Stack(gap: 4, align: Align.Center) {
        NeonWord("Osy Birds", "#FFC24D", "30px");
        Text("space to flap · fly the gaps · a lit, shadowed 3D valley on one canvas",
             fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim, letterSpacing: "0.14em",
             textTransform: TextTransform.Uppercase);
        Box(keys: [Space, Up, W], onKeyDown: Flapped, rounded: Radius.Cab, overflow: Overflow.Hidden, lineHeight: "0") {
          Pressable(onClick: Tap) {
            Canvas(w: 1280, h: 800, borderW: 2, border: Colors.Amber, maxW: "100%",
                   shadow: "0 0 40px rgba(255,194,77,0.25)");
          }
        }
        Row(gap: 6, align: Align.Center) {
          if (state == 2 && me != null && score > 0 && !posted) {
            Pressable(onClick: PostScore) { ArcadeButton("Save score", "#FFC24D"); }
          }
          if (posted) {
            Text("score on the board ✓", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Cyan, letterSpacing: "0.18em");
          }
          Text("1280×800 · Draw.Mesh · sun shadows · fog · ~150 meshes/frame",
               fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, letterSpacing: "0.2em",
               textTransform: TextTransform.Uppercase);
        }
        Link("/") { Text("← back to the floor", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim); }
      }
    }
  }
}

/// One door in the endless train. A class, not parallel arrays — `foreach` hands the loop one thing.
class Pipe {
  public double X;        // how far up the valley it still is
  public double GapY;     // the door's centre height
  public bool Scored;
}

/// A piece of scenery: where it is in its band, how big, how fast its band slides past, and a colour pick.
class Prop {
  public double X;
  public double Z;
  public double S;
  public double Par;
  public double Tone;
}
model/pages/blockfall.osy262 lines
// BLOCKFALL — a real falling-block game, and the first FINISHED machine on the floor.

[Page("/blockfall")]
[AllowAnonymous]
component Blockfall() {
  meta { title = "Blockfall"; }

  int[] shape = [
    0,1, 1,1, 2,1, 3,1,   2,0, 2,1, 2,2, 2,3,   0,2, 1,2, 2,2, 3,2,   1,0, 1,1, 1,2, 1,3,
    1,0, 2,0, 1,1, 2,1,   1,0, 2,0, 1,1, 2,1,   1,0, 2,0, 1,1, 2,1,   1,0, 2,0, 1,1, 2,1,
    1,0, 0,1, 1,1, 2,1,   1,0, 1,1, 2,1, 1,2,   0,1, 1,1, 2,1, 1,2,   1,0, 0,1, 1,1, 1,2,
    1,0, 2,0, 0,1, 1,1,   1,0, 1,1, 2,1, 2,2,   1,1, 2,1, 0,2, 1,2,   0,0, 0,1, 1,1, 1,2,
    0,0, 1,0, 1,1, 2,1,   2,0, 1,1, 2,1, 1,2,   0,1, 1,1, 1,2, 2,2,   1,0, 0,1, 1,1, 0,2,
    0,0, 0,1, 1,1, 2,1,   1,0, 2,0, 1,1, 1,2,   0,1, 1,1, 2,1, 2,2,   1,0, 1,1, 0,2, 1,2,
    2,0, 0,1, 1,1, 2,1,   1,0, 1,1, 1,2, 2,2,   0,1, 1,1, 2,1, 0,2,   0,0, 1,0, 1,1, 1,2
  ];

  string[] palette = ["#22E5FF", "#FFC24D", "#B45CFF", "#4DFF6E", "#FF2D95", "#4D7CFF", "#FF8A3D"];

  int[] well = [];
  string[] view = [];
  string[] preview = [];   // the NEXT piece, composed in the loop — see the note on Preview() below

  int piece = 0;
  int rot = 0;
  int px = 3;
  int py = 0;
  int nextPiece = 1;

  double drop = 0;
  double moveHold = 0;
  double rotHold = 0;

  int lines = 0;
  int score = 0;
  int level = 1;
  bool over = false;
  int seed = 12345;

  /// ⚑ HOISTED TO A MEMBER ON PURPOSE. `Session.CurrentUser` is a SERVER read, so it cannot be an inline value in a
  /// render expression — the compiler refuses it, naming the choice it cannot make for you: fetched once, or re-
  /// read when its inputs change. Once, is right here: who is signed in does not change while a game is being
  /// played.
  var me = Session.CurrentUser;

  /// Whether THIS run's score is on the board. An `action` may write component state; a reaction may not.
  bool posted = false;

  on mount { Restart(); }

  action Restart() {
    var blank = new List<int>();
    for (var i = 0; i < 200; i = i + 1) { blank.Add(0); }
    well = blank;
    lines = 0; score = 0; level = 1; over = false; posted = false;
    seed = 12345;
    nextPiece = Roll();
    Spawn();
    view = Compose();
    preview = Preview();
  }

  int Roll() {
    seed = (seed * 75 + 74) % 65537;
    return seed % 7;
  }

  /// ⚑ POSTED ONCE, WHEN THE GAME ENDS — not per line, or the board would fill with one player's whole session.
  /// Anonymous play is the norm here, so `Session.CurrentUser` is null most of the time and there is simply nothing
  /// to post: the game is free, the board is what a handle buys. A zero is not a record either.
  action PostScore() {
    if (me == null) { return; }
    if (score <= 0) { return; }
    posted = true;
    var posted = new Score {
      Player = me,
      Game = Game.Blockfall,
      Points = score,
      Lines = lines,
      Level = level,
    };
    UnitOfWork.Commit();
  }

  void Spawn() {
    piece = nextPiece;
    nextPiece = Roll();
    rot = 0;
    px = 3;
    py = 0;
    drop = 0;
    if (Hits(piece, rot, px, py)) { over = true; }
  }

  bool Hits(int p, int r, int ox, int oy) {
    for (var i = 0; i < 4; i = i + 1) {
      var b = ((p * 4 + r) * 4 + i) * 2;
      var cx = ox + shape[b];
      var cy = oy + shape[b + 1];
      if (cx < 0 || cx > 9 || cy > 19) { return true; }
      if (cy >= 0 && well[cy * 10 + cx] != 0) { return true; }
    }
    return false;
  }

  on frame (double dt) {
    if (over) { return; }

    moveHold = moveHold - dt;
    if (moveHold <= 0) {
      if (Keyboard.Down(Left)  && !Hits(piece, rot, px - 1, py)) { px = px - 1; moveHold = 0.09; }
      if (Keyboard.Down(Right) && !Hits(piece, rot, px + 1, py)) { px = px + 1; moveHold = 0.09; }
    }

    rotHold = rotHold - dt;
    if ((Keyboard.Down(Up) || Keyboard.Down(X)) && rotHold <= 0) {
      var nr = (rot + 1) % 4;
      if (!Hits(piece, nr, px, py))          { rot = nr; rotHold = 0.16; }
      else if (!Hits(piece, nr, px - 1, py)) { rot = nr; px = px - 1; rotHold = 0.16; }
      else if (!Hits(piece, nr, px + 1, py)) { rot = nr; px = px + 1; rotHold = 0.16; }
      else if (!Hits(piece, nr, px - 2, py)) { rot = nr; px = px - 2; rotHold = 0.16; }
      else if (!Hits(piece, nr, px + 2, py)) { rot = nr; px = px + 2; rotHold = 0.16; }
    }

    var interval = 0.75 - (level - 1) * 0.06;
    if (interval < 0.08) { interval = 0.08; }
    if (Keyboard.Down(Down)) { interval = 0.03; }

    var soft = Keyboard.Down(Down);
    drop = drop + dt;
    while (drop >= interval) {
      drop = drop - interval;
      if (!Hits(piece, rot, px, py + 1)) { py = py + 1; if (soft) { score = score + 1; } }
      else { Lock(); }
    }

    view = Compose();
    preview = Preview();
  }

  void Lock() {
    var next = new List<int>();
    for (var i = 0; i < 200; i = i + 1) { next.Add(well[i]); }
    for (var i = 0; i < 4; i = i + 1) {
      var b = ((piece * 4 + rot) * 4 + i) * 2;
      var cx = px + shape[b];
      var cy = py + shape[b + 1];
      if (cy >= 0) { next[cy * 10 + cx] = piece + 1; }
    }
    well = next;
    Clear();
    Spawn();
  }

  void Clear() {
    var cleared = FullRowCount(well);
    if (cleared == 0) { return; }
    well = CollapseRows(well);
    lines = lines + cleared;
    score = score + LineScore(cleared, level);
    level = LevelFor(lines);
  }

  string[] Compose() {
    var cells = new List<string>();
    for (var i = 0; i < 200; i = i + 1) {
      cells.Add(well[i] == 0 ? "" : palette[well[i] - 1]);
    }
    if (!over) {
      for (var i = 0; i < 4; i = i + 1) {
        var b = ((piece * 4 + rot) * 4 + i) * 2;
        var cx = px + shape[b];
        var cy = py + shape[b + 1];
        if (cy >= 0 && cy <= 19 && cx >= 0 && cx <= 9) { cells[cy * 10 + cx] = palette[piece]; }
      }
    }
    return cells;
  }

  string[] Preview() {
    var cells = new List<string>();
    for (var i = 0; i < 16; i = i + 1) { cells.Add(""); }
    for (var i = 0; i < 4; i = i + 1) {
      var b = ((nextPiece * 4 + 0) * 4 + i) * 2;
      cells[shape[b + 1] * 4 + shape[b]] = palette[nextPiece];
    }
    return cells;
  }

  render {
    Box(minH: "100vh", w: "100%", bg: Colors.Bg, color: Colors.OnBg, position: Position.Relative, overflow: Overflow.Hidden,
        bgImage: "radial-gradient(110% 60% at 50% -10%, rgba(34,229,255,0.16) 0%, rgba(7,3,13,0) 60%)") {
      Stack(gap: 4, px: 6, py: 7, align: Align.Center) {

        NeonWord("Blockfall", "#22E5FF", "34px");
        Text("← → move · ↑ rotate · ↓ soft drop · click the well first",
             fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim, letterSpacing: "0.16em", textTransform: TextTransform.Uppercase);

        Row(gap: 5, align: Align.Start) {
          Box(keys: [Left, Right, Up, Down, X],
              display: Display.Grid, cols: "repeat(10, 26px)", rows: "repeat(20, 26px)", gap: 1,
              p: 2, bg: Colors.Screen, rounded: Radius.Cab, borderW: 2, border: Colors.Cyan,
              shadow: "0 0 40px rgba(34,229,255,0.28), inset 0 0 50px rgba(0,0,0,0.9)") {
            foreach (var c in view) {
              Box(rounded: Radius.Chip,
                  bg: c == "" ? "#0B0616" : c,
                  shadow: c == "" ? "none" : "0 0 10px " + c);
            }
          }

          Stack(gap: 4, minW: "170px") {
            Text("next", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, letterSpacing: "0.28em",
                 textTransform: TextTransform.Uppercase);
            Box(display: Display.Grid, cols: "repeat(4, 22px)", rows: "repeat(4, 22px)", gap: 1,
                p: 2, bg: Colors.Screen, rounded: Radius.Chip, borderW: 1, border: Colors.CabinetLip) {
              foreach (var c in preview) {
                Box(rounded: Radius.Chip, bg: c == "" ? "#0B0616" : c,
                    shadow: c == "" ? "none" : "0 0 8px " + c);
              }
            }

            Stat("score", $"{score}");
            Stat("lines", $"{lines}");
            Stat("level", $"{level}");

            if (over) {
              Text("game over", fontFamily: Font.Display, fontSize: FontSize.Head, fontWeight: FontWeight.Black, color: Colors.Pink,
                   textShadow: "0 0 8px #FF2D95, 0 0 24px #FF2D95", textTransform: TextTransform.Uppercase);
              if (me != null && !posted && score > 0) {
                Pressable(onClick: PostScore) { ArcadeButton("Save score", "#FFC24D"); }
              }
              if (me != null && posted) {
                Text("on the board", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Amber,
                     letterSpacing: "0.2em", textTransform: TextTransform.Uppercase);
              }
              if (me == null) {
                Link("/login") {
                  Text("sign in to save a score", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim,
                       textDecoration: TextDecoration.Underline);
                }
              }
            }
            Pressable(onClick: Restart) { ArcadeButton("New game", "#22E5FF"); }
            Link("/scores") { Text("high scores →", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Amber); }
            Link("/") { Text("← back to the floor", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim); }
          }
        }
      }
    }
  }
}

[Composable] component Stat(string label, string value) {
  render {
    Stack(gap: 1) {
      Text(label, fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, letterSpacing: "0.28em",
           textTransform: TextTransform.Uppercase);
      Text(value, fontFamily: Font.Display, fontSize: FontSize.Head, fontWeight: FontWeight.Black, color: Colors.Cyan);
    }
  }
}
model/pages/corridor.osy200 lines
// CORRIDOR — a Wolfenstein-style raycaster, drawn entirely out of `Box` atoms.

[Page("/corridor")]
[AllowAnonymous]
component Corridor() {
  meta { title = "Corridor"; }

  int[] map = [
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,2,2,0,0,0,0,0,0,0,0,3,3,0,1,
    1,0,2,0,0,0,0,0,0,0,0,0,0,3,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,0,0,0,4,4,0,0,4,4,0,0,0,0,1,
    1,0,0,0,0,4,0,0,0,0,4,0,0,0,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,0,0,0,4,0,0,0,0,4,0,0,0,0,1,
    1,0,0,0,0,4,4,0,0,4,4,0,0,0,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,3,0,0,0,0,0,0,0,0,0,0,2,0,1,
    1,0,3,3,0,0,0,0,0,0,0,0,2,2,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
  ];

  double posX = 8.0;
  double posY = 14.0;
  double dirX = 0;      // facing up the map (−Y)
  double dirY = -1;
  double planeX = 0.66; // the camera plane, perpendicular to dir — its LENGTH is the field of view
  double planeY = 0;

  double moveSpeed = 2.4;    // cells / second
  double turnSpeed = 2.4;    // radians / second

  Column[] cols = [];
  double fps = 0;
  int frames = 0;
  double window = 0;

  on mount { Cast(); }

  on frame (double dt) {
    frames = frames + 1;
    window = window + dt;
    if (window >= 0.5) { fps = Math.Round(frames / window); frames = 0; window = 0; }

    if (Keyboard.Down(Left))  { Turn(-turnSpeed * dt); }
    if (Keyboard.Down(Right)) { Turn(turnSpeed * dt); }

    var fwd = 0.0;
    if (Keyboard.Down(W) || Keyboard.Down(Up))   { fwd = moveSpeed * dt; }
    if (Keyboard.Down(S) || Keyboard.Down(Down)) { fwd = fwd - moveSpeed * dt; }
    var strafe = 0.0;
    if (Keyboard.Down(A)) { strafe = -moveSpeed * dt; }
    if (Keyboard.Down(D)) { strafe = moveSpeed * dt; }

    if (fwd != 0 || strafe != 0) {
      TryMove(dirX * fwd + planeX * strafe, dirY * fwd + planeY * strafe);
    }

    Cast();
  }

  void Turn(double a) {
    var c = Math.Cos(a);
    var s = Math.Sin(a);
    var oldDirX = dirX;
    dirX = dirX * c - dirY * s;
    dirY = oldDirX * s + dirY * c;
    var oldPlaneX = planeX;
    planeX = planeX * c - planeY * s;
    planeY = oldPlaneX * s + planeY * c;
  }

  void TryMove(double dx, double dy) {
    var nx = posX + dx;
    var ny = posY + dy;
    var mx = dx > 0 ? 0.2 : (dx < 0 ? -0.2 : 0.0);
    var my = dy > 0 ? 0.2 : (dy < 0 ? -0.2 : 0.0);
    if (!Solid(nx + mx, posY)) { posX = nx; }
    if (!Solid(posX, ny + my)) { posY = ny; }
  }

  bool Solid(double x, double y) { return At(Cell(x), Cell(y)) > 0; }

  int Cell(double v) { return (int)Math.Floor(v); }

  int At(int mx, int my) {
    if (mx < 0 || my < 0 || mx > 15 || my > 15) { return 1; }   // outside the map is solid, so a ray always lands
    return map[my * 16 + mx];
  }

  void Cast() {
    var next = new List<Column>();
    for (var x = 0; x < 120; x = x + 1) {
      var cameraX = 2.0 * x / 120.0 - 1.0;
      var rayX = dirX + planeX * cameraX;
      var rayY = dirY + planeY * cameraX;

      var mapX = Cell(posX);
      var mapY = Cell(posY);

      var deltaX = rayX > -0.000001 && rayX < 0.000001 ? 1000000.0 : (rayX < 0 ? -1.0 / rayX : 1.0 / rayX);
      var deltaY = rayY > -0.000001 && rayY < 0.000001 ? 1000000.0 : (rayY < 0 ? -1.0 / rayY : 1.0 / rayY);

      var stepX = 0;
      var stepY = 0;
      var sideX = 0.0;
      var sideY = 0.0;
      if (rayX < 0) { stepX = -1; sideX = (posX - mapX) * deltaX; }
      else          { stepX = 1;  sideX = (mapX + 1.0 - posX) * deltaX; }
      if (rayY < 0) { stepY = -1; sideY = (posY - mapY) * deltaY; }
      else          { stepY = 1;  sideY = (mapY + 1.0 - posY) * deltaY; }

      var hit = 0;
      var side = 0;          // 0 = a wall facing along X, 1 = along Y — used for shading
      var guard = 0;
      while (hit == 0 && guard < 64) {
        guard = guard + 1;
        if (sideX < sideY) { sideX = sideX + deltaX; mapX = mapX + stepX; side = 0; }
        else               { sideY = sideY + deltaY; mapY = mapY + stepY; side = 1; }
        hit = At(mapX, mapY);
      }

      var dist = side == 0
        ? (mapX - posX + (1 - stepX) / 2.0) / rayX
        : (mapY - posY + (1 - stepY) / 2.0) / rayY;
      if (dist < 0.05) { dist = 0.05; }

      var raw = 320.0 / dist;
      var h = raw < 4.0 ? 4.0 : (raw > 900.0 ? 900.0 : raw);

      var wallX = side == 0 ? posY + dist * rayY : posX + dist * rayX;
      wallX = wallX - Math.Floor(wallX);
      var texCol = (int)(wallX * 64.0);
      if (texCol < 0) { texCol = 0; }
      if (texCol > 63) { texCol = 63; }

      next.Add(new Column {
        H = h,
        Tex = WallTexture(hit),
        Pos = (texCol * 100.0 / 63.0) + "% 0%",
        Dim = Darkness(side, dist)
      });
    }
    cols = next;
  }

  string Darkness(int side, double dist) {
    var k = dist * 0.075;
    if (k > 0.82) { k = 0.82; }
    if (side == 1) { k = k + 0.22; }
    if (k > 0.88) { k = 0.88; }
    return "rgba(0,0,0," + Math.Round(k * 100) / 100 + ")";
  }

  render {
    Box(minH: "100vh", w: "100%", bg: Colors.Bg, color: Colors.OnBg) {
      Stack(gap: 4, px: 6, py: 6, align: Align.Center) {

        NeonWord("Corridor", "#FF2D95", "34px");
        Text("W A S D walk · ← → turn · click the view first",
             fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim, letterSpacing: "0.18em", textTransform: TextTransform.Uppercase);

        Box(keys: [Left, Right, Up, Down, W, A, S, D],
            position: Position.Relative, w: "100%", maxW: "960px", h: "320px", overflow: Overflow.Hidden,
            rounded: Radius.Cab, borderW: 2, border: Colors.Pink,
            bgImage: "linear-gradient(180deg, #241633 0%, #14101f 50%, #0d0a14 50%, #1b1220 100%)",
            shadow: "0 0 44px rgba(255,45,149,0.3), inset 0 0 60px rgba(0,0,0,0.9)") {
          Row(gap: 0, w: "100%", h: "100%", align: Align.Center) {
            foreach (var c in cols) {
              Box(grow: 1, h: c.H, bgImage: c.Tex, bgSize: "6400% 100%", bgPosition: c.Pos) {
                Box(w: "100%", h: "100%", bg: c.Dim);
              }
            }
          }
        }

        Row(gap: 6, align: Align.Center) {
          Text($"{fps} fps", fontFamily: Font.Mono, fontSize: FontSize.Body, color: Colors.Pink, letterSpacing: "0.2em");
          Text("120 columns · no canvas", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, letterSpacing: "0.2em",
               textTransform: TextTransform.Uppercase);
        }
        Link("/") { Text("← back to the floor", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim); }
      }
    }
  }
}

/// One screen column: how tall the wall is, and what colour. A class rather than two parallel arrays — `foreach`
/// hands the renderer one thing, and a column IS one thing.
class Column {
  public double H;      // wall height in px
  public string Tex;    // `url(data:…)` for this wall's material
  public string Pos;    // which texture column, as a background-position percentage
  public string Dim;    // the shading overlay
}
model/pages/lobby.osy232 lines
// THE LOBBY — the room you walk into.

// ── ATTRACT ART — one per machine, each drawn out of boxes ───────────────────────────────────────────────

/// TETRIS: four tetrominoes falling down a well, in the canonical piece colours.
[Composable] component AttractBlocks() {
  render {
    Box(position: Position.Absolute, inset: 0, p: 3) {
      Stack(gap: 1, align: Align.Center, justify: Justify.Center, h: "100%", animation: AttractBob) {
        Row(gap: 1) {
          Box(w: "16px", h: "16px", bg: "#22E5FF", rounded: Radius.Chip, shadow: "0 0 12px #22E5FF");
          Box(w: "16px", h: "16px", bg: "#22E5FF", rounded: Radius.Chip, shadow: "0 0 12px #22E5FF");
          Box(w: "16px", h: "16px", bg: "#22E5FF", rounded: Radius.Chip, shadow: "0 0 12px #22E5FF");
          Box(w: "16px", h: "16px", bg: "#22E5FF", rounded: Radius.Chip, shadow: "0 0 12px #22E5FF");
        }
        Row(gap: 1) {
          Box(w: "16px", h: "16px", bg: "#FFC24D", rounded: Radius.Chip, shadow: "0 0 12px #FFC24D");
          Box(w: "16px", h: "16px", bg: "#FFC24D", rounded: Radius.Chip, shadow: "0 0 12px #FFC24D");
          Box(w: "16px", h: "16px", bg: "#B45CFF", rounded: Radius.Chip, shadow: "0 0 12px #B45CFF");
          Box(w: "16px", h: "16px", bg: "#4DFF6E", rounded: Radius.Chip, shadow: "0 0 12px #4DFF6E");
        }
        Row(gap: 1) {
          Box(w: "16px", h: "16px", bg: "#FF2D95", rounded: Radius.Chip, shadow: "0 0 12px #FF2D95");
          Box(w: "16px", h: "16px", bg: "#B45CFF", rounded: Radius.Chip, shadow: "0 0 12px #B45CFF");
          Box(w: "16px", h: "16px", bg: "#B45CFF", rounded: Radius.Chip, shadow: "0 0 12px #B45CFF");
          Box(w: "16px", h: "16px", bg: "#4DFF6E", rounded: Radius.Chip, shadow: "0 0 12px #4DFF6E");
        }
      }
    }
  }
}

/// OSY BIRDS: a still of the valley — dusk sky, sun on the horizon, two pipe gates and the bird between them.
[Composable] component AttractBirds() {
  render {
    Box(position: Position.Absolute, inset: 0,
        bgImage: "linear-gradient(180deg, #0A041E 0%, #46104C 58%, #0B0616 58%, #0B0616 100%)") {
      Box(position: Position.Absolute, left: "50%", top: "42%", w: "44px", h: "44px", ml: "-22px", mt: "-22px",
          rounded: Radius.Pill, bg: "#FF7A45", shadow: "0 0 24px rgba(255,45,149,0.8)");
      Box(position: Position.Absolute, left: 0, right: 0, top: "57%", h: "2px", bg: "rgba(255,194,77,0.55)");
      Box(position: Position.Absolute, left: "14%", top: 0, bottom: "58%", w: "13%", bg: "#2BB24C",
          borderW: 1, border: "#1A0B2E", shadow: "0 0 14px rgba(77,255,110,0.45)");
      Box(position: Position.Absolute, left: "14%", top: "72%", bottom: 0, w: "13%", bg: "#2BB24C",
          borderW: 1, border: "#1A0B2E");
      Box(position: Position.Absolute, right: "16%", top: 0, bottom: "70%", w: "10%", bg: "#249342",
          borderW: 1, border: "#1A0B2E");
      Box(position: Position.Absolute, right: "16%", top: "58%", bottom: 0, w: "10%", bg: "#249342",
          borderW: 1, border: "#1A0B2E");
      Box(position: Position.Absolute, left: "46%", top: "34%", w: "22px", h: "22px",
          rounded: Radius.Pill, bg: "#FFC24D", borderW: 2, border: "#1A0B2E",
          shadow: "0 0 14px rgba(255,194,77,0.8)", animation: AttractBob);
    }
  }
}

/// WOLFENSTEIN: eight wall columns at descending heights — a still frame of the raycaster, drawn the exact way the
/// real one will be. If this reads as a corridor, the technique reads, and 120 of these is the only open question.
[Composable] component AttractCorridor() {
  render {
    Box(position: Position.Absolute, inset: 0,
        bgImage: "linear-gradient(180deg, #2A1B3D 0%, #2A1B3D 50%, #120A1E 50%, #120A1E 100%)") {
      Row(gap: 0, align: Align.Center, h: "100%", w: "100%") {
        Box(grow: 1, h: "44%",  bg: "#7A2438", borderRW: 1, border: Colors.Ink);
        Box(grow: 1, h: "58%",  bg: "#93304A", borderRW: 1, border: Colors.Ink);
        Box(grow: 1, h: "76%",  bg: "#AE3B58", borderRW: 1, border: Colors.Ink);
        Box(grow: 1, h: "96%",  bg: "#C74766", borderRW: 1, border: Colors.Ink);
        Box(grow: 1, h: "96%",  bg: "#B03F5B", borderRW: 1, border: Colors.Ink);
        Box(grow: 1, h: "72%",  bg: "#95324C", borderRW: 1, border: Colors.Ink);
        Box(grow: 1, h: "54%",  bg: "#7C283E", borderRW: 1, border: Colors.Ink);
        Box(grow: 1, h: "40%",  bg: "#651F32", borderRW: 1, border: Colors.Ink);
      }
      Box(position: Position.Absolute, bottom: "0", left: "42%", w: "16%", h: "34%",
          bg: "#2E2A36", rounded: Radius.Chip, shadow: "0 0 18px rgba(0,0,0,0.9)");
    }
  }
}

/// THE STRATEGY BOARD: a 5×5 grid mid-game, two colours of stone.
[Composable] component AttractMines() {
  var cells = [9, 1, 0, 0, 0,
               1, 1, 0, 8, 0,
               0, 0, 0, 0, 0,
               0, 2, 1, 1, 0,
               0, 0, 0, 8, 0];
  render {
    Box(position: Position.Absolute, inset: 0, p: 4) {
      Box(display: Display.Grid, cols: "repeat(5, 1fr)", rows: "repeat(5, 1fr)", gap: 1, w: "100%", h: "100%") {
        foreach (var c in cells) {
          Box(rounded: Radius.Chip, borderW: 1, border: "#2A1B3D",
              bg: c == 0 ? "#1A0B2E" : "#0B0616",
              align: Align.Center, justify: Justify.Center) {
            if (c == 1) { Text("1", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: "#22E5FF", fontWeight: FontWeight.Bold); }
            if (c == 2) { Text("2", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: "#4DFF6E", fontWeight: FontWeight.Bold); }
            if (c == 8) { Text("\u2691", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: "#FFC24D"); }
            if (c == 9) { Text("\u2731", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: "#FF2D95"); }
          }
        }
      }
    }
  }
}

[Composable] component AttractBoard() {
  var cells = [0, 1, 0, 0, 2,
               0, 2, 1, 0, 0,
               1, 1, 3, 2, 0,
               0, 2, 2, 1, 0,
               0, 0, 1, 0, 0];
  render {
    Box(position: Position.Absolute, inset: 0, p: 4) {
      Box(display: Display.Grid, cols: "repeat(5, 1fr)", rows: "repeat(5, 1fr)", gap: 1, w: "100%", h: "100%") {
        foreach (var c in cells) {
          Box(rounded: Radius.Chip, borderW: 1, border: "#2A1B3D",
              bg: c == 0 ? "#0B0616" : (c == 1 ? "#4DFF6E" : (c == 2 ? "#FF2D95" : "#FFC24D")),
              shadow: c == 0 ? "none" : "0 0 10px rgba(255,255,255,0.22)");
        }
      }
    }
  }
}

/// THE ARENA: four players on a floor, each a lit dot with a trail behind it.
[Composable] component AttractArena() {
  render {
    Box(position: Position.Absolute, inset: 0,
        bgImage: "repeating-linear-gradient(90deg, rgba(180,92,255,0.16) 0 1px, rgba(0,0,0,0) 1px 26px), repeating-linear-gradient(0deg, rgba(180,92,255,0.16) 0 1px, rgba(0,0,0,0) 1px 26px)") {
      Box(position: Position.Absolute, left: "18%", top: "28%", w: "14px", h: "14px", rounded: Radius.Pill,
          bg: "#22E5FF", shadow: "0 0 16px #22E5FF", animation: AttractBob);
      Box(position: Position.Absolute, left: "62%", top: "22%", w: "14px", h: "14px", rounded: Radius.Pill,
          bg: "#FF2D95", shadow: "0 0 16px #FF2D95", animation: ScreenBreathe);
      Box(position: Position.Absolute, left: "40%", top: "58%", w: "14px", h: "14px", rounded: Radius.Pill,
          bg: "#FFC24D", shadow: "0 0 16px #FFC24D", animation: AttractBob);
      Box(position: Position.Absolute, left: "76%", top: "64%", w: "14px", h: "14px", rounded: Radius.Pill,
          bg: "#4DFF6E", shadow: "0 0 16px #4DFF6E", animation: ScreenBreathe);
    }
  }
}

/// PINBALL — the fifth cabinet, and the one with no plan behind it. Every arcade has a machine nobody has fixed.
[Composable] component AttractPinball() {
  render {
    Box(position: Position.Absolute, inset: 0,
        bgImage: "radial-gradient(80% 90% at 50% 15%, rgba(255,194,77,0.3) 0%, rgba(0,0,0,0) 65%)") {
      Box(position: Position.Absolute, left: "46%", top: "20%", w: "18px", h: "18px", rounded: Radius.Pill,
          bg: "#E8E4F0", shadow: "0 0 16px rgba(232,228,240,0.8)", animation: AttractBob);
      Box(position: Position.Absolute, left: "24%", bottom: "18%", w: "42px", h: "9px", rounded: Radius.Pill,
          bg: "#FF2D95", rotate: "22deg", shadow: "0 0 12px #FF2D95");
      Box(position: Position.Absolute, right: "24%", bottom: "18%", w: "42px", h: "9px", rounded: Radius.Pill,
          bg: "#FF2D95", rotate: "-22deg", shadow: "0 0 12px #FF2D95");
    }
  }
}

[Page("/")]
[AllowAnonymous]
component Lobby() {
  meta { title = "Arcade"; }

  /// Who is at the machine, or null.
  var me = Session.CurrentUser;

  action SignOut() { Session.SignOut(); }

  render {
    Box(minH: "100vh", w: "100%", bg: Colors.Bg, color: Colors.OnBg, position: Position.Relative, overflow: Overflow.Hidden,
        bgImage: "radial-gradient(120% 70% at 50% -10%, rgba(180,92,255,0.22) 0%, rgba(7,3,13,0) 60%), radial-gradient(90% 50% at 50% 108%, rgba(34,229,255,0.16) 0%, rgba(7,3,13,0) 60%)") {

      Stack(gap: 8, px: 6, py: 9, maxW: "1240px", mx: "auto") {

        Marquee();

        Row(gap: 3, align: Align.Center, justify: Justify.Center) {
          if (me == null) {
            Text("playing as guest — scores are not saved",
                 fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, letterSpacing: "0.18em", textTransform: TextTransform.Uppercase);
            Link("/login") { ArcadeButton("Insert coin", "#B45CFF"); }
          }
          if (me != null) {
            Text($"player {me.Handle}",
                 fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Lime, letterSpacing: "0.18em", textTransform: TextTransform.Uppercase);
            Pressable(onClick: SignOut) {
              Text("sign out", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, textDecoration: TextDecoration.Underline);
            }
          }
          Link("/scores") { ArcadeButton("High scores", "#FFC24D"); }
        }

        Box(display: Display.Grid, cols: "repeat(auto-fit, minmax(268px, 1fr))", gap: 6, w: "100%") {

          Cabinet("Blockfall", "Held keys, a frame tick, and two hundred cells repainting per drop. The render engine's throughput floor, wearing a hat.", new Look { Accent = "#22E5FF", Glow = "rgba(34,229,255,0.14)" }, "/blockfall", true) {
            AttractBlocks();
          }

          Cabinet("Minefield", "No frame loop at all — one tap unwinds a recursive flood sixty calls deep. Where Blockfall measures throughput, this measures DEPTH.", new Look { Accent = "#4DFF6E", Glow = "rgba(77,255,110,0.14)" }, "/minefield", true) {
            AttractMines();
          }

          Cabinet("Corridor", "A per-column raycaster built from ordinary boxes — to measure the ceiling BEFORE anyone buys a drawing surface for it.", new Look { Accent = "#FF2D95", Glow = "rgba(255,45,149,0.14)" }, "/corridor", true) {
            AttractCorridor();
          }

          Cabinet("Osy Birds", "A whole 3D valley on one canvas — projected pipe cylinders, a scanline sun, a bird that is nothing but shaded discs. Flap.", new Look { Accent = "#FFC24D", Glow = "rgba(255,194,77,0.14)" }, "/birds", true) {
            AttractBirds();
          }

          Cabinet("Gambit", "An agent as the opponent, thinking out loud over a stream while you take your turn.", new Look { Accent = "#4DFF6E", Glow = "rgba(77,255,110,0.14)" }, "/gambit", false) {
            AttractBoard();
          }

          Cabinet("Arena", "Many players, one floor, live. Waiting on a real message channel — the entity broadcast is signal-only, and stays that way.", new Look { Accent = "#FFC24D", Glow = "rgba(255,194,77,0.14)" }, "/arena", false) {
            AttractArena();
          }

          Cabinet("Tilt", "Out of order. Every arcade has one, and this one is honest about it.", new Look { Accent = "#B45CFF", Glow = "rgba(180,92,255,0.14)" }, "/tilt", false) {
            AttractPinball();
          }
        }

        Stack(gap: 2, align: Align.Center, py: 6) {
          Text("— no coins required —",
               fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim, letterSpacing: "0.3em", textTransform: TextTransform.Uppercase);
          Text("every machine on this floor is compiled Osy#",
               fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, letterSpacing: "0.16em");
        }
      }

      Box(position: Position.Fixed, inset: 0, pointerEvents: PointerEvents.None, z: ZIndex.Overlay,
          bgImage: "repeating-linear-gradient(180deg, rgba(0,0,0,0.16) 0px, rgba(0,0,0,0.16) 1px, rgba(0,0,0,0) 2px, rgba(0,0,0,0) 4px)");
    }
  }
}
model/pages/login.osy77 lines
// THE COIN SLOT — sign in, or take a handle.

[Page("/login")]
[AllowAnonymous]
component LoginPage() {
  meta { title = "Insert coin"; }

  string handle = "";
  string password = "";
  string problem = "";
  bool joining = false;      // false = signing in, true = taking a new handle

  action SignIn() {
    problem = "";
    if (handle == "" || password == "") { problem = "A handle and a password, please."; return; }
    var ticket = Login(handle, password);
    if (ticket == "") { problem = "That handle and password do not match."; return; }
    Session.SignIn(ticket);
    Navigation.Go("/");
  }

  action Join() {
    problem = "";
    if (handle == "") { problem = "Pick a handle — it is what goes on the board."; return; }
    if (password.Length < 4) { problem = "Four characters or more for the password."; return; }
    var ticket = Signup(handle, password);
    if (ticket == "") { problem = "That handle is taken — pick another."; return; }
    Session.SignIn(ticket);
    Navigation.Go("/");
  }

  action Toggle() { joining = !joining; problem = ""; }

  render {
    Box(minH: "100vh", w: "100%", bg: Colors.Bg, color: Colors.OnBg, position: Position.Relative, overflow: Overflow.Hidden,
        bgImage: "radial-gradient(110% 60% at 50% -10%, rgba(180,92,255,0.18) 0%, rgba(7,3,13,0) 60%)") {
      Stack(gap: 5, px: 6, py: 9, align: Align.Center) {

        NeonWord("Insert Coin", "#B45CFF", "34px");
        Text(joining ? "take a handle — it is what goes on the board"
                     : "sign in to put your score on the board",
             fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim, letterSpacing: "0.16em", textTransform: TextTransform.Uppercase);

        Stack(gap: 3, minW: "340px", maxW: "340px", p: 5, bg: Colors.Screen, rounded: Radius.Cab, borderW: 2, border: Colors.Violet,
              shadow: "0 0 40px rgba(180,92,255,0.28), inset 0 0 50px rgba(0,0,0,0.9)") {

          Text("handle", labelFor: handleBox, fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim,
               letterSpacing: "0.28em", textTransform: TextTransform.Uppercase);
          Input(value: handle, id: handleBox, placeholder: "AAA", w: "100%", onEnter: joining ? Join : SignIn);

          Text("password", labelFor: passwordBox, fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim,
               letterSpacing: "0.28em", textTransform: TextTransform.Uppercase);
          Input(value: password, id: passwordBox, type: "password", placeholder: "••••", w: "100%",
                onEnter: joining ? Join : SignIn);

          if (problem != "") {
            Text(problem, fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Pink,
                 textShadow: "0 0 8px rgba(255,45,149,0.7)");
          }

          Pressable(onClick: joining ? Join : SignIn) {
            ArcadeButton(joining ? "Take it" : "Sign in", "#B45CFF");
          }

          Pressable(onClick: Toggle) {
            Text(joining ? "…or sign in with a handle you already have"
                         : "…or take a new handle",
                 fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim, textDecoration: TextDecoration.Underline);
          }
        }

        Link("/") { Text("← back to the floor", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim); }
      }
    }
  }
}
model/pages/minesweeper.osy220 lines
// MINEFIELD — the cabinet that is not about frames.

// ── THE VIEW MODEL ──────────────────────────────────────────────────────────────────────────────────────
class Cell {
  public int At;        // WHICH square — the whole reason this is a class and not a colour string
  public string Face;
  public string Bg;
  public string Fg;
}

[Page("/minefield")]
[AllowAnonymous]
component Minefield() {
  meta { title = "Minefield"; }

  int W = 9;
  int N = 81;
  int MINES = 10;

  int[][] mine = [];
  int[][] adj  = [];
  int[][] cell = [];     // 0 hidden · 1 revealed · 2 flagged

  Cell[] view = [];

  bool laid    = false;   // mines are placed on the FIRST tap, not at load — see Lay()
  bool over    = false;
  bool won     = false;
  int  flags   = 0;
  double clock = 0;
  int  seed    = 12345;

  int Roll(int n) {
    seed = (seed * 75 + 74) % 65537;
    return seed % n;
  }

  /// Place the mines, avoiding the square just tapped AND its neighbours.
  void Lay(int safe) {
    var placed = 0;
    while (placed < MINES) {
      var at = Roll(N);
      var mx = at % W;
      var my = at / W;
      if (mine[my][mx] == 1) { continue; }
      if (Near(at, safe)) { continue; }
      mine[my][mx] = 1;
      placed = placed + 1;
    }
    for (var y = 0; y < W; y = y + 1) {
      for (var x = 0; x < W; x = x + 1) { adj[y][x] = Count(x, y); }
    }
    laid = true;
  }

  bool Near(int a, int b) {
    var dx = (a % W) - (b % W);
    var dy = (a / W) - (b / W);
    if (dx < 0) { dx = -dx; }
    if (dy < 0) { dy = -dy; }
    return dx <= 1 && dy <= 1;
  }

  int Count(int x, int y) {
    var n = 0;
    for (var dy = -1; dy <= 1; dy = dy + 1) {
      for (var dx = -1; dx <= 1; dx = dx + 1) {
        var cx = x + dx;
        var cy = y + dy;
        if (cx < 0 || cy < 0 || cx >= W || cy >= W) { continue; }
        if (mine[cy][cx] == 1) { n = n + 1; }
      }
    }
    return n;
  }

  /// THE FLOOD. Reveals a square, and if it has no neighbouring mine, reveals everything around it — recursively.
  void Flood(int x, int y) {
    if (x < 0 || y < 0 || x >= W || y >= W) { return; }
    if (cell[y][x] != 0) { return; }    // already revealed, or flagged — a flag stops the flood, as it should
    cell[y][x] = 1;
    if (adj[y][x] != 0) { return; }     // a number is a wall: reveal it, do not spread past it
    Flood(x - 1, y - 1); Flood(x, y - 1); Flood(x + 1, y - 1);
    Flood(x - 1, y);                    Flood(x + 1, y);
    Flood(x - 1, y + 1); Flood(x, y + 1); Flood(x + 1, y + 1);
  }

  /// One tap. SHIFT flags instead of revealing.
  action Flag(int at) {
    if (over) { return; }
    var fx = at % W;
    var fy = at / W;
    if (cell[fy][fx] == 1) { return; }
    if (cell[fy][fx] == 2) { cell[fy][fx] = 0; flags = flags - 1; }
    else                   { cell[fy][fx] = 2; flags = flags + 1; }
    view = Compose();
  }

  action Tap(int at) {
    if (over) { return; }

    var tx = at % W;
    var ty = at / W;

    if (Keyboard.Down(Shift)) { Flag(at); return; }

    if (cell[ty][tx] != 0) { return; }
    if (!laid) { Lay(at); }

    if (mine[ty][tx] == 1) {
      over = true;
      for (var y = 0; y < W; y = y + 1) {
        for (var x = 0; x < W; x = x + 1) { if (mine[y][x] == 1) { cell[y][x] = 1; } }
      }
      view = Compose();
      return;
    }

    Flood(tx, ty);

    var hidden = 0;
    for (var y = 0; y < W; y = y + 1) {
      for (var x = 0; x < W; x = x + 1) { if (cell[y][x] != 1 && mine[y][x] == 0) { hidden = hidden + 1; } }
    }
    if (hidden == 0) { won = true; over = true; }

    view = Compose();
  }

  action Restart() {
    var m = new List<int[]>();
    var a = new List<int[]>();
    var c = new List<int[]>();
    for (var y = 0; y < W; y = y + 1) {
      var mr = new List<int>();
      var ar = new List<int>();
      var cr = new List<int>();
      for (var x = 0; x < W; x = x + 1) { mr.Add(0); ar.Add(0); cr.Add(0); }
      m.Add(mr.ToArray()); a.Add(ar.ToArray()); c.Add(cr.ToArray());
    }
    mine = m.ToArray(); adj = a.ToArray(); cell = c.ToArray();
    laid = false; over = false; won = false; flags = 0; clock = 0;
    view = Compose();
  }

  /// The board as buttons. Composed whole on every change rather than read cell-by-cell in render — blockfall's
  /// shape, and for its reason: the loop produces a frame, the renderer paints it, and they never interleave.
  Cell[] Compose() {
    var cells = new List<Cell>();
    for (var i = 0; i < N; i = i + 1) {
      var face = "";
      var bg = "#3B2259";
      var fg = "#F2E9FF";

      var cy = i / W;
      var cx = i % W;
      if (cell[cy][cx] == 2) { face = "⚑"; fg = "#FFC24D"; }
      if (cell[cy][cx] == 1) {
        bg = "#0B0616";
        if (mine[cy][cx] == 1) { face = "✸"; fg = "#FF2D95"; bg = "#3A0A1E"; }
        else if (adj[cy][cx] > 0) { face = $"{adj[cy][cx]}"; fg = Shade(adj[cy][cx]); }
      }
      cells.Add(new Cell { At = i, Face = face, Bg = bg, Fg = fg });
    }
    return cells.ToArray();
  }

  /// The canonical minesweeper number colours, in the arcade's neon rather than Windows 3.1's.
  string Shade(int n) {
    if (n == 1) { return "#22E5FF"; }
    if (n == 2) { return "#4DFF6E"; }
    if (n == 3) { return "#FF2D95"; }
    if (n == 4) { return "#B45CFF"; }
    if (n == 5) { return "#FFC24D"; }
    return "#F2E9FF";
  }

  on mount { Restart(); }

  on frame (double dt) {
    if (laid && !over) { clock = clock + dt; }
  }

  render {
    Box(minH: "100vh", w: "100%", bg: Colors.Bg, color: Colors.OnBg, position: Position.Relative, overflow: Overflow.Hidden,
        bgImage: "radial-gradient(110% 60% at 50% -10%, rgba(77,255,110,0.14) 0%, rgba(7,3,13,0) 60%)") {
      Stack(gap: 4, px: 6, py: 7, align: Align.Center) {

        NeonWord("Minefield", "#4DFF6E", "34px");
        Text("click to clear · RIGHT-CLICK (or shift-click) to flag",
             fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim, letterSpacing: "0.16em", textTransform: TextTransform.Uppercase);

        Row(gap: 6, align: Align.Center) {
          Stat("mines", $"{MINES - flags}");
          Stat("time", $"{(int)clock}");
          Stat("state", over ? (won ? "swept" : "boom") : (laid ? "live" : "ready"));
        }

        Box(keys: [Shift], autoFocus: true,
            display: Display.Grid, cols: "repeat(9, 34px)", rows: "repeat(9, 34px)", gap: 1,
            p: 2, bg: Colors.Screen, rounded: Radius.Cab, borderW: 2, border: Colors.Lime,
            shadow: "0 0 40px rgba(77,255,110,0.26), inset 0 0 50px rgba(0,0,0,0.9)") {
          foreach (var c in view) {
            Pressable(onClick: () => Tap(c.At), onContextMenu: () => Flag(c.At),
                      bg: c.Bg, color: c.Fg, rounded: Radius.Chip, borderW: 0,
                      fontFamily: Font.Mono, fontSize: FontSize.Small, fontWeight: FontWeight.Bold) {
              Text(c.Face);
            }
          }
        }

        Row(gap: 3, align: Align.Center) {
          Pressable(onClick: Restart) { ArcadeButton("New board", "#4DFF6E"); }
          Link("/") { ArcadeButton("Back to lobby", "#B45CFF"); }
        }
      }
    }
  }
}
model/pages/ops.osy162 lines
// THE OPS RIG — how much INTERPRETED WORK fits in one frame, and where 60fps breaks?

[Page("/ops")]
[AllowAnonymous]
component Ops() {
  meta { title = "Ops"; }

  int units = 1000;         // units of interpreted work per frame
  int mode = 0;             // 0 arithmetic · 1 call · 2 buffer · 3 bare (arithmetic + an empty call) · 4 floor
  double sink = 0;          // the result, RENDERED — so no part of the loop is dead code

  int frames = 0;
  double window = 0;
  double fps = 0;

  int TEX = 64;
  int FB = 4096;
  List<int> tex = new List<int>();
  List<int> fb = new List<int>();

  on mount {
    for (var i = 0; i < TEX * TEX; i = i + 1) { tex.Add(i * 7 % 255); }
    for (var i = 0; i < FB; i = i + 1) { fb.Add(0); }
  }

  on frame (double dt) {
    frames = frames + 1;
    window = window + dt;
    if (window >= 0.5) {
      fps = Math.Round(frames / window);
      frames = 0;
      window = 0;
    }

    var a = sink;
    if (mode == 0) {
      for (var i = 0; i < units; i = i + 1) {
        a = a + i * 0.001;
        if (a > 1000.0) { a = a - 1000.0; }
      }
    }
    if (mode == 1) {
      for (var i = 0; i < units; i = i + 1) {
        a = Step(a, i);
      }
    }
    if (mode == 2) {
      var buf = new List<double>();
      for (var i = 0; i < units; i = i + 1) {
        a = a + i * 0.001;
        if (a > 1000.0) { a = a - 1000.0; }
        buf.Add(a);
      }
      a = a + buf.Count * 0.0;   // consume the buffer so it cannot be treated as unreachable
    }
    if (mode == 3) {
      for (var i = 0; i < units; i = i + 1) {
        a = a + i * 0.001;
        if (a > 1000.0) { a = a - 1000.0; }
        var z = Zero();
      }
    }
    if (mode == 4) {
      var fx = a;
      var fy = a * 0.5;
      for (var i = 0; i < units; i = i + 1) {
        fx = fx + 0.013;
        fy = fy + 0.007;
        var cx = (int)fx;
        var cy = (int)fy;
        var tx = (int)(64.0 * (fx - cx)) % 64;
        var ty = (int)(64.0 * (fy - cy)) % 64;
        fb[i % FB] = tex[64 * ty + tx];
      }
      a = a + 0.001 + fb[0] * 0.000001;
      if (a > 1000.0) { a = a - 1000.0; }
    }
    sink = a;
  }

  /// ONE UNIT, as a method — byte-for-byte the arithmetic of the inline branch above. The only difference between
  /// workload 0 and workload 1 is that this call happens.
  double Step(double a, int i) {
    var v = a + i * 0.001;
    if (v > 1000.0) { v = v - 1000.0; }
    return v;
  }

  /// The emptiest callee a call can have — one `return`, no parameters, no arithmetic. Workload 3 is workload 0 plus
  /// one of these per unit, so subtracting the two per-unit TIMES leaves the call MACHINERY on its own: the throw,
  /// the frame push, the parameter binding, the savepoint, the memo park, and the caller's statement replay. Without
  /// it, "a call costs 5.3x an inline unit" cannot be split into what the callee's own body costs and what the
  /// interpreter charges to get there — and only the second half is something an optimisation can remove.
  double Zero() { return 0.0; }

  action SetUnits(int n) { units = n; frames = 0; window = 0; fps = 0; }
  action SetMode(int m)  { mode = m;  frames = 0; window = 0; fps = 0; }

  render {
    Box(minH: "100vh", w: "100%", bg: Colors.Bg, color: Colors.OnBg, position: Position.Relative, overflow: Overflow.Hidden,
        bgImage: "radial-gradient(110% 60% at 50% -10%, rgba(34,229,255,0.16) 0%, rgba(7,3,13,0) 60%)") {
      Stack(gap: 5, px: 6, py: 8, align: Align.Center, maxW: "1100px", mx: "auto") {

        NeonWord("Ops", "#22E5FF", "34px");
        Text("interpreted work per frame — the DOM is held still",
             fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim, letterSpacing: "0.18em", textTransform: TextTransform.Uppercase);

        Row(gap: 6, align: Align.Center) {
          Stack(gap: 1, align: Align.Center) {
            Text($"{fps}", fontFamily: Font.Display, fontSize: "58px", fontWeight: FontWeight.Black, color: Colors.OnBg,
                 textShadow: "0 0 8px #22E5FF, 0 0 28px #22E5FF");
            Text("frames / sec", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, letterSpacing: "0.24em",
                 textTransform: TextTransform.Uppercase);
          }
          Stack(gap: 1, align: Align.Center) {
            Text($"{units}", fontFamily: Font.Display, fontSize: "34px", fontWeight: FontWeight.Black, color: Colors.Cyan);
            Text("units / frame", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, letterSpacing: "0.24em",
                 textTransform: TextTransform.Uppercase);
          }
          Stack(gap: 1, align: Align.Center) {
            Text(mode == 0 ? "arith" : (mode == 1 ? "call" : (mode == 2 ? "buffer" : (mode == 3 ? "bare" : "floor"))),
                 fontFamily: Font.Display, fontSize: "34px", fontWeight: FontWeight.Black, color: Colors.Pink);
            Text("workload", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, letterSpacing: "0.24em",
                 textTransform: TextTransform.Uppercase);
          }
        }

        Row(gap: 2) {
          Pressable(onClick: () => SetMode(0)) { OpsChip("arith"); }
          Pressable(onClick: () => SetMode(1)) { OpsChip("call"); }
          Pressable(onClick: () => SetMode(2)) { OpsChip("buffer"); }
          Pressable(onClick: () => SetMode(3)) { OpsChip("bare"); }
          Pressable(onClick: () => SetMode(4)) { OpsChip("floor"); }
        }

        Row(gap: 2) {
          Pressable(onClick: () => SetUnits(100))    { OpsChip("100"); }
          Pressable(onClick: () => SetUnits(300))    { OpsChip("300"); }
          Pressable(onClick: () => SetUnits(1000))   { OpsChip("1000"); }
          Pressable(onClick: () => SetUnits(3000))   { OpsChip("3000"); }
          Pressable(onClick: () => SetUnits(10000))  { OpsChip("10000"); }
          Pressable(onClick: () => SetUnits(30000))  { OpsChip("30000"); }
          Pressable(onClick: () => SetUnits(100000)) { OpsChip("100000"); }
          Pressable(onClick: () => SetUnits(300000)) { OpsChip("300000"); }
        }

        Text($"acc {sink}", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim);

        Link("/") { Text("← back to the floor", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim); }
      }
    }
  }
}

[Composable] component OpsChip(string label) {
  render {
    Box(px: 3, py: 2, rounded: Radius.Chip, bg: Colors.CabinetLip, borderW: 1, border: Colors.Violet) {
      Text(label, fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.OnBg, letterSpacing: "0.12em");
    }
  }
}
model/pages/osystein.osy316 lines
// OSYSTEIN — the same raycaster as `/corridor`, drawn on a CANVAS instead of out of atoms.

[Page("/osystein")]
[AllowAnonymous]
component Osystein() {
  meta { title = "Osystein"; }

  int[] map = [
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,2,2,0,0,0,0,0,0,0,0,3,3,0,1,
    1,0,2,0,0,0,0,0,0,0,0,0,0,3,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,0,0,0,4,4,0,0,4,4,0,0,0,0,1,
    1,0,0,0,0,4,0,0,0,0,4,0,0,0,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,0,0,0,4,0,0,0,0,4,0,0,0,0,1,
    1,0,0,0,0,4,4,0,0,4,4,0,0,0,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,3,0,0,0,0,0,0,0,0,0,0,2,0,1,
    1,0,3,3,0,0,0,0,0,0,0,0,2,2,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
  ];

  double posX = 8.0;
  double posY = 14.0;
  double dirX = 0;
  double dirY = -1;
  double planeX = 0.66;
  double planeY = 0;
  double moveSpeed = 2.4;
  double turnSpeed = 2.4;

  int COLS = 160;
  int COLW = 4;
  int W = 640;
  int H = 400;
  int TEX = 64;
  int FW = 640;
  int FH = 200;
  List<int> floorBuf = new List<int>();
  List<int> floorTex = new List<int>();
  double WRAP = 1024.0;

  double[] zbuf = [];

  double[] lampX = [3.5, 12.5, 8.0];
  double[] lampY = [3.5, 12.5, 8.0];

  double fps = 0;
  int frames = 0;
  double window = 0;

  on mount {
    ResetZ();
    for (var i = 0; i < FW * FH; i = i + 1) { floorBuf.Add(0); }
  }

  on frame (double dt) {
    frames = frames + 1;
    window = window + dt;
    if (window >= 0.5) { fps = Math.Round(frames / window); frames = 0; window = 0; }

    if (Keyboard.Down(Left))  { Turn(-turnSpeed * dt); }
    if (Keyboard.Down(Right)) { Turn(turnSpeed * dt); }

    var fwd = 0.0;
    if (Keyboard.Down(W) || Keyboard.Down(Up))   { fwd = moveSpeed * dt; }
    if (Keyboard.Down(S) || Keyboard.Down(Down)) { fwd = fwd - moveSpeed * dt; }
    var strafe = 0.0;
    if (Keyboard.Down(A)) { strafe = -moveSpeed * dt; }
    if (Keyboard.Down(D)) { strafe = moveSpeed * dt; }
    if (fwd != 0 || strafe != 0) { TryMove(dirX * fwd + planeX * strafe, dirY * fwd + planeY * strafe); }

    DrawGround();
    DrawWalls();
    DrawLamps();
    DrawHud();
  }

  void DrawGround() {
    var half = H / 2;

    for (var y = 1; y < half; y = y + 1) {
      var rowDist = 200.0 / y;
      var shade = Math.Clamp(1.0 - rowDist * 0.055, 0.06, 1.0);
      Draw.Rect(0, half - y, W, 1, Grey(shade * 0.20, shade * 0.16, shade * 0.34));
    }

    if (floorTex.Count < TEX * TEX) {
      floorTex = Texture.Pixels(wall2);
      if (floorTex.Count < TEX * TEX) { return; }
    }

    var rowStep = (H / 2.0) / FH;
    for (var r = 0; r < FH; r = r + 1) {
      var screenY = (r + 1) * rowStep;
      var rowDist = 200.0 / screenY;

      var leftX = dirX - planeX;
      var leftY = dirY - planeY;
      var stepX = rowDist * ((dirX + planeX) - leftX) / FW;
      var stepY = rowDist * ((dirY + planeY) - leftY) / FW;

      var fx = posX + rowDist * leftX + WRAP;
      var fy = posY + rowDist * leftY + WRAP;
      var row = r * FW;

      for (var x = 0; x < FW; x = x + 1) {
        var cx = (int)fx;
        var cy = (int)fy;
        floorBuf[row + x] = floorTex[TEX * (int)(TEX * (fy - cy)) + (int)(TEX * (fx - cx))];
        fx = fx + stepX;
        fy = fy + stepY;
      }
    }

    Draw.Pixels(floorBuf, FW, FH, 0, half, W, half);

    for (var r = 0; r < FH; r = r + 1) {
      var d = 200.0 / ((r + 1) * rowStep);
      var dark = Math.Clamp(d * 0.055, 0.0, 0.88);
      Draw.Rect(0, half + r * rowStep, W, rowStep + 1, Shade(dark));
    }
  }

  void DrawWalls() {
    var next = new List<double>();
    for (var c = 0; c < COLS; c = c + 1) {
      var cameraX = 2.0 * c / COLS - 1.0;
      var rayX = dirX + planeX * cameraX;
      var rayY = dirY + planeY * cameraX;

      var mapX = Cell(posX);
      var mapY = Cell(posY);

      var ax = Math.Abs(rayX);
      var ay = Math.Abs(rayY);
      var deltaX = ax < 0.000001 ? 1000000.0 : 1.0 / ax;
      var deltaY = ay < 0.000001 ? 1000000.0 : 1.0 / ay;

      var stepX = 0;
      var stepY = 0;
      var sideX = 0.0;
      var sideY = 0.0;
      if (rayX < 0) { stepX = -1; sideX = (posX - mapX) * deltaX; }
      else          { stepX = 1;  sideX = (mapX + 1.0 - posX) * deltaX; }
      if (rayY < 0) { stepY = -1; sideY = (posY - mapY) * deltaY; }
      else          { stepY = 1;  sideY = (mapY + 1.0 - posY) * deltaY; }

      var hit = 0;
      var side = 0;
      var guard = 0;
      while (hit == 0 && guard < 64) {
        guard = guard + 1;
        if (sideX < sideY) { sideX = sideX + deltaX; mapX = mapX + stepX; side = 0; }
        else               { sideY = sideY + deltaY; mapY = mapY + stepY; side = 1; }
        hit = At(mapX, mapY);
      }

      var dist = side == 0
        ? (mapX - posX + (1 - stepX) / 2.0) / rayX
        : (mapY - posY + (1 - stepY) / 2.0) / rayY;
      if (dist < 0.05) { dist = 0.05; }
      next.Add(dist);

      var lineH = 400.0 / dist;
      var top = H / 2 - lineH / 2;
      if (top < 0) { top = 0; }
      var bot = H / 2 + lineH / 2;
      if (bot > H) { bot = H; }
      var drawn = bot - top;
      if (drawn < 1) { drawn = 1; }

      var wallX = side == 0 ? posY + dist * rayY : posX + dist * rayX;
      wallX = wallX - Math.Floor(wallX);

      var lit = 1.0 - dist * 0.075;
      if (side == 1) { lit = lit - 0.22; }
      if (lit < 0.10) { lit = 0.10; }

      var texX = (int)(wallX * TEX);
      if (texX < 0) { texX = 0; }
      if (texX > TEX - 1) { texX = TEX - 1; }
      DrawWallColumn(hit, texX, c * COLW, top, drawn);

      Draw.Rect(c * COLW, top, COLW, drawn, Shade(1.0 - lit));
    }
    zbuf = next;
  }

  void DrawWallColumn(int kind, int texX, double x, double top, double h) {
    if (kind == 2) { Draw.Image(Textures.Wall2, texX, 0, 1, TEX, x, top, COLW, h); return; }
    if (kind == 3) { Draw.Image(Textures.Wall3, texX, 0, 1, TEX, x, top, COLW, h); return; }
    if (kind == 4) { Draw.Image(Textures.Wall4, texX, 0, 1, TEX, x, top, COLW, h); return; }
    Draw.Image(Textures.Wall1, texX, 0, 1, TEX, x, top, COLW, h);
  }

  string Shade(double a) {
    if (a < 0) { a = 0; }
    if (a > 0.92) { a = 0.92; }
    return "rgba(0,0,0," + Math.Round(a * 100) / 100.0 + ")";
  }

  void DrawLamps() {
    if (zbuf.Count < COLS) { return; }
    for (var i = 0; i < 3; i = i + 1) {
      var sx = lampX[i] - posX;
      var sy = lampY[i] - posY;

      var det = planeX * dirY - dirX * planeY;
      if (det > -0.000001 && det < 0.000001) { continue; }
      var inv = 1.0 / det;
      var tx = inv * (dirY * sx - dirX * sy);
      var depth = inv * (planeX * sy - planeY * sx);
      if (depth < 0.2) { continue; }                       // behind the camera, or on top of it

      var screenX = (W / 2.0) * (1.0 + tx / depth);
      var size = 300.0 / depth;
      if (size > 700.0) { size = 700.0; }
      var top = H / 2.0 - size / 2.0;

      var lit = 1.0 - depth * 0.07;
      if (lit < 0.12) { lit = 0.12; }

      var startC = (int)((screenX - size / 2.0) / COLW);
      var endC = (int)((screenX + size / 2.0) / COLW);
      for (var c = startC; c <= endC; c = c + 1) {
        if (c < 0 || c >= COLS) { continue; }
        if (depth >= zbuf[c]) { continue; }                // ← THE Z-TEST: a wall is nearer in this column

        var u = (c * COLW + COLW / 2.0 - (screenX - size / 2.0)) / size;
        var dx = (u - 0.5) * 2.0;
        var r2 = dx * dx;
        if (r2 > 1.0) { continue; }
        var hHalf = size * 0.5 * Math.Sqrt(1.0 - r2);
        Draw.Rect(c * COLW, top + size * 0.5 - hHalf, COLW, hHalf * 2.0, Grey(lit * 1.0, lit * 0.82, lit * 0.30));
      }
    }
  }

  void DrawHud() {
    Draw.Rect(0, 0, W, 34, "rgba(11,6,22,0.72)");
    Draw.Text("OSYSTEIN", 12, 8, "#FFC24D", 20);
    Draw.Text($"{fps} FPS", W - 90, 10, "#22E5FF", 16);
    Draw.Text("WASD / ARROWS", W / 2 - 60, 12, "#7A6A94", 12);
  }

  void ResetZ() {
    var z = new List<double>();
    for (var i = 0; i < COLS; i = i + 1) { z.Add(1000.0); }
    zbuf = z;
  }

  void Turn(double a) {
    double c = Math.Cos(a);
    double s = Math.Sin(a);
    var oldDirX = dirX;
    dirX = dirX * c - dirY * s;
    dirY = oldDirX * s + dirY * c;
    var oldPlaneX = planeX;
    planeX = planeX * c - planeY * s;
    planeY = oldPlaneX * s + planeY * c;
  }

  void TryMove(double dx, double dy) {
    var nx = posX + dx;
    var ny = posY + dy;
    var mx = dx > 0 ? 0.2 : (dx < 0 ? -0.2 : 0.0);
    var my = dy > 0 ? 0.2 : (dy < 0 ? -0.2 : 0.0);
    if (!Solid(nx + mx, posY)) { posX = nx; }
    if (!Solid(posX, ny + my)) { posY = ny; }
  }

  bool Solid(double x, double y) { return At(Cell(x), Cell(y)) > 0; }

  int Cell(double v) { return (int)Math.Floor(v); }

  int At(int mx, int my) {
    if (mx < 0 || my < 0 || mx > 15 || my > 15) { return 1; }
    return map[my * 16 + mx];
  }

  string Material(int kind, double f) {
    if (f < 0.04) { f = 0.04; }
    if (kind == 2) { return Grey(f * 0.42, f * 0.78, f * 0.52); }
    if (kind == 3) { return Grey(f * 0.40, f * 0.56, f * 0.92); }
    if (kind == 4) { return Grey(f * 0.92, f * 0.66, f * 0.34); }
    return Grey(f * 0.86, f * 0.32, f * 0.46);
  }

  string Grey(double r, double g, double b) {
    return "rgb(" + Math.Round(r * 255) + "," + Math.Round(g * 255) + "," + Math.Round(b * 255) + ")";
  }

  render {
    Box(minH: "100vh", w: "100%", bg: Colors.Bg, color: Colors.OnBg, p: 6) {
      Stack(gap: 4, align: Align.Center) {
        NeonWord("Osystein", "#FF2D95", "30px");
        Text("the same raycaster as /corridor — one canvas instead of 160 elements",
             fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim, letterSpacing: "0.14em", textTransform: TextTransform.Uppercase);
        Box(keys: [Left, Right, Up, Down, W, A, S, D], rounded: Radius.Cab, overflow: Overflow.Hidden, lineHeight: "0") {
          Canvas(w: 640, h: 400, borderW: 2, border: Colors.Pink, maxW: "100%",
                 fontFamily: Font.Display, shadow: "0 0 40px rgba(255,45,149,0.25)");
        }
        Row(gap: 4) {
          Link("/corridor") { Text("↔ the atoms version", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Cyan); }
          Link("/") { Text("← back to the floor", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim); }
        }
      }
    }
  }
}
model/pages/paint.osy54 lines
// THE FIRST CANVAS — the smallest thing that proves the draw surface is real, and moving.

[Page("/paint")]
[AllowAnonymous]
component Paint() {
  meta { title = "Paint"; }

  double t = 0;
  double ballX = 60;
  double ballY = 60;
  double vx = 210;
  double vy = 160;

  on frame (double dt) {
    t = t + dt;

    ballX = ballX + vx * dt;
    ballY = ballY + vy * dt;
    if (ballX < 24) { ballX = 24; vx = 0 - vx; }
    if (ballX > 616) { ballX = 616; vx = 0 - vx; }
    if (ballY < 24) { ballY = 24; vy = 0 - vy; }
    if (ballY > 336) { ballY = 336; vy = 0 - vy; }

    Draw.Clear("#0B0616");

    for (var i = 0; i < 20; i = i + 1) {
      var h = 40 + i * 6;
      Draw.Rect(i * 32, 360 - h, 31, h, i % 2 == 0 ? "#2A1B3D" : "#3A2551");
    }

    Draw.Circle(ballX, ballY, 22, "#22E5FF");
    Draw.Circle(ballX - 6, ballY - 7, 7, "#B9F6FF");

    Draw.Line(320, 180, 320 + Cos(t) * 120, 180 + Sin(t) * 120, "#FF2D95", 4);

    Draw.Text("DRAWN BY OSY#", 18, 16, "#FFC24D", 22);
    Draw.Text($"t {Math.Round(t)}s", 18, 44, "#7A6A94", 14);
  }

  double Cos(double a) { double c = Math.Cos(a); return c; }
  double Sin(double a) { double c = Math.Sin(a); return c; }

  render {
    Box(minH: "100vh", w: "100%", bg: Colors.Bg, color: Colors.OnBg, p: 6) {
      Stack(gap: 4, align: Align.Center) {
        NeonWord("Paint", "#22E5FF", "34px");
        Canvas(w: 640, h: 360, rounded: Radius.Cab, borderW: 2, border: Colors.Pink,
               shadow: "0 0 40px rgba(255,45,149,0.25)", maxW: "100%", fontFamily: Font.Display);
        Link("/") { Text("← back to the floor", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim); }
      }
    }
  }
}
model/pages/paintops.osy99 lines
// THE RENDER RIG — how much INTERPRETED WORK fits in one frame when it is written in `render` rather than in
// `on frame`, and what a CALL costs there.

[Page("/paintops")]
[AllowAnonymous]
component PaintOps() {
  meta { title = "PaintOps"; }

  int rows = 100;           // rows rendered per frame — the SAME in both modes
  int mode = 0;             // 0 plain (no call) · 1 call
  int tick = 0;             // bumped every frame, READ by both modes, so the tree re-renders either way

  int frames = 0;
  double window = 0;
  double fps = 0;

  List<int> items = new List<int>();

  on mount { Rebuild(); }

  on frame (double dt) {
    frames = frames + 1;
    window = window + dt;
    if (window >= 0.5) {
      fps = Math.Round(frames / window);
      frames = 0;
      window = 0;
    }
    tick = tick + 1;
  }

  /// The CALL arm's callee: straight-line by construction (see the header — a loop here would lower it and the
  /// measurement would quietly become a measurement of compiled JS).
  string Label(int r, int t) { return $"{r} {t}"; }

  action Rebuild() {
    var next = new List<int>();
    for (var i = 0; i < rows; i = i + 1) { next.Add(i); }
    items = next;
    frames = 0; window = 0; fps = 0;
  }

  action SetRows(int n) { rows = n; Rebuild(); }
  action SetMode(int m) { mode = m; frames = 0; window = 0; fps = 0; }

  render {
    Box(minH: "100vh", w: "100%", bg: Colors.Bg, color: Colors.OnBg, overflow: Overflow.Hidden) {
      Stack(gap: 3, px: 4, py: 4, maxW: "1100px", mx: "auto") {

        Text("PaintOps", fontFamily: Font.Display, fontSize: "28px", fontWeight: FontWeight.Black, color: Colors.Cyan);
        Text("interpreted work in RENDER — both arms paint the same node count",
             fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, letterSpacing: "0.18em", textTransform: TextTransform.Uppercase);

        Row(gap: 5, align: Align.Center) {
          Stack(gap: 1, align: Align.Center) {
            Text($"{fps}", fontFamily: Font.Display, fontSize: "42px", fontWeight: FontWeight.Black, color: Colors.OnBg);
            Text("frames / sec", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, textTransform: TextTransform.Uppercase);
          }
          Stack(gap: 1, align: Align.Center) {
            Text($"{rows}", fontFamily: Font.Display, fontSize: "28px", fontWeight: FontWeight.Black, color: Colors.Cyan);
            Text("rows / frame", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, textTransform: TextTransform.Uppercase);
          }
          Stack(gap: 1, align: Align.Center) {
            Text(mode == 0 ? "plain" : "call", fontFamily: Font.Display, fontSize: "28px", fontWeight: FontWeight.Black, color: Colors.Pink);
            Text("mode", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, textTransform: TextTransform.Uppercase);
          }
        }

        Row(gap: 2) {
          Pressable(onClick: () => SetMode(0)) { OpsChip("plain"); }
          Pressable(onClick: () => SetMode(1)) { OpsChip("call"); }
        }
        Row(gap: 2) {
          Pressable(onClick: () => SetRows(100))   { OpsChip("100"); }
          Pressable(onClick: () => SetRows(300))   { OpsChip("300"); }
          Pressable(onClick: () => SetRows(1000))  { OpsChip("1000"); }
          Pressable(onClick: () => SetRows(3000))  { OpsChip("3000"); }
          Pressable(onClick: () => SetRows(6000))  { OpsChip("6000"); }
          Pressable(onClick: () => SetRows(10000)) { OpsChip("10000"); }
          Pressable(onClick: () => SetRows(16000)) { OpsChip("16000"); }
        }

        Box(display: Display.Grid, cols: "repeat(auto-fill, minmax(70px, 1fr))", gap: 0, w: "100%") {
          if (mode == 0) {
            foreach (var r in items) {
              Text($"{r} {tick}", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim);
            }
          }
          if (mode == 1) {
            foreach (var r in items) {
              Text(Label(r, tick), fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim);
            }
          }
        }
      }
    }
  }
}
model/pages/scores.osy92 lines
// THE BOARD — the top ten, and where you sit on it.

[Page("/scores")]
[AllowAnonymous]
component Scores() {
  meta { title = "High scores"; }

  live var top = Score.Where(s => s.Game == Game.Blockfall)
                      .OrderByDescending(s => s.Points)
                      .ThenBy(s => s.AchievedAt)      // an equal score that came FIRST ranks first
                      .Take(10)
                      .Include(s => s.Player);

  live var birds = Score.Where(s => s.Game == Game.OsyBirds)
                        .OrderByDescending(s => s.Points)
                        .ThenBy(s => s.AchievedAt)
                        .Take(10)
                        .Include(s => s.Player);

  var me = Session.CurrentUser;

  render {
    Box(minH: "100vh", w: "100%", bg: Colors.Bg, color: Colors.OnBg, position: Position.Relative, overflow: Overflow.Hidden,
        bgImage: "radial-gradient(110% 60% at 50% -10%, rgba(255,194,77,0.16) 0%, rgba(7,3,13,0) 60%)") {
      Stack(gap: 5, px: 6, py: 8, align: Align.Center) {

        NeonWord("High Scores", "#FFC24D", "34px");

        Row(gap: 6, align: Align.Start, justify: Justify.Center, wrap: Wrapping.Wrap) {

          Stack(gap: 3, align: Align.Center) {
            Text("blockfall — top ten", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim,
                 letterSpacing: "0.24em", textTransform: TextTransform.Uppercase);
            Stack(gap: 2, minW: "460px", maxW: "460px", p: 5, bg: Colors.Screen, rounded: Radius.Cab, borderW: 2, border: Colors.Amber,
                  shadow: "0 0 40px rgba(255,194,77,0.22), inset 0 0 50px rgba(0,0,0,0.9)") {

              if (top.Count == 0) {
                Text("no scores yet — the board is yours to open",
                     fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim, letterSpacing: "0.12em");
              }

              foreach (var s in top) {
                Row(gap: 4, align: Align.Center, justify: Justify.SpaceBetween, px: 3, py: 2, rounded: Radius.Chip, bg: "#0B0616") {
                  Text(s.Player.Handle, fontFamily: Font.Display, fontSize: FontSize.Small, fontWeight: FontWeight.Black, color: Colors.OnBg,
                       letterSpacing: "0.16em", textTransform: TextTransform.Uppercase);
                  Row(gap: 4, align: Align.Center) {
                    Text($"{s.Lines} lines", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim);
                    Text($"lv {s.Level}", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim);
                    Text($"{s.Points}", fontFamily: Font.Display, fontSize: FontSize.Head, fontWeight: FontWeight.Black, color: Colors.Cyan,
                         textShadow: "0 0 10px #22E5FF");
                  }
                }
              }
            }
          }

          Stack(gap: 3, align: Align.Center) {
            Text("osy birds — top ten", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim,
                 letterSpacing: "0.24em", textTransform: TextTransform.Uppercase);
            Stack(gap: 2, minW: "460px", maxW: "460px", p: 5, bg: Colors.Screen, rounded: Radius.Cab, borderW: 2, border: Colors.Cyan,
                  shadow: "0 0 40px rgba(34,229,255,0.22), inset 0 0 50px rgba(0,0,0,0.9)") {

              if (birds.Count == 0) {
                Text("no flights logged — the sky is yours to open",
                     fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim, letterSpacing: "0.12em");
              }

              foreach (var s in birds) {
                Row(gap: 4, align: Align.Center, justify: Justify.SpaceBetween, px: 3, py: 2, rounded: Radius.Chip, bg: "#0B0616") {
                  Text(s.Player.Handle, fontFamily: Font.Display, fontSize: FontSize.Small, fontWeight: FontWeight.Black, color: Colors.OnBg,
                       letterSpacing: "0.16em", textTransform: TextTransform.Uppercase);
                  Text($"{s.Points}", fontFamily: Font.Display, fontSize: FontSize.Head, fontWeight: FontWeight.Black, color: Colors.Amber,
                       textShadow: "0 0 10px #FFC24D");
                }
              }
            }
          }
        }

        if (me == null) {
          Stack(gap: 2, align: Align.Center) {
            Text("scores are saved for signed-in players", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim);
            Link("/login") { ArcadeButton("Insert coin", "#B45CFF"); }
          }
        }

        Link("/") { Text("← back to the floor", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim); }
      }
    }
  }
}
model/pages/stress.osy107 lines
// THE STRESS RIG — how many cells can the render engine repaint per frame, and at what frame rate?

[Page("/stress")]
[AllowAnonymous]
component Stress() {
  meta { title = "Stress"; }

  string[] cells = [];
  int count = 120;          // the raycaster's column count, as the default — this is the number that decides D214

  int frames = 0;
  double window = 0;
  double fps = 0;
  double worstFrame = 0;    // the longest single frame in the window, in ms — where the stutter lives

  double phase = 0;

  on mount { Resize(120); }

  action Resize(int n) {
    count = n;
    var next = new List<string>();
    for (var i = 0; i < n; i = i + 1) { next.Add("#22E5FF"); }
    cells = next;
    frames = 0; window = 0; fps = 0; worstFrame = 0;
  }

  on frame (double dt) {
    frames = frames + 1;
    window = window + dt;
    if (dt * 1000 > worstFrame) { worstFrame = dt * 1000; }
    if (window >= 0.5) {
      fps = Math.Round(frames / window);
      frames = 0;
      window = 0;
      worstFrame = 0;
    }

    phase = phase + dt;
    var next = new List<string>();
    for (var i = 0; i < count; i = i + 1) {
      next.Add(Hue(i, phase));
    }
    cells = next;
  }

  string Hue(int i, double t) {
    var v = (i * 7 + Math.Round(t * 120)) % 360;
    return "hsl(" + v + " 90% 60%)";
  }

  render {
    Box(minH: "100vh", w: "100%", bg: Colors.Bg, color: Colors.OnBg, position: Position.Relative, overflow: Overflow.Hidden,
        bgImage: "radial-gradient(110% 60% at 50% -10%, rgba(255,45,149,0.16) 0%, rgba(7,3,13,0) 60%)") {
      Stack(gap: 5, px: 6, py: 8, align: Align.Center, maxW: "1100px", mx: "auto") {

        NeonWord("Stress", "#FF2D95", "34px");
        Text("every cell repaints every frame — the worst case, on purpose",
             fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim, letterSpacing: "0.18em", textTransform: TextTransform.Uppercase);

        Row(gap: 6, align: Align.Center) {
          Stack(gap: 1, align: Align.Center) {
            Text($"{fps}", fontFamily: Font.Display, fontSize: "58px", fontWeight: FontWeight.Black, color: Colors.OnBg,
                 textShadow: "0 0 8px #FF2D95, 0 0 28px #FF2D95");
            Text("frames / sec", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, letterSpacing: "0.24em",
                 textTransform: TextTransform.Uppercase);
          }
          Stack(gap: 1, align: Align.Center) {
            Text($"{count}", fontFamily: Font.Display, fontSize: "34px", fontWeight: FontWeight.Black, color: Colors.Cyan);
            Text("cells repainted", fontFamily: Font.Mono, fontSize: FontSize.Micro, color: Colors.Dim, letterSpacing: "0.24em",
                 textTransform: TextTransform.Uppercase);
          }
        }

        Row(gap: 2) {
          Pressable(onClick: () => Resize(60))   { SizeChip("60"); }
          Pressable(onClick: () => Resize(120))  { SizeChip("120"); }
          Pressable(onClick: () => Resize(240))  { SizeChip("240"); }
          Pressable(onClick: () => Resize(480))  { SizeChip("480"); }
          Pressable(onClick: () => Resize(960))  { SizeChip("960"); }
          Pressable(onClick: () => Resize(1920)) { SizeChip("1920"); }
          Pressable(onClick: () => Resize(3840)) { SizeChip("3840"); }
          Pressable(onClick: () => Resize(7680)) { SizeChip("7680"); }
        }

        Row(gap: 0, w: "100%", h: "260px", align: Align.Stretch,
            bg: Colors.Screen, rounded: Radius.Cab, overflow: Overflow.Hidden, borderW: 2, border: Colors.Pink,
            shadow: "0 0 40px rgba(255,45,149,0.25), inset 0 0 50px rgba(0,0,0,0.9)") {
          foreach (var c in cells) {
            Box(grow: 1, bg: c);
          }
        }

        Link("/") { Text("← back to the floor", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim); }
      }
    }
  }
}

[Composable] component SizeChip(string label) {
  render {
    Box(px: 3, py: 2, rounded: Radius.Chip, bg: Colors.CabinetLip, borderW: 1, border: Colors.Violet) {
      Text(label, fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.OnBg, letterSpacing: "0.12em");
    }
  }
}
model/pages/wolf.osy264 lines
// WOLF — a WHOLE software-rendered frame, in Osy#. Every pixel on the screen is written by this file's arithmetic.

[Page("/wolf")]
[AllowAnonymous]
component Wolf() {
  meta { title = "Wolf"; }

  int[] map = [
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,2,2,0,0,0,0,0,0,0,0,3,3,0,1,
    1,0,2,0,0,0,0,0,0,0,0,0,0,3,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,0,0,0,4,4,0,0,4,4,0,0,0,0,1,
    1,0,0,0,0,4,0,0,0,0,4,0,0,0,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,0,0,0,4,0,0,0,0,4,0,0,0,0,1,
    1,0,0,0,0,4,4,0,0,4,4,0,0,0,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,0,3,0,0,0,0,0,0,0,0,0,0,2,0,1,
    1,0,3,3,0,0,0,0,0,0,0,0,2,2,0,1,
    1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
  ];

  double posX = 8.0;
  double posY = 14.0;
  double dirX = 0;
  double dirY = -1;
  double planeX = 0.66;
  double planeY = 0;
  double moveSpeed = 2.4;
  double turnSpeed = 2.4;

  int W = 1280;
  int H = 800;
  int HALF = 400;
  int TEX = 64;
  int TEXMASK = 63;

  List<int> buf = new List<int>();
  double[] zbuf = [];

  List<int> tex1 = new List<int>();
  List<int> tex2 = new List<int>();
  List<int> tex3 = new List<int>();
  List<int> tex4 = new List<int>();

  double fps = 0;
  int frames = 0;
  double window = 0;

  on mount {
    var z = new List<double>();
    for (var i = 0; i < W; i = i + 1) { z.Add(1000000.0); }
    zbuf = z;
    for (var i = 0; i < W * H; i = i + 1) { buf.Add(0); }
  }

  on frame (double dt) {
    frames = frames + 1;
    window = window + dt;
    if (window >= 0.5) { fps = Math.Round(frames / window); frames = 0; window = 0; }

    if (Keyboard.Down(Left))  { Turn(-turnSpeed * dt); }
    if (Keyboard.Down(Right)) { Turn(turnSpeed * dt); }
    if (Keyboard.Down(Up))    { Walk(moveSpeed * dt); }
    if (Keyboard.Down(Down))  { Walk(-moveSpeed * dt); }
    if (Keyboard.Down(W))     { Walk(moveSpeed * dt); }
    if (Keyboard.Down(S))     { Walk(-moveSpeed * dt); }
    if (Keyboard.Down(A))     { Strafe(-moveSpeed * dt); }
    if (Keyboard.Down(D))     { Strafe(moveSpeed * dt); }

    if (!Ready()) {
      tex1 = Texture.Pixels(brick1);
      tex2 = Texture.Pixels(brick2);
      tex3 = Texture.Pixels(brick3);
      tex4 = Texture.Pixels(brick4);
      if (!Ready()) { return; }
    }

    CastFloorAndCeiling();
    CastWalls();
    Draw.Pixels(buf, W, H, 0, 0, W, H);
  }

  bool Ready() {
    return tex1.Count >= TEX * TEX && tex2.Count >= TEX * TEX
        && tex3.Count >= TEX * TEX && tex4.Count >= TEX * TEX
        && buf.Count >= W * H && zbuf.Count >= W;
  }

  int Shade(int c, double f) {
    int r = (int)(((c >> 16) & 255) * f);
    int g = (int)(((c >> 8) & 255) * f);
    int b = (int)((c & 255) * f);
    return (r << 16) | (g << 8) | b;
  }

  void CastFloorAndCeiling() {
    for (var y = HALF + 1; y < H; y = y + 1) {
      var rayX0 = dirX - planeX;
      var rayY0 = dirY - planeY;
      var rayX1 = dirX + planeX;
      var rayY1 = dirY + planeY;
      var p = y - HALF;
      var rowDistance = HALF / (p * 1.0);
      var stepX = rowDistance * (rayX1 - rayX0) / W;
      var stepY = rowDistance * (rayY1 - rayY0) / W;
      var fx = posX + rowDistance * rayX0 + 1024.0;
      var fy = posY + rowDistance * rayY0 + 1024.0;

      var lit = 1.6 / (1.0 + rowDistance * 0.55);
      if (lit > 1.0) { lit = 1.0; }
      if (lit < 0.25) { lit = 0.25; }

      var rowFloor = y * W;
      var rowCeil = (H - y - 1) * W;
      for (var x = 0; x < W; x = x + 1) {
        var cx = (int)fx;
        var cy = (int)fy;
        var t = TEX * (int)(TEX * (fy - cy)) + (int)(TEX * (fx - cx));
        buf[rowFloor + x] = Shade(tex3[t], lit);
        buf[rowCeil + x] = Shade(tex2[t], lit * 0.7);
        fx = fx + stepX;
        fy = fy + stepY;
      }
    }
  }

  void CastWalls() {
    for (var x = 0; x < W; x = x + 1) {
      var cameraX = 2.0 * x / W - 1.0;
      var rayX = dirX + planeX * cameraX;
      var rayY = dirY + planeY * cameraX;

      var mapX = (int)Math.Floor(posX);
      var mapY = (int)Math.Floor(posY);
      var ax = rayX < 0 ? -rayX : rayX;
      var ay = rayY < 0 ? -rayY : rayY;
      var deltaX = ax < 0.000001 ? 1000000.0 : 1.0 / ax;
      var deltaY = ay < 0.000001 ? 1000000.0 : 1.0 / ay;

      var stepX = 1;
      var stepY = 1;
      var sideX = 0.0;
      var sideY = 0.0;
      if (rayX < 0) { stepX = -1; sideX = (posX - mapX) * deltaX; }
      else { sideX = (mapX + 1.0 - posX) * deltaX; }
      if (rayY < 0) { stepY = -1; sideY = (posY - mapY) * deltaY; }
      else { sideY = (mapY + 1.0 - posY) * deltaY; }

      var hit = 0;
      var side = 0;
      var guard = 0;
      while (hit == 0 && guard < 64) {
        guard = guard + 1;
        if (sideX < sideY) { sideX = sideX + deltaX; mapX = mapX + stepX; side = 0; }
        else { sideY = sideY + deltaY; mapY = mapY + stepY; side = 1; }
        hit = At(mapX, mapY);
      }

      var dist = side == 0
        ? (mapX - posX + (1 - stepX) / 2.0) / rayX
        : (mapY - posY + (1 - stepY) / 2.0) / rayY;
      if (dist < 0.05) { dist = 0.05; }
      zbuf[x] = dist;

      var lineH = H / dist;
      var start = (int)(HALF - lineH / 2.0);
      if (start < 0) { start = 0; }
      var end = (int)(HALF + lineH / 2.0);
      if (end > H) { end = H; }

      var wallX = side == 0 ? posY + dist * rayY : posX + dist * rayX;
      wallX = wallX - Math.Floor(wallX);
      var texX = (int)(wallX * TEX);
      if (side == 0 && rayX > 0) { texX = TEX - texX - 1; }
      if (side == 1 && rayY < 0) { texX = TEX - texX - 1; }
      if (texX < 0) { texX = 0; }
      if (texX > TEX - 1) { texX = TEX - 1; }

      var sideF = side == 1 ? 0.72 : 1.0;
      var lit = sideF * (2.2 / (1.0 + dist * 0.85));
      if (lit > sideF) { lit = sideF; }
      if (lit < 0.18) { lit = 0.18; }

      var step = TEX / lineH;
      var texPos = (start - HALF + lineH / 2.0) * step;

      if (hit == 2) { DrawColumn(tex2, x, start, end, texX, texPos, step, lit); }
      else if (hit == 3) { DrawColumn(tex3, x, start, end, texX, texPos, step, lit); }
      else if (hit == 4) { DrawColumn(tex4, x, start, end, texX, texPos, step, lit); }
      else { DrawColumn(tex1, x, start, end, texX, texPos, step, lit); }
    }
  }

  void DrawColumn(List<int> tex, int x, int start, int end, int texX, double texPos, double step, double lit) {
    var pos = texPos;
    for (var y = start; y < end; y = y + 1) {
      var texY = ((int)pos) & TEXMASK;
      pos = pos + step;
      buf[y * W + x] = Shade(tex[TEX * texY + texX], lit);
    }
  }

  int At(int mx, int my) {
    if (mx < 0 || my < 0 || mx > 15 || my > 15) { return 1; }
    return map[my * 16 + mx];
  }

  void Turn(double a) {
    var c = Math.Cos(a);
    var s = Math.Sin(a);
    var odx = dirX;
    dirX = dirX * c - dirY * s;
    dirY = odx * s + dirY * c;
    var opx = planeX;
    planeX = planeX * c - planeY * s;
    planeY = opx * s + planeY * c;
  }

  void Walk(double d) {
    var nx = posX + dirX * d;
    var ny = posY + dirY * d;
    var mx = dirX * d > 0 ? 0.2 : -0.2;
    var my = dirY * d > 0 ? 0.2 : -0.2;
    if (!Solid(nx + mx, posY)) { posX = nx; }
    if (!Solid(posX, ny + my)) { posY = ny; }
  }

  void Strafe(double d) {
    var nx = posX + planeX * d;
    var ny = posY + planeY * d;
    var mx = planeX * d > 0 ? 0.2 : -0.2;
    var my = planeY * d > 0 ? 0.2 : -0.2;
    if (!Solid(nx + mx, posY)) { posX = nx; }
    if (!Solid(posX, ny + my)) { posY = ny; }
  }

  bool Solid(double x, double y) { return At((int)Math.Floor(x), (int)Math.Floor(y)) > 0; }

  render {
    Box(minH: "100vh", w: "100%", bg: Colors.Bg, color: Colors.OnBg, p: 6) {
      Stack(gap: 4, align: Align.Center) {
        NeonWord("Wolf", "#FF2D95", "30px");
        Text("every pixel written by the game — floor, ceiling and walls sampled per pixel, shaded per pixel",
             fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim, letterSpacing: "0.14em", textTransform: TextTransform.Uppercase);
        Box(keys: [Left, Right, Up, Down, W, A, S, D], rounded: Radius.Cab, overflow: Overflow.Hidden, lineHeight: "0") {
          Canvas(w: 1280, h: 800, borderW: 2, border: Colors.Pink, maxW: "100%",
                 fontFamily: Font.Display, shadow: "0 0 40px rgba(255,45,149,0.25)");
        }
        Row(gap: 6, align: Align.Center) {
          Text($"{fps} fps", fontFamily: Font.Mono, fontSize: FontSize.Body, color: Colors.Pink, letterSpacing: "0.2em");
          Text("1280x800 · 1,024,000 px/frame · one Draw.Pixels", fontFamily: Font.Mono, fontSize: FontSize.Micro,
               color: Colors.Dim, letterSpacing: "0.2em", textTransform: TextTransform.Uppercase);
        }
        Link("/") { Text("← back to the lobby", fontFamily: Font.Mono, fontSize: FontSize.Tiny, color: Colors.Dim); }
      }
    }
  }
}