From 91f1990857d58d669e20042d31a5a2482ced68b6 Mon Sep 17 00:00:00 2001 From: Kalle Fagerberg Date: Fri, 20 Feb 2026 20:38:40 +0100 Subject: [PATCH] Add scores, badges, and boards --- example/scores/assets/eg_6x10.fff | Bin 0 -> 727 bytes example/scores/assets/eg_6x10.fff.license | 5 ++ example/scores/firefly.toml | 14 ++++ example/scores/main.mbt | 77 +++++++++++++++++++++ example/scores/moon.mod.json | 9 +++ example/scores/moon.pkg | 14 ++++ src/graphics_font.mbt | 23 +++++++ src/internal/ffi/stats.mbt | 7 ++ src/stats.mbt | 78 ++++++++++++++++++++++ 9 files changed, 227 insertions(+) create mode 100644 example/scores/assets/eg_6x10.fff create mode 100644 example/scores/assets/eg_6x10.fff.license create mode 100644 example/scores/firefly.toml create mode 100644 example/scores/main.mbt create mode 100644 example/scores/moon.mod.json create mode 100644 example/scores/moon.pkg create mode 100644 src/internal/ffi/stats.mbt create mode 100644 src/stats.mbt diff --git a/example/scores/assets/eg_6x10.fff b/example/scores/assets/eg_6x10.fff new file mode 100644 index 0000000000000000000000000000000000000000..9f84c60066629220e91563e0cb62386dfdce0362 GIT binary patch literal 727 zcmZ{iK~EDw7>2)2ComcgTkXNagiNQcE|sLn!INp$MvPewQhq^WV$_5K?Jt;J7MNWQ zka#0|s1pvQdTG3PG7S=K@W5Y?$brNI5iW$2-&U+>!h4w~@4WeD=FMba^r8mo*^yy`#%FLtFC*ZJvj9DXvs9fyyM%cS-SzHK%OEf=5e z*-Hzib4SeXi!*P^@0j<8=-)=)pD=!5kPnl5v&>JAc=%bK+wql$UE27$&(CcWm1A!Q zBHKu0v7u&=TgbJCmh(iNl({UHgl`E4{1QQ)3oHwHTgu{ntt~9V>&Lkrsn_FcJ?wgT zKxIe>e{BGZ0ZW5YtvYTXQ>8#)AoZ2M+8ytDCO^wz(rlJxUv0^ECUw+WJCs^|kZnWI zr{fy^wn_cKJPCJA^Wwb{sDcY|NdLi0tCR3ZE=c+rFcYrnzU3!L4}l2j-a@NtoB7-= uQD>3OtHqLhMulQ~FNyoSRNG?ML%03lp2_O+N=`8)a1!8OL;dDdFX3OfC!&)8 literal 0 HcmV?d00001 diff --git a/example/scores/assets/eg_6x10.fff.license b/example/scores/assets/eg_6x10.fff.license new file mode 100644 index 0000000..6b2e063 --- /dev/null +++ b/example/scores/assets/eg_6x10.fff.license @@ -0,0 +1,5 @@ +SPDX-FileCopyrightText: 2020 James Waples + +SPDX-License-Identifier: MIT + +Obtained from: https://fonts.fireflyzero.com/ascii diff --git a/example/scores/firefly.toml b/example/scores/firefly.toml new file mode 100644 index 0000000..106a973 --- /dev/null +++ b/example/scores/firefly.toml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 Kalle Fagerberg +# +# SPDX-License-Identifier: CC0-1.0 + +author_id = "demo" +app_id = "moonbit-scores" +author_name = "Demo" +app_name = "Scores demo (MoonBit)" + +[files] +font = { path = "assets/eg_6x10.fff" } + +[boards] +1 = { name = "default" } diff --git a/example/scores/main.mbt b/example/scores/main.mbt new file mode 100644 index 0000000..663340c --- /dev/null +++ b/example/scores/main.mbt @@ -0,0 +1,77 @@ +///| +using @firefly {type Point, type Peer, type Board} + +///| +fn main { + +} + +///| +/// ID of a board set in `firefly.toml` +let board : Board = 1 + +///| +let font : Ref[@firefly.Font] = Ref::new(@firefly.Font::default()) + +///| +let players : Array[Player] = Array::new(capacity=4) + +///| +struct Player { + peer : Peer + name : Bytes + mut curr : Int16 + mut best : Int16 +} + +///| +/// boot is only called once, after all the memory is initialized. +pub fn boot() -> Unit { + font.val = @firefly.load_file("font").unwrap().as_font() + let peers = @firefly.get_peers().to_fixed_array() + for peer in peers { + players.push(Player::{ + peer, + name: @firefly.get_name(peer), + curr: 0, + best: @firefly.get_score(peer, board), + }) + } +} + +///| +/// update is called ~60 times per second. +pub fn update() -> Unit { + for p in players { + let btns = @firefly.read_buttons(peer=p.peer) + if btns.south { + p.curr += 1 + } + if btns.east { + p.best = @firefly.add_score(p.peer, board, p.curr) + p.curr = 0 + } + } +} + +///| +/// render is called before updating the image on the screen. +/// +/// It might be called less often than `update` if the device sees that the game +/// is slow and needs more resources. +/// This is the best place to call all drawing functions. +pub fn render() -> Unit { + @firefly.clear_screen(White) + let c = @firefly.Color::DarkBlue + for i, p in players { + let y = 10 + 10 * i + @firefly.draw_text(p.name, font.val, Point::new(10, y), c) + @firefly.draw_text(format(p.curr), font.val, Point::new(120, y), c) + @firefly.draw_text(format(p.best), font.val, Point::new(150, y), c) + } +} + +///| +fn[T : Show] format(v : T) -> Bytes { + @utf8.encode(v.to_string()) +} diff --git a/example/scores/moon.mod.json b/example/scores/moon.mod.json new file mode 100644 index 0000000..0f871f3 --- /dev/null +++ b/example/scores/moon.mod.json @@ -0,0 +1,9 @@ +{ + "name": "example-scores", + "deps": { + "firefly/firefly": { + "path": "../.." + } + }, + "preferred-target": "wasm" +} diff --git a/example/scores/moon.pkg b/example/scores/moon.pkg new file mode 100644 index 0000000..2e8dce8 --- /dev/null +++ b/example/scores/moon.pkg @@ -0,0 +1,14 @@ +import { + "moonbitlang/core/encoding/utf8", + "firefly/firefly", +} + +options( + "is-main": true, + link: { + "wasm": { + "export-memory-name": "memory", + "exports": [ "boot", "update", "render" ], + }, + }, +) diff --git a/src/graphics_font.mbt b/src/graphics_font.mbt index 086dede..201c3ca 100644 --- a/src/graphics_font.mbt +++ b/src/graphics_font.mbt @@ -27,3 +27,26 @@ pub fn Font::char_width(self : Font) -> Int { pub fn Font::char_height(self : Font) -> Int { self.0[3].to_int() } + +///| +/// Render text using the this font. +/// +/// Unlike in the other drawing functions, here `point` does not represent +/// the top-left corner, but instead the text baseline position +/// (such as the bottom left pixel of an underscore "`_`"). +/// +/// The text must be valid UTF-8. To convert String into text, +/// use [@encoding/utf8]: +/// +/// ```mbt nocheck +/// let utf8 = @encoding/utf8.encode("привет, мир!") +/// ``` +#inline +pub fn Font::draw( + self : Font, + text : Bytes, + point : Point, + color : Color, +) -> Unit { + draw_text(text, self, point, color) +} diff --git a/src/internal/ffi/stats.mbt b/src/internal/ffi/stats.mbt new file mode 100644 index 0000000..f9044b2 --- /dev/null +++ b/src/internal/ffi/stats.mbt @@ -0,0 +1,7 @@ +///| +/// Add the given value to the progress for the badge. +pub fn add_progress(peerID : UInt, badgeID : UInt, val : Int) -> UInt = "stats" "add_progress" + +///| +/// Add the given score to the board. +pub fn add_score(peerID : UInt, boardID : UInt, val : Int) -> Int = "stats" "add_score" diff --git a/src/stats.mbt b/src/stats.mbt new file mode 100644 index 0000000..29dc4fa --- /dev/null +++ b/src/stats.mbt @@ -0,0 +1,78 @@ +///| +/// A badge (aka achievement) ID. +pub(all) struct Badge(Byte) derive(Eq, Compare, Hash, Default, Show) + +///| +/// A board (aka score board / leader board) ID. +pub(all) struct Board(Byte) derive(Eq, Compare, Hash, Default, Show) + +///| +#valtype +struct Progress { + /// How many points the player has. + done : UInt16 + /// How many points the player needs to earn the badge. + goal : UInt16 +} derive(Show, Eq, Default) + +///| +/// True if the player got enough points to unlock the badge. +pub fn Progress::is_earned(self : Progress) -> Bool { + self.done >= self.goal +} + +///| +/// Get the progress of earning the badge. +pub fn Progress::get_progress(peer : Peer, badge : Badge) -> Progress { + add_progress(peer, badge, 0) +} + +///| +/// Add the given value to the progress for the badge. +/// +/// May be negative if you want to decrease the progress. +/// If zero, does not change the progress. +/// +/// If using `Peer::combined()`, the progress is added to every peer +/// and the returned value is the lowest progress. +pub fn add_progress(peer : Peer, badge : Badge, value : Int16) -> Progress { + @ffi.add_progress( + peer.raw.reinterpret_as_uint(), + badge.0.to_uint(), + value.to_int(), + ) + |> Progress::parse +} + +///| +/// Get the personal best of the player. +pub fn get_score(peer : Peer, board : Board) -> Int16 { + add_score(peer, board, 0) +} + +///| +fn Progress::parse(result : UInt) -> Progress { + Progress::{ done: (result >> 16).to_uint16(), goal: result.to_uint16() } +} + +///| +test "parse" { + inspect(Progress::parse((87 << 16) + 391), content="{done: 87, goal: 391}") +} + +///| +/// Add the given score to the board. +/// +/// May be negative if you want to decrease the progress. +/// If zero, does not change the progress. +/// +/// If using `Peer::combined()`, the progress is added to every peer +/// and the returned value is the lowest progress. +pub fn add_score(peer : Peer, board : Board, value : Int16) -> Int16 { + @ffi.add_score( + peer.raw.reinterpret_as_uint(), + board.0.to_uint(), + value.to_int(), + ) + |> Int16::from_int +}