arcade
A collection of small arcade games — the demo that pushes the UI engine hardest.
21 source files1 test file

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); } } } } }