Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

basic multiplayer engine -- pacmans moving on the grid -- mix compiles #2

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ deps
*.beam
*.plt
erl_crash.dump
_build

52 changes: 52 additions & 0 deletions lib/pacman.ex
Original file line number Diff line number Diff line change
@@ -1,2 +1,54 @@
require Pacman.SharedEvents # ? just the first is required to mix
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe discuss this next meetup...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zampino Definitely! Looks interesting.

# require Pacman.Engine

defmodule Pacman do
@moduledoc """

## Pacman Module

home of Pacmans. Works as parent process for
the Engine and Shared Events.

## Run

$> iex -S mix

## Usage

Pacman.boot # starts the engine

Pacman.turn :up # changes direction of (default) pacman

Pacman.add :name # adds a player to the grid

Pacman.turn :down, :name # changes direction of :name Pacman
"""

def boot do
event_loop_pid = spawn_link(Pacman.SharedEvents, :events_loop, [])
Process.register event_loop_pid, :events
engine_pid = spawn_link(Pacman.Engine, :main, [])
Process.register engine_pid, :engine
add :default
IO.puts "started"
end

def event(event) do
:events <- {:queue_event, event}
end

def turn(direction, pacman // :default) do
pacman <- {:turn, direction}
end

def add(name) do
event [type: :register_pacman, name: name]
end

# NOTE: what happens to linked children if parent crashes/is killed?
# could we just kill the parent?
def exit do
:engine <- :quit
:events <- :quit
end
end
46 changes: 46 additions & 0 deletions lib/pacman/engine.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
defmodule Pacman.Engine do

@doc "the main animation loop changes states of the pacman's world"
def main(world // Pacman.World.new) do
catch_exit
event = fetch_event
IO.puts "received: #{inspect(event)}"
world = react_on_event(world, event)
world = Pacman.World.move_pacmans(world)
# TODO:
# world = World.eat_food(world)
Pacman.World.represent(world)
# NOTE: should be rather run on 24/frames sec
:timer.sleep 200
main(world)
end

def catch_exit do
receive do
:quit -> Process.exit(self, :kill)
after
0 -> "no exit signal"
end
end

@doc "this ensures we process just
one shared event per cycle in a non-blocking fashion"
def fetch_event do
:events <- :pop_event
receive do
{:event, event} -> event
after
# NOTE: we could even use this as sleep time...
0 -> nil
end
end

@doc "changes the world's state based on incoming shared event"
def react_on_event(world, [type: :register_pacman, name: name]) do
Pacman.World.register_pacman(world, name)
end

def react_on_event(world, _) do
world
end
end
21 changes: 21 additions & 0 deletions lib/pacman/shared_events.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule Pacman.SharedEvents do
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if we need this module, I just wanted to experiment "sharing states" ...


# NOTE: not sure we want to rebuild a mailbox
# might solve synchronization issues (?)

def events_loop(events_queue // []) do
receive do
:quit -> Process.exit(self, :kill)

{:queue_event, event} ->
# NOTE: couldn't find better push for List
events_queue = List.flatten(events_queue, [event])
events_loop(events_queue)

:pop_event when length(events_queue) > 0 ->
[event | rest] = events_queue
:engine <- {:event, event}
events_loop(rest)
end
end
end
16 changes: 16 additions & 0 deletions lib/pacman/user_events.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule Pacman.UserEvents do

# NOTE: maybe move data from the grid's pacmans to this state
# there might be synchronization issues
defrecord State, direction: :right

def events_loop(name, state // State.new) do
receive do
:fetch_direction ->
:engine <- {:new_direction, state.direction}
events_loop name, state
{:turn, direction} ->
events_loop name, state.update(direction: direction)
end
end
end
89 changes: 89 additions & 0 deletions lib/pacman/world.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
defmodule Pacman.World do
@size 20
@directions [right: {1,0}, up: {0, -1}, left: {-1, 0}, down: {0, 1}]

@moduledoc """
Keeps track of instant state of the Grid
has two record definitions:
Grid and Pacman.

Grid#pacmans is a hash of :name => Pacman records

Stores 'Class instance variables':

- @size (of the grid)

- @directions human -> vector translations

### NOTE

World should rather implement a protocol for the Grid
in the sense of http://elixir-lang.org/getting_started/4.html
or maybe something like @impl
"""

defrecord Grid, pacmans: HashDict.new, food: [], phantoms: []
defrecord Pacman, direction: {1,0}, position: {div(@size, 2), div(@size, 2)}, score: 0

def new do
Grid.new
end

def spawn_user_events(name) do
# FIXME: why do I need to namespace from root?
pid = spawn_link(Elixir.Pacman.UserEvents, :events_loop, [name])
Process.register pid, name
end

@doc "register a new pacman user process under its name and updates the grid"
def register_pacman(Grid[] = grid, name) do
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be abstracted to move also monsters in the grid....

pacmans = grid.pacmans
pacmans = HashDict.put_new pacmans, name, Pacman.new
spawn_user_events(name)
grid.update(pacmans: pacmans)
end

def move_pacmans(Grid[] = grid) do
grid.update_pacmans fn(pacmans) -> HashDict.new(Enum.map(pacmans, displace)) end
end

def displace do
fn({name, pcm}) ->
{dx, dy} = ask_direction(name, pcm.direction)
{x, y} = pcm.position
new_x = wrap_position(x, dx)
new_y = wrap_position(y, dy)
new_position = {new_x, new_y}
new_pcm = pcm.update(position: new_position)
{name, new_pcm}
end
end

@doc "again a 'synchronous call' to ask the direction to the user's process which falls back to the old direction"
def ask_direction(name, old_dir) do
name <- :fetch_direction
receive do
{:new_direction, dir} -> translate_direction(dir)
after
0 ->
IO.puts "missed!"
old_dir
end
end

def translate_direction(name) do
@directions[name]
end

def wrap_position(value, delta) do
rem(@size + value + delta, @size)
end

@doc "a Grid's instantaneous representation"
def represent(Grid[] = grid) do
represent_one = fn({name, pcm}) ->
"#{name}: #{inspect(pcm.position)} -- score: #{pcm.score}"
end
IO.puts(Enum.map_join(grid.pacmans, "\n", represent_one))
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Pacman.Mixfile do
def project do
[ app: :pacman,
version: "0.0.1",
elixir: "~> 0.10.1",
elixir: "~> 0.11.2",
deps: deps ]
end

Expand Down