From ab871cc1bd55401ef326a35878d9aaf8c5b365d7 Mon Sep 17 00:00:00 2001 From: Zihang Ye Date: Wed, 8 May 2024 14:59:14 +0800 Subject: [PATCH] fix snake example (#188) * fix: modernize snake example * move: main logic * cleanup * reorganize: put into main * update: internalize color definition * simplify: make program more readable * simplify: reduce game logic * fix: ffi * update: makefile --- examples/snake/.gitignore | 2 + examples/snake/.moonbit-lsp.json | 3 + examples/snake/Makefile | 5 +- examples/snake/extern/moon.pkg.json | 5 + examples/snake/extern/string.mbt | 33 +++++ examples/snake/index.html | 136 +++---------------- examples/snake/lib/draw.mbt | 59 ++++---- examples/snake/lib/moon.pkg.json | 6 +- examples/snake/lib/snake.mbt | 203 ++++++++-------------------- examples/snake/lib/utils.mbt | 33 ++--- examples/snake/main.mjs | 90 ++++++++++++ examples/snake/main/main.mbt | 65 +++++++-- examples/snake/main/moon.pkg.json | 9 +- examples/snake/moon.mod.json | 5 +- 14 files changed, 320 insertions(+), 334 deletions(-) create mode 100644 examples/snake/.gitignore create mode 100644 examples/snake/.moonbit-lsp.json create mode 100644 examples/snake/extern/moon.pkg.json create mode 100644 examples/snake/extern/string.mbt create mode 100644 examples/snake/main.mjs diff --git a/examples/snake/.gitignore b/examples/snake/.gitignore new file mode 100644 index 00000000..53353713 --- /dev/null +++ b/examples/snake/.gitignore @@ -0,0 +1,2 @@ +target/ +.mooncakes/ \ No newline at end of file diff --git a/examples/snake/.moonbit-lsp.json b/examples/snake/.moonbit-lsp.json new file mode 100644 index 00000000..5c10e682 --- /dev/null +++ b/examples/snake/.moonbit-lsp.json @@ -0,0 +1,3 @@ +{ + "backend": "wasm-gc" +} \ No newline at end of file diff --git a/examples/snake/Makefile b/examples/snake/Makefile index e6813203..4b8a62e8 100644 --- a/examples/snake/Makefile +++ b/examples/snake/Makefile @@ -1,3 +1,4 @@ run: - moon build - sudo python3 -m http.server 8080 \ No newline at end of file + moon install + moon build --target wasm-gc + python3 -m http.server 8080 \ No newline at end of file diff --git a/examples/snake/extern/moon.pkg.json b/examples/snake/extern/moon.pkg.json new file mode 100644 index 00000000..3cc9b68d --- /dev/null +++ b/examples/snake/extern/moon.pkg.json @@ -0,0 +1,5 @@ +{ + "import": [ + "peter-jerry-ye/memory" + ] +} \ No newline at end of file diff --git a/examples/snake/extern/string.mbt b/examples/snake/extern/string.mbt new file mode 100644 index 00000000..dfcb0790 --- /dev/null +++ b/examples/snake/extern/string.mbt @@ -0,0 +1,33 @@ +type JS_String + +fn string_load_ffi(offset : Int, length : Int) -> JS_String = "string" "load" + +fn string_store_ffi(string : JS_String, offset : Int) = "string" "store" + +pub fn length(self : JS_String) -> Int = "string" "length" + +pub fn log(self : JS_String) = "string" "log" + +pub fn JS_String::empty() -> JS_String = "string" "empty" + +pub fn JS_String::from_string(str : String) -> Option[JS_String] { + if str.length() == 0 { + return Some(JS_String::empty()) + } + let memory = @memory.allocate(str.length() * 2)? + memory.store_bytes(str.to_bytes()) + let str = string_load_ffi(memory.offset, str.length()) + memory.free()? + Some(str) +} + +pub fn JS_String::to_string(str : JS_String) -> Option[String] { + if str.length() == 0 { + return Some("") + } + let memory = @memory.allocate(str.length() * 2)? + string_store_ffi(str, memory.offset) + let str = memory.load_bytes().to_string() + memory.free()? + Some(str) +} diff --git a/examples/snake/index.html b/examples/snake/index.html index 587bda68..d224626b 100644 --- a/examples/snake/index.html +++ b/examples/snake/index.html @@ -1,125 +1,21 @@ - - - - - -
-
-

up: go up

-

left: go left

-

right: go right

-

down: go down

-
-
- - -WebAssembly.instantiateStreaming(fetch("target/wasm/release/build/main/main.wasm"), importObject).then( - (obj) => { - obj.instance.exports._start(); - snake_draw = obj.instance.exports["snake/main::draw"]; - snake_new = obj.instance.exports["snake/main::new"]; - snake_step = obj.instance.exports["snake/main::step"]; - snake = snake_new(); - requestAnimationFrameId = requestAnimationFrame(update); - } - ) - \ No newline at end of file diff --git a/examples/snake/lib/draw.mbt b/examples/snake/lib/draw.mbt index abcfbb7c..50183252 100644 --- a/examples/snake/lib/draw.mbt +++ b/examples/snake/lib/draw.mbt @@ -1,10 +1,10 @@ -// fn newCanvas(w: Int, h: Int) -> Int= "canvas""newCanvas" +pub type Canvas_ctx -// fn updateCanvas(id: Int, ) +fn set_stroke_color_ffi(self : Canvas_ctx, color : @extern.JS_String) = "canvas" "set_stroke_color" -type Canvas_ctx - -fn set_stroke_color(self : Canvas_ctx, color : Int) = "canvas" "set_stroke_color" +fn set_stroke_color(self : Canvas_ctx, color : String) -> Unit { + self.set_stroke_color_ffi(@extern.JS_String::from_string(color).unwrap()) +} fn set_line_width(self : Canvas_ctx, width : Double) = "canvas" "set_line_width" @@ -12,41 +12,48 @@ fn stroke_rect(self : Canvas_ctx, x : Int, y : Int, width : Int, height : Int) = fn fill_rect(self : Canvas_ctx, x : Int, y : Int, width : Int, height : Int) = "canvas" "fill_rect" -fn set_fill_style(self : Canvas_ctx, color : Int) = "canvas" "set_fill_style" +fn set_fill_style_ffi(self : Canvas_ctx, color : @extern.JS_String) = "canvas" "set_fill_style" + +fn set_fill_style(self : Canvas_ctx, color : String) -> Unit { + self.set_fill_style_ffi(@extern.JS_String::from_string(color).unwrap()) +} +fn body_color(grid : GridType) -> String { + match grid { + Body => "#ffb900" + Food => "#2753f1" + Default => "#dcdcdc" + } +} -pub fn draw(canvas : Canvas_ctx, snake : GameState) { - let mut c = 0 +let border_color = "#00263f" +pub fn draw(canvas : Canvas_ctx, snake : GameState) -> Unit { // draw backgroud - while c < grid_col_count { - canvas.set_fill_style(0) + for c = 0; c < grid_col_count; c = c + 1 { + canvas.set_fill_style(body_color(Default)) canvas.fill_rect(c, 0, 1, grid_row_count) - c = c + 1 } - draw_piece(canvas, snake.grid, (0, 0)) } -pub fn draw_piece(canvas : Canvas_ctx, matrix : Array[Int], - offset : (Int, Int)) { - +fn draw_piece( + canvas : Canvas_ctx, + matrix : Array[GridType], + offset : (Int, Int) +) -> Unit { let mut r = 0 - let mut c = 0 let mut c0 = 0 - while c < matrix.length() { - if matrix[c] == 0 { - c = c + 1 - continue + for c = 0; c < matrix.length(); c = c + 1 { + if matrix[c] == Default { + continue c + 1 } c0 = c % grid_col_count r = c / grid_col_count - canvas.set_fill_style(matrix[c] + 1) - canvas.fill_rect( offset.0 + c0, r, 1, 1) - canvas.set_stroke_color(1) + canvas.set_fill_style(body_color(matrix[c])) + canvas.fill_rect(offset.0 + c0, r, 1, 1) + canvas.set_stroke_color(border_color) canvas.set_line_width(0.1) - canvas.stroke_rect( c0, r, 1, 1) - c = c + 1 + canvas.stroke_rect(c0, r, 1, 1) } } - diff --git a/examples/snake/lib/moon.pkg.json b/examples/snake/lib/moon.pkg.json index 6f31cf5a..80927d66 100644 --- a/examples/snake/lib/moon.pkg.json +++ b/examples/snake/lib/moon.pkg.json @@ -1 +1,5 @@ -{ } \ No newline at end of file +{ + "import": [ + "snake/extern" + ] +} \ No newline at end of file diff --git a/examples/snake/lib/snake.mbt b/examples/snake/lib/snake.mbt index bf3b02d1..e415a28a 100644 --- a/examples/snake/lib/snake.mbt +++ b/examples/snake/lib/snake.mbt @@ -2,7 +2,7 @@ let grid_row_count = 20 let grid_col_count = 20 -enum Direction{ +pub enum Direction { Up Down Left @@ -10,208 +10,113 @@ enum Direction{ Default } -enum GridType{ +enum GridType { Body Food Default -} - -// enum Action{ -// 1 -// 2 -// 3 -// 4 -// 5 -// } - -pub fn grid_num(self: GridType) -> Int{ - match self{ - Body => 1 - Food => 2 - Default => 0 +} derive(Eq) + +fn dir_posi(self : Direction) -> Position { + match self { + Up => { x: 0, y: -1 } + Down => { x: 0, y: 1 } + Left => { x: -1, y: 0 } + Right => { x: 1, y: 0 } + Default => { x: 0, y: 0 } } } -pub fn dir_posi(self: Direction) -> Position{ - match self{ - Up => {x: 0, y: -1} - Down => {x: 0, y: 1} - Left => {x: -1, y: 0} - Right => {x: 1, y: 0} - Default => {x: 0, y: 0} - } -} - -fn Direction::op_equal(x: Direction, y: Direction) -> Bool{ - let x0: Position = dir_posi(x) - let y0: Position = dir_posi(y) - if (x0.x == y0.x && x0.y == y0.y){ +fn Direction::op_equal(x : Direction, y : Direction) -> Bool { + let x0 : Position = dir_posi(x) + let y0 : Position = dir_posi(y) + if x0.x == y0.x && x0.y == y0.y { true - }else{ + } else { false } } -struct Position{ - mut x: Int - mut y: Int +struct Position { + mut x : Int + mut y : Int } -struct GameState{ - mut grid: Array[Int] - mut body: List[Position] - mut dir: Direction +struct GameState { + mut grid : Array[GridType] + mut body : List[Position] + mut dir : Direction } -pub fn initialize(self: GameState){ - self.grid = Array::make(grid_row_count*grid_col_count, 0) +fn initialize(self : GameState) -> Unit { + self.grid = Array::make(grid_row_count * grid_col_count, Default) self.dir = Up - - self.body = Cons({x: grid_col_count/2, y: grid_col_count/2}, Nil) - self.grid[grid_col_count/2 * grid_col_count + grid_col_count/2] = 1 - + self.body = Cons({ x: grid_col_count / 2, y: grid_col_count / 2 }, Nil) + self.grid[grid_col_count / 2 * grid_col_count + grid_col_count / 2] = Body self.generate_Food() - } -fn setGridType(self: GameState, p: Position, t: GridType){ - self.grid[p.y * grid_col_count + p.x] = grid_num(t) - +fn setGridType(self : GameState, p : Position, t : GridType) -> Unit { + self.grid[p.y * grid_col_count + p.x] = t } fn random() -> Double = "Math" "random" -fn floor(i: Double) -> Int = "Math" "floor" -fn generate_Food(self: GameState){ +fn floor(i : Double) -> Int = "Math" "floor" + +fn generate_Food(self : GameState) -> Unit { while true { let i : Int = floor(random() * 20.0) let j : Int = floor(random() * 20.0) - - if(self.grid[j * grid_col_count + i] == grid_num(Default)){ - self.setGridType({x: i, y: j}, Food) + if self.grid[j * grid_col_count + i] == Default { + self.setGridType({ x: i, y: j }, Food) return } } } -fn go_step(self: GameState){ +fn go_step(self : GameState) -> Unit { // if (prev == Up && self.dir == Down) || (prev == Down && self.dir == Up) || (prev == Left && self.dir == Right) || (prev == Right && self.dir == Left){ // self.dir = prev // } let head : Position = get_head(self.body) - let newHead : Position = {x: head.x , y: head.y } - - newHead.x = dir_posi(self.dir).x + newHead.x + let newHead : Position = { x: head.x, y: head.y } + newHead.x = dir_posi(self.dir).x + newHead.x newHead.y = dir_posi(self.dir).y + newHead.y - newHead.x = (newHead.x + grid_col_count) % grid_col_count newHead.y = (newHead.y + grid_col_count) % grid_col_count - - if self.grid[newHead.y * grid_col_count + newHead.x] == 1{ - + if self.grid[newHead.y * grid_col_count + newHead.x] == Body { initialize(self) return - }else if self.grid[newHead.y * grid_col_count + newHead.x] == 2{ - + } else if self.grid[newHead.y * grid_col_count + newHead.x] == Food { self.setGridType(newHead, Body) self.body = Cons(newHead, self.body) generate_Food(self) - }else { - + } else { self.setGridType(newHead, Body) self.body = Cons(newHead, self.body) self.setGridType(get_tail(self.body), Default) self.body = delete_tail(self.body) } - } -pub fn step(self : GameState, action : Direction) { - - match action { - // move up - Up =>{ - if length(self.body) == 1{ - self.dir = Up - }else{ - if self.dir == Left || self.dir == Right || self.dir == Up{ - self.dir = Up - }else{ - self.dir = self.dir - } - } - - } - - // move down - Down =>{ - if length(self.body) == 1{ - self.dir = Down - }else{ - if self.dir == Left || self.dir == Right || self.dir == Down{ - self.dir = Down - }else{ - self.dir = self.dir - } - } - - } - - // move left - Left =>{ - if length(self.body) == 1{ - self.dir = Left - }else{ - if self.dir == Up || self.dir == Left || self.dir == Down{ - self.dir = Left - }else{ - self.dir = self.dir - } - } - - } - - // move right - Right =>{ - if length(self.body) == 1{ - self.dir = Right - }else{ - if self.dir == Up || self.dir == Right || self.dir == Down{ - self.dir = Right - }else{ - self.dir = self.dir - } - } +pub fn step(self : GameState, action : Direction) -> Unit { + match (action, self.dir) { + (Up, Down) | (Left, Right) | (Right, Left) | (Down, Up) => + if self.body.length() == 1 { + self.dir = action + self.go_step() } - - _ =>{ - self.dir = self.dir + _ => { + if action != Default { + self.dir = action } - - } - - self.go_step() -} - -pub fn tran_step(self : GameState, a : Int){ - let mut action : Direction = Default - match a { - 1 => action = Up - 2 => action = Down - 3 => action = Left - 4 => action = Right - _ => action = Default + self.go_step() + } } - - self.step(action) } -pub fn new() -> GameState{ - let game: GameState = { - grid: [], - body: Nil, - dir: Up - } +pub fn new() -> GameState { + let game : GameState = { grid: [], body: Nil, dir: Up } game.initialize() game -} \ No newline at end of file +} diff --git a/examples/snake/lib/utils.mbt b/examples/snake/lib/utils.mbt index 69da045e..cfe60b10 100644 --- a/examples/snake/lib/utils.mbt +++ b/examples/snake/lib/utils.mbt @@ -1,25 +1,21 @@ -pub fn get_head(cur: List[Position]) -> Position{ - match cur{ - Nil => {x: 0, y: 0} +fn get_head(cur : List[Position]) -> Position { + match cur { + Nil => { x: 0, y: 0 } Cons(x, _) => x } } -pub fn get_tail(cur: List[Position]) -> Position{ - fn go(list: List[Position]) -> Position{ - match list{ - Nil => {x: 0, y:0} - Cons(x, Nil) => x - Cons(_, xs) => go(xs) - } +fn get_tail(cur : List[Position]) -> Position { + loop cur { + Nil => { x: 0, y: 0 } + Cons(x, Nil) => x + Cons(_, xs) => continue xs } - - go(cur) } -pub fn delete_tail(cur: List[Position]) -> List[Position]{ - fn go(list: List[Position]) -> List[Position]{ - match list{ +fn delete_tail(cur : List[Position]) -> List[Position] { + fn go(list : List[Position]) -> List[Position] { + match list { Nil => Nil Cons(_, Nil) => Nil Cons(x, xs) => Cons(x, go(xs)) @@ -27,11 +23,4 @@ pub fn delete_tail(cur: List[Position]) -> List[Position]{ } go(cur) -} - -pub fn length[T](now : List[T]) -> Int { - match now { - Nil => 0 - Cons(_, n) => 1 + length(n) - } } \ No newline at end of file diff --git a/examples/snake/main.mjs b/examples/snake/main.mjs new file mode 100644 index 00000000..6bc072e5 --- /dev/null +++ b/examples/snake/main.mjs @@ -0,0 +1,90 @@ + +// @ts-check + +const canvas = /** @type {HTMLCanvasElement | null} */ (document.getElementById("canvas")); +const context = canvas?.getContext("2d") +if (!canvas || !context) { + throw Error("Canvas not found"); +} +const WIDTH = 480 +const HEIGHT = WIDTH + +canvas.width = WIDTH +canvas.height = HEIGHT + +context.scale(24, 24) + +const [log, flush] = (() => { + var buffer = []; + function flush() { + if (buffer.length > 0) { + console.log(new TextDecoder("utf-16").decode(new Uint16Array(buffer).valueOf())); + buffer = []; + } + } + function log(ch) { + if (ch == '\n'.charCodeAt(0)) { flush(); } + else if (ch == '\r'.charCodeAt(0)) { /* noop */ } + else { buffer.push(ch); } + } + return [log, flush] +})(); + +/** @type {WebAssembly.Memory} */ +let memory + +const importObject = { + canvas: { + get_context: () => context, + stroke_rect: (ctx, x, y, width, height) => ctx.strokeRect(x, y, width, height), + set_line_width: (ctx, width) => ctx.lineWidth = width, + fill_rect: (ctx, x, y, width, height) => ctx.fillRect(x, y, width, height), + set_stroke_color: (ctx, color) => ctx.strokeStyle = color, + set_fill_style: (ctx, color) => ctx.fillStyle = color + }, + spectest: { + print_char: log, + }, + string: { + empty: () => "", + length: str => str.length, + load: (offset, length) => { + const bytes = new Uint16Array(memory.buffer, offset, length); + const string = new TextDecoder("utf-16").decode(bytes); + return string + }, + store: (string, offset) => { + const view = new DataView(memory.buffer); + for (let i = 0; i < string.length; i++) { + view.setUint16(offset + i * 2, string.charCodeAt(i), true); + } + } + }, + Math: { + random: Math.random, + floor: Math.floor, + }, + keyboard_event: { + /** @type {(event: KeyboardEvent) => String} */ + key: (event) => event.key, + }, + document: { + /** @type {(callback: (ev: KeyboardEvent) => void) => void} */ + set_onkeydown: (callback) => { document.onkeydown = callback }, + }, + window: { + requestAnimationFrame: window.requestAnimationFrame + }, + "moonbit:ffi": { + make_closure: (funcref, closure) => funcref.bind(null, closure) + }, +}; + +WebAssembly.instantiateStreaming(fetch("target/wasm-gc/release/build/main/main.wasm"), importObject).then( + (obj) => { + memory = /** @type {WebAssembly.Memory} */ (obj.instance.exports["moonbit.memory"]); + // @ts-ignore + obj.instance.exports._start(); + flush(); + } +) \ No newline at end of file diff --git a/examples/snake/main/main.mbt b/examples/snake/main/main.mbt index e06caf5a..71e1ddc5 100644 --- a/examples/snake/main/main.mbt +++ b/examples/snake/main/main.mbt @@ -1,15 +1,62 @@ -pub fn new() -> @lib.GameState{ - return @lib.new() -} +fn request_animation_frame(callback : (Double) -> Unit) -> Int = "window" "requestAnimationFrame" + +fn set_on_keydown(callback : (KeyboardEvent) -> Unit) = "document" "set_onkeydown" + +fn get_context() -> @lib.Canvas_ctx = "canvas" "get_context" + +type KeyboardEvent + +fn key_ffi(self : KeyboardEvent) -> @extern.JS_String = "keyboard_event" "key" -pub fn step(game: @lib.GameState, a: Int){ - @lib.tran_step(game, a) +fn key(self : KeyboardEvent) -> String { + self.key_ffi().to_string().unwrap() } -pub fn draw(canvas : @lib.Canvas_ctx, game : @lib.GameState){ - @lib.draw(canvas, game) +let state : @lib.GameState = @lib.new() + +let context : @lib.Canvas_ctx = get_context() + +let last_frame : Ref[Double] = { val: 0.0 } + +let game_interval = 500.0 + +fn update(time : Double) -> Unit { + if time - last_frame.val > game_interval { + @lib.step(state, @lib.Direction::Default) + @lib.draw(context, state) + last_frame.val = time + } else { + @lib.draw(context, state) + } + request_animation_frame(update) |> ignore } -fn init { - () +fn main { + set_on_keydown( + fn(event) { + if last_frame.val < 0.0 { // not started yet + return + } + match event.key() { + "ArrowLeft" => { + @lib.step(state, @lib.Direction::Left) + @lib.draw(context, state) + } + "ArrowRight" => { + @lib.step(state, @lib.Direction::Right) + @lib.draw(context, state) + } + "ArrowDown" => { + @lib.step(state, @lib.Direction::Down) + @lib.draw(context, state) + } + "ArrowUp" => { + @lib.step(state, @lib.Direction::Up) + @lib.draw(context, state) + } + _ => () + } + }, + ) + request_animation_frame(update) |> ignore } diff --git a/examples/snake/main/moon.pkg.json b/examples/snake/main/moon.pkg.json index ac0d86b2..50e93633 100644 --- a/examples/snake/main/moon.pkg.json +++ b/examples/snake/main/moon.pkg.json @@ -1,6 +1,7 @@ { "is_main": true, - "import": { - "snake/lib": "" - } - } \ No newline at end of file + "import": [ + "snake/extern", + "snake/lib" + ] +} \ No newline at end of file diff --git a/examples/snake/moon.mod.json b/examples/snake/moon.mod.json index fee85a52..e3f38920 100644 --- a/examples/snake/moon.mod.json +++ b/examples/snake/moon.mod.json @@ -1,3 +1,6 @@ { - "name": "snake" + "name": "snake", + "deps": { + "peter-jerry-ye/memory": "0.6.1" + } } \ No newline at end of file