Created
April 27, 2010 21:39
-
-
Save kixxauth/381373 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
%% | |
%% 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