Skip to content

Instantly share code, notes, and snippets.

@kixxauth
Created April 27, 2010 21:39
Show Gist options
  • Save kixxauth/381373 to your computer and use it in GitHub Desktop.
Save kixxauth/381373 to your computer and use it in GitHub Desktop.
%%
%% jkp.erl Was created for The Hudson Valley Programmers' Meetup
%% homework assignment #13. The requirement is to build a
%% rock -- paper -- scissors game between parallel processes.
%% http://hvprogrammers.org/homework-13.html
%%
%% It's also my first experiment with Erlang.
%%
%% [email protected]
%%
%% Start an Erlang session while in the same directory as jkp.erl and then
%% `c(jkp).`
%% to compile the jkp.erl file into the jkp module and then
%% `jkp:main(PLAYERS).`
%% where PLAYERS is the number of player processes to run.
%%
%% An Erlang module starts out just like we would expect it to.
%% I like the fact that exports are explicit. Erlang achieves some level
%% of security by encapsulating modules and allowing module authors to
%% explicitly export functions meant to be public.
-module(jkp).
-export([main/1, player/0]).
%% There are some notes about syntax scattered throughout the comments.
%% The first is that functions are always referred to by their name and arity
%% like main/1 and player/0 in the -export() declaration.
%% Another is that lists and tuples can be created with
%% the literals [] and {}.
%% One important note is that variables are not variable. They are immutable
%% and cannot be reassigned once an assignment has been made. Also, they
%% must be named starting with an uppercase letter or underscore. Tokens
%% starting with a lowercase letter are atoms, which is something totally
%% different than a variable in Erlang.
%% Another nice thing about Erlang is that it allows us to define functions
%% anywhere in the module and call them from anywhere else. I like see programs
%% that read from top down instead of bottom up and so I create the main
%% function right at the top of the module.
%% Erlang does parallelism well. It is a shared nothing message passing
%% environment that kicks off in the main/1 function.
%% To operate from the command line main/1 must take a string as an argument and
%% convert to an integer which seems like a major PITA for Erlang.
main(N) ->
register(mother_ship, self()),
io:format(" First Player -> ~w~n", [self()]),
%% spawn_players/3 returns a list and the | operator pushed self() onto the head
Players = [self() | spawn_players(0, N -1, [])],
%% fun() ... end creates an anonymous function.
%% This anonymous function uses the ! operator to send a message.
%% The left side of ! must evaluate to the process id of the target,
%% and right side evaluation will be sent as the message value.
lists:foreach(fun(P) -> P ! Players end, Players),
player(),
collect_scores(length(Players), 0),
halt().
%% The first thing that the main/1 function does is register the
%% main process to the global mother_ship atom. Whenever self() is called, it
%% returns the UID of the process from which it was called. The UID cannot be
%% guessed by other processes, and so other processes must be explicitly
%% introduced to each other. This is another great security feature. However,
%% for convenience Erlang allows us to register a process id with an atom like
%% mother_ship. Other processes may use the mother_ship atom to send messages
%% to the mother_ship process, but it is not possible for them to derive the
%% UID of the mother_ship process from the atom.
%% Functions can be built using multiple clauses usually separated by ;.
%% The , is used for andalso while the ; is used for orelse.
%% spawn_players/3 recursively spawns all the player processes for the
%% rock - paper - scissors game.
spawn_players(N, Threads, Players) when N >= Threads ->
Players;
spawn_players(N, Threads, Players) ->
%% Spawning a process returns the process identifier.
Pid = spawn(?MODULE, player, []),
io:format(" Spawned Player ~w~n", [Pid]),
%% Recurse.
spawn_players(N+1, Threads, [Pid|Players]).
%% collect_scores/2 is another recursive function. It implements a loop that
%% will collect messages from the spawned players.
collect_scores(NumPlayers, N) when N >= NumPlayers ->
true;
collect_scores(NumPlayers, N) ->
%% The receive operator checks the mailbox for this process.
receive
%% Check for the correct message by pattern matching.
%% This pattern matcher is looking for a tuple of the form {A, B}.
{Player, Score} ->
io:format(" * Player ~w scored ~w~n", [Player, Score]),
collect_scores(NumPlayers, N +1);
%% The underscore is used as a catchall in Erlang.
_ ->
collect_scores(NumPlayers, N)
end.
%% The player/0 function starts a new player process when called by spawn/3.
player() ->
seed_random_algo(),
mother_ship ! {self(), game(meet_other_players())}.
%% First, seed the random number algo so it will
%% actually generate random numbers.
seed_random_algo() ->
{A1, A2, A3} = now(),
random:seed(A1, A2, A3).
%% This is another message listening loop.
%% It waits for the main process to introduce this process to the other
%% processes by sending a message.
meet_other_players() ->
receive
%% mother_ship will send all processes a list of all players.
Players when is_list(Players) ->
%% A list comprehension is used to filter out self(),
%% because this process does not need to be introduced to itself.
[X || X <- Players, X =/= self()];
%% Skip all messages that are not lists.
_ ->
meet_other_players()
end.
%% This function just calls the recursive play/4 function.
game(Players) ->
play(Players, 1000, 0, 0).
%% _Players has a leading underscore because it is not used in the first clause
%% of this function.
play(_Players, UpperBound, Score, N) when N >= UpperBound ->
%% When the play count (N) has reached the UpperBound,
%% return the accumulated Score.
Score;
%% The second clause of the play/4 function is where the action happens.
play(Players, UpperBound, Score, N) ->
%% Choose rock, paper, or scissors.
Choice = strategy(),
%% Send Choice to the other players.
lists:foreach(fun(P) -> P ! Choice end, Players),
%% When get_results/4 returns this function calls itself again.
play(Players, UpperBound, Score + get_results(Players, Choice, 0, 0), N +1).
%% A very simple strategy.
%% TODO: Try different strategies.
strategy() ->
%% Get a random number between 0 and 1.
Seed = random:uniform(),
if Seed < 0.33 -> rock;
Seed < 0.66 -> paper;
true -> scissors
end.
%% Another recursive function. This one continues to check the mailbox
%% for this process and will not return until it has all the plays from
%% the other players and has calculated the score for this round.
get_results(Players, _Choice, Score, N) when N >= length(Players) ->
Score;
get_results(Players, Choice, Score, N) ->
receive
rock when Choice =:= scissors ->
get_results(Players, Choice, Score +1, N +1);
paper when Choice =:= rock ->
get_results(Players, Choice, Score +1, N +1);
scissors when Choice =:= paper ->
get_results(Players, Choice, Score +1, N +1);
_ ->
get_results(Players, Choice, Score, N +1)
end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment