diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..14f252e --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +EXE = opacman.exe + +all: $(EXE) + +$(EXE): src/*.opa #resources/* + opa src/*.opa -o $(EXE) + +clean: + rm -Rf *.exe _build _tracks *.log **/#*# diff --git a/src/base.opa b/src/base.opa new file mode 100644 index 0000000..987fd78 --- /dev/null +++ b/src/base.opa @@ -0,0 +1,44 @@ +@client Base = {{ + + Dir = {{ + + facing_angle(dir:Base.direction) = + match dir with + | {up} -> -Math.PI/2. + | {down} -> Math.PI/2. + | {left} -> Math.PI + | {right} -> 0. + | {still} -> 0. + + deltas(dir:Base.direction) = + match dir with + | {up} -> (0, -1) + | {down} -> (0, 1) + | {left} -> (-1, 0) + | {right} -> (1, 0) + | {still} -> (0, 0) + + back(dir:Base.direction):Base.direction = + match dir with + | {up} -> {down} + | {down} -> {up} + | {left} -> {right} + | {right} -> {left} + | x -> x + + }} + + @both make(x, y, dir, max_steps) = { + pos = ~{x y} + cur_step = 0 + ~dir ~max_steps + } + + center(b:Base.t) = + w = base_size + d = (w*b.cur_step) / b.max_steps + (dx, dy) = Dir.deltas(b.dir) + (1+w/2+w*b.pos.x+d*dx, + 1+w/2+w*b.pos.y+d*dy) + +}} diff --git a/src/food.opa b/src/food.opa new file mode 100644 index 0000000..7dd1945 --- /dev/null +++ b/src/food.opa @@ -0,0 +1,54 @@ +(initial_food, walls) = [ + [1,1,1,1,1,1,1,1,1,1,1,1,8,8,1,1,1,1,1,1,1,1,1,1,1,1], + [2,8,8,8,8,1,8,8,8,8,8,1,8,8,1,8,8,8,8,8,1,8,8,8,8,2], + [1,8,8,8,8,1,8,8,8,8,8,1,8,8,1,8,8,8,8,8,1,8,8,8,8,1], + [1,8,8,8,8,1,8,8,8,8,8,1,8,8,1,8,8,8,8,8,1,8,8,8,8,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,8,8,8,8,1,8,8,1,8,8,8,8,8,8,8,8,1,8,8,1,8,8,8,8,1], + [1,8,8,8,8,1,8,8,1,8,8,8,8,8,8,8,8,1,8,8,1,8,8,8,8,1], + [1,1,1,1,1,1,8,8,1,1,1,1,8,8,1,1,1,1,8,8,1,1,1,1,1,1], + [8,8,8,8,8,1,8,8,8,8,8,0,8,8,0,8,8,8,8,8,1,8,8,8,8,8], + [8,8,8,8,8,1,8,8,8,8,8,0,8,8,0,8,8,8,8,8,1,8,8,8,8,8], + [8,8,8,8,8,1,8,8,0,0,0,0,0,0,0,0,0,0,8,8,1,8,8,8,8,8], + [8,8,8,8,8,1,8,8,0,8,8,8,0,0,8,8,8,0,8,8,1,8,8,8,8,8], + [8,8,8,8,8,1,8,8,0,8,0,0,8,8,0,0,8,0,8,8,1,8,8,8,8,8], + [0,0,0,0,8,1,0,0,0,8,0,0,0,0,0,0,8,0,0,0,1,8,0,0,0,0], + [8,8,8,8,8,1,8,8,0,8,0,0,0,0,0,0,8,0,8,8,1,8,8,8,8,8], + [8,8,8,8,8,1,8,8,0,8,8,8,8,8,8,8,8,0,8,8,1,8,8,8,8,8], + [8,8,8,8,8,1,8,8,0,0,0,0,0,0,0,0,0,0,8,8,1,8,8,8,8,8], + [8,8,8,8,8,1,8,8,0,8,8,8,8,8,8,8,8,0,8,8,1,8,8,8,8,8], + [8,8,8,8,8,1,8,8,0,8,8,8,8,8,8,8,8,0,8,8,1,8,8,8,8,8], + [1,1,1,1,1,1,1,1,1,1,1,1,8,8,1,1,1,1,1,1,1,1,1,1,1,1], + [1,8,8,8,8,1,8,8,8,8,8,1,8,8,1,8,8,8,8,8,1,8,8,8,8,1], + [2,8,8,8,8,1,8,8,8,8,8,1,8,8,1,8,8,8,8,8,1,8,8,8,8,2], + [1,1,1,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,1,1,1], + [8,8,1,8,8,1,8,8,1,8,8,8,8,8,8,8,8,1,8,8,1,8,8,1,8,8], + [8,8,1,8,8,1,8,8,1,8,8,8,8,8,8,8,8,1,8,8,1,8,8,1,8,8], + [1,1,1,1,1,1,8,8,1,1,1,1,8,8,1,1,1,1,8,8,1,1,1,1,1,1], + [1,8,8,8,8,8,8,8,8,8,8,1,8,8,1,8,8,8,8,8,8,8,8,8,8,1], + [1,8,8,8,8,8,8,8,8,8,8,1,8,8,1,8,8,8,8,8,8,8,8,8,8,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], +] |> List.foldi( + y, l, (food, walls) -> + List.foldi( + x, v, (food, walls) -> + if v == 1 then (Set.add(~{x y}, food), walls) + else if v == 8 then (food, Set.add(~{x y}, walls)) + else (food, walls), + l, (food, walls)), + _, (Set.empty:set(Base.pos), Set.empty:set(Base.pos))) + +@client Food = {{ + + draw(ctx:Canvas.context) = + food = game.get().food + w = base_size + do Canvas.save(ctx) + do Canvas.set_fill_style(ctx, {color=Color.red}) + do Set.iter( + ~{x y} -> Canvas.fill_rect(ctx, w/2+x*w-2, w/2+y*w-2, 6, 6), + food) + do Canvas.restore(ctx) + void + +}} diff --git a/src/ghost.opa b/src/ghost.opa new file mode 100644 index 0000000..c737db3 --- /dev/null +++ b/src/ghost.opa @@ -0,0 +1,125 @@ +@client Ghost = {{ + + @server default = [ + { ai = {dumb} + base = Base.make(5, 4, {right}, 10) + color = Color.orange }, + { ai = {guard} + base = Base.make(20, 4, {down}, 10) + color = Color.darkred }, + { ai = {dumb} + base = Base.make(20, 22, {left}, 10) + color = Color.gold }, + { ai = {guard} + base = Base.make(5, 22, {up}, 10) + color = Color.green } + ] : list(Ghost.t) + + invert_color(c:color) = + Color.set_r(c, 255-Color.r(c)) + |> Color.set_g(_, 255-Color.g(c)) + |> Color.set_b(_, 255-Color.b(c)) + + draw_one(ctx:Canvas.context, g:Ghost.t) = + w = base_size + + do Canvas.save(ctx) + do Canvas.set_fill_style(ctx, {color=g.color}) + (center_x, center_y) = Base.center(g.base) + do Canvas.translate(ctx, center_x, center_y) + + do Canvas.begin_path(ctx) + do Canvas.move_to(ctx, w/2, 0) + do Canvas.quadratic_curve_to(ctx, w/2, -w/2, 0, -w/2) + do Canvas.quadratic_curve_to(ctx, -w/2, -w/2, -w/2, 0) + do Canvas.line_to(ctx, -w/2, w/2) + do Canvas.line_to(ctx, -w/6, w/3) + do Canvas.line_to(ctx, 0, w/2) + do Canvas.line_to(ctx, w/6, w/3) + do Canvas.line_to(ctx, w/2, w/2) + do Canvas.fill(ctx) + + do Canvas.clear_rect(ctx, -w/4, -w/4, w/2, w/4) + do Canvas.set_fill_style(ctx, {color=invert_color(g.color)}) + dx = + base = g.base.max_steps + step = + if g.base.cur_step > base/2 then base - g.base.cur_step + else g.base.cur_step + (w*step)/(2*base) + do Canvas.fill_rect(ctx, dx-w/4, -w/4, w/4, w/4) + + do Canvas.restore(ctx) + void + + @private build_move_options(b:Base.t, no_back) = + all_options = [] : list(Base.direction) + |> (if Wall.at(b.pos.x+1, b.pos.y) then identity + else List.add({right}, _)) + |> (if Wall.at(b.pos.x-1, b.pos.y) then identity + else List.add({left}, _)) + |> (if Wall.at(b.pos.x, b.pos.y+1) then identity + else List.add({down}, _)) + |> (if Wall.at(b.pos.x, b.pos.y-1) then identity + else List.add({up}, _)) + if List.length(all_options) == 1 then all_options + else if no_back then + back = Base.Dir.back(b.dir) + List.filter(x -> x!=back, all_options) + else all_options + + @private move_one_generic(g:Ghost.t, move_fun) = + cur_step = g.base.cur_step + 1 + cur_step = + if cur_step >= g.base.max_steps then 0 + else cur_step + if cur_step != 0 then {g with base = {g.base with ~cur_step}} + else + (dx, dy) = Base.Dir.deltas(g.base.dir) + pos = { + x = g.base.pos.x + dx + y = g.base.pos.y + dy + } + g = {g with base = {g.base with ~pos}} + dirs = move_fun(g.base) + dir = List.get(Random.int(List.length(dirs)), dirs) ? {down} + {g with base = {g.base with ~dir ~cur_step}} + + @private move_one_dumb(ghost:Ghost.t) = + move_one_generic(ghost, build_move_options(_, true)) + + @private move_one_guard(ghost:Ghost.t, bp:Base.t) = + move_fun(bg) = + opts = build_move_options(bg, false) + can_see(dir) = + if bg.pos.x != bp.pos.x && bg.pos.y != bp.pos.y then false + else if bg.pos.x == bp.pos.x && bg.pos.y > bp.pos.y + && dir == {up} then true + else if bg.pos.x == bp.pos.x && bg.pos.y < bp.pos.y + && dir == {down} then true + else if bg.pos.y == bp.pos.y && bg.pos.x > bp.pos.x + && dir == {left} then true + else if bg.pos.y == bp.pos.y && bg.pos.x < bp.pos.x + && dir == {right} then true + else false + bias = List.filter(can_see, opts) + if bias == [] then + back = Base.Dir.back(bg.dir) + List.filter(x -> x!=back, opts) + else bias + move_one_generic(ghost, move_fun) + + move() = + g = game.get() + ghosts = List.map( + ghost -> match ghost.ai with + | {dumb} -> move_one_dumb(ghost) + | {guard} -> move_one_guard(ghost, g.pacman.base), + g.ghosts) + game.set({g with ~ghosts}) + + draw(ctx:Canvas.context) = + g = game.get() + List.iter(draw_one(ctx, _), g.ghosts) + +}} diff --git a/src/opacman.opa b/src/opacman.opa new file mode 100644 index 0000000..bdb8965 --- /dev/null +++ b/src/opacman.opa @@ -0,0 +1,131 @@ +/* Config */ + +fps = 60 +base_size = 32 +grid_width = 26 +grid_heigth = 29 + +/* Defaults */ + +default_game = { + pacman = Pacman.default + ghosts = Ghost.default + food = initial_food + score = 0 +} : Game.status + +/* Game */ + +@client game = Mutable.make(default_game) + +@client draw_grid(ctx:Canvas.context) = + w = base_size + do Canvas.save(ctx) + do Canvas.set_stroke_style(ctx, {color=Color.pink}) + do Canvas.set_line_width(ctx, 1.) + do Canvas.begin_path(ctx) + // lh = List.init(identity, grid_heigth) + do List.iter( + x -> + do Canvas.move_to(ctx, x*w, 1) + do Canvas.line_to(ctx, x*w, 1+w*grid_heigth) + void, + List.init(x->x+1, grid_width-1)) + do List.iter( + y -> + do Canvas.move_to(ctx, 1, y*w) + do Canvas.line_to(ctx, 1+w*grid_width, y*w) + void, + List.init(y->y+1, grid_heigth-1)) + do Canvas.stroke(ctx) + do Canvas.restore(ctx) + void + +@client clean_frame(ctx:Canvas.context) = + Canvas.clear_rect( + ctx, 0, 0, + 2+2*base_size*grid_width, + 2+2*base_size*grid_heigth) + +@client print_infos(g:Game.status) = + p = g.pacman + cont = + <> + Pacman at ({p.base.pos.x},{p.base.pos.y}), moving {"{p.base.dir}"} + - {Set.size(g.food)} food left + - Score: {g.score} + + Dom.transform([#info <- cont]) + +@client next_frame(ctx:Canvas.context)() = + do clean_frame(ctx) + do Pacman.move() + do Ghost.move() + do Wall.draw(ctx) + do Food.draw(ctx) + do Pacman.draw(ctx) + do Ghost.draw(ctx) + void + +@client keyfun(e) = + g = game.get() + p = g.pacman + p = match (p.base.dir, e.key_code) with + // z + | ({down}, {some=122}) -> + {p with next_dir={up} + base={p.base with dir={up} + cur_step=-p.base.cur_step}} + | (_, {some=122}) -> {p with next_dir={up}} + + // q + | ({right}, {some=113}) -> + {p with next_dir={left} + base={p.base with dir={left} + cur_step=-p.base.cur_step}} + | (_, {some=113}) -> {p with next_dir={left}} + + // s + | ({up}, {some=115}) -> + {p with next_dir={down} + base={p.base with dir={down} + cur_step=-p.base.cur_step}} + | (_, {some=115}) -> {p with next_dir={down}} + + // d + | ({left}, {some=100}) -> + {p with next_dir={right} + base={p.base with dir={left} + cur_step=-p.base.cur_step}} + | (_, {some=100}) -> {p with next_dir={right}} + + // space (pause) + | (_, {some=32}) -> {p with next_dir={still}} + | _ -> p + game.set({g with pacman=p}) + +@client init() = + match Canvas.get(#game_holder) with + | {none} -> void + | {some=canvas} -> + ctx = Canvas.get_context_2d(canvas) |> Option.get + t = Scheduler.make_timer(1000/fps, next_frame(ctx)) + _ = Dom.bind(Dom.select_document(), {keypress}, keyfun) + t.start() + +body() = + <> + + You can't see canvas, upgrade your browser ! + +
+ init()}> +
+ + +server = one_page_server("OPAcman", body) + +css = css + canvas { border: 1px solid black; } diff --git a/src/pacman.opa b/src/pacman.opa new file mode 100644 index 0000000..cbe604d --- /dev/null +++ b/src/pacman.opa @@ -0,0 +1,93 @@ +@client Pacman = {{ + + @server default = { + base = Base.make(0, 0, {right}, 10) + next_dir = {right} + mouth_state = 0 + mouth_incr = 1 + mouth_steps = 10 + } : Pacman.t + + draw(ctx:Canvas.context) = + g = game.get() + p = g.pacman + w = base_size + + mouth = p.mouth_state + dmouth = p.mouth_incr + steps = p.mouth_steps + + do Canvas.save(ctx) + do Canvas.set_fill_style(ctx, {color=Color.black}) + (center_x, center_y) = Base.center(p.base) + do Canvas.translate(ctx, center_x, center_y) + alpha = Base.Dir.facing_angle(p.base.dir) + do Canvas.rotate(ctx, alpha) + + angle = Math.PI*Int.to_float((steps-mouth)/(3*steps)) + x = Int.of_float(Float.of_int(w)*Math.cos(angle)/2.)-1 + y = (w*(steps-mouth))/(4*steps) + + do Canvas.begin_path(ctx) + do Canvas.move_to(ctx, -w/10, 0) + // Could replace all curves but currently not available in OPA :( + // do Canvas.arc(0, 0, w/2, -angle, angle, 1) + do Canvas.line_to(ctx, x, y) + do Canvas.quadratic_curve_to(ctx, w/2, w/2, 0, w/2) + do Canvas.quadratic_curve_to(ctx, -w/2, w/2, -w/2, 0) + do Canvas.quadratic_curve_to(ctx, -w/2, -w/2, 0, -w/2) + do Canvas.quadratic_curve_to(ctx, w/2, -w/2, x, -y) + do Canvas.fill(ctx) + do Canvas.restore(ctx) + + mouth = mouth + dmouth; + dmouth = + if (mouth == steps-1 || mouth == 0) then -dmouth + else dmouth + do game.set({g with pacman = { + p with + mouth_state = mouth + mouth_incr = dmouth + }}) + void + + move() = + g = game.get() + p = g.pacman + ignore_incr = p.base.cur_step < 0 + cur_step = p.base.cur_step + 1 + cur_step = if cur_step >= p.base.max_steps then 0 + else if p.base.dir == {still} then 0 + else cur_step + test_wall(on_ok, on_err, x, y) = + if Wall.at(x,y) then on_err + else on_ok + (dir, dx, dy) = + if cur_step != 0 || ignore_incr then (p.base.dir, 0, 0) + else + do print_infos(g) + (dx, dy) = Base.Dir.deltas(p.base.dir) + (ddx, ddy) = Base.Dir.deltas(p.next_dir) + dir = test_wall(p.next_dir, {still}, + p.base.pos.x+dx+ddx, p.base.pos.y+dy+ddy) + (dx, dy, dir) = + test_wall((dx,dy,dir), (0,0,{still}), + p.base.pos.x+dx, p.base.pos.y+dy) + (dir, dx, dy) + pos = { + x = p.base.pos.x + dx + y = p.base.pos.y + dy + } + (food, score) = + if cur_step != p.base.max_steps/2 then (g.food, g.score) + else + if Set.mem(pos, g.food) then + food = Set.remove(pos, g.food) + if food == Set.empty then (initial_food, g.score+1010) + else (food, g.score+10) + else (g.food, g.score) + pacman = {p with base = { p.base with + ~pos ~dir ~cur_step }} + game.set({g with ~pacman ~food ~score}) + +}} diff --git a/src/types.opa b/src/types.opa new file mode 100644 index 0000000..153d4d8 --- /dev/null +++ b/src/types.opa @@ -0,0 +1,38 @@ +type Base.direction = {still} / {up} / {down} / {left} / {right} + +type Base.pos = { + x : int + y : int +} + +type Base.t = { + pos : Base.pos + dir : Base.direction + cur_step : int /* Current step */ + max_steps : int /* Max steps in the move (determines speed) */ +} + +type Pacman.t = { + base : Base.t + next_dir : Base.direction + mouth_state : int + mouth_incr : int + mouth_steps : int +} + +type Ghost.ai = + {dumb} + / {guard} + +type Ghost.t = { + ai : Ghost.ai + base : Base.t + color : color +} + +type Game.status = { + pacman : Pacman.t + ghosts : list(Ghost.t) + food : set(Base.pos) + score : int +} diff --git a/src/wall.opa b/src/wall.opa new file mode 100644 index 0000000..9cb74ca --- /dev/null +++ b/src/wall.opa @@ -0,0 +1,18 @@ +@client Wall = {{ + + draw(ctx:Canvas.context) = + w = base_size + do Canvas.save(ctx) + do Canvas.set_fill_style(ctx, {color=Color.darkblue}) + do Set.iter( + ~{x y} -> Canvas.fill_rect(ctx, 1+x*w, 1+y*w, w, w), + walls) + do Canvas.restore(ctx) + void + + at(x, y) = + x >= grid_width || y >= grid_heigth + || x < 0 || y < 0 + || Set.mem(~{x y}, walls) + +}}