diff --git a/ebin/ggs.app b/ebin/ggs.app index 3315864..b3ea744 100644 --- a/ebin/ggs.app +++ b/ebin/ggs.app @@ -3,7 +3,8 @@ {vsn, "0.1.0"}, {modules, [ ggs_app, - ggs_sup + ggs_sup, + ggs_dispatcher ]}, {registered, [ggs_sup]}, {applications, [kernel, stdlib]}, diff --git a/erlang_js b/erlang_js index 5350ed2..2f2785f 160000 --- a/erlang_js +++ b/erlang_js @@ -1 +1 @@ -Subproject commit 5350ed21606606dbee5ecb07e974f2abb9106270 +Subproject commit 2f2785fafb0da6db75810eb6fa97d09c58257588 diff --git a/games/tic-tac-toe-js/js/TicTacToeClient.js b/games/tic-tac-toe-js/js/TicTacToeClient.js index 80f8ef1..79e4064 100644 --- a/games/tic-tac-toe-js/js/TicTacToeClient.js +++ b/games/tic-tac-toe-js/js/TicTacToeClient.js @@ -90,4 +90,4 @@ TicTacToeClient.prototype.updateBoard = function(gameBoardData) { this.spots[k++].innerHTML = t; } } -} \ No newline at end of file +} diff --git a/games/tic-tac-toe/TODO b/games/tic-tac-toe/TODO index 07c0710..a957a1f 100644 --- a/games/tic-tac-toe/TODO +++ b/games/tic-tac-toe/TODO @@ -1,12 +1,6 @@ -- background image -- subimages for game_area:s -- subimages for game markers (X or 0) -- rectangle collision on game_area:s +- background - redraw background then all game_area:s -- board_state: a hashtable where key is game_area_index - and value is either'x' 'o' or ' ' - board contains nr_of_squares = 9 board contains array of squares with nr_of_squares elements board contains x, y, width and height board contains a frame diff --git a/games/tic-tac-toe/data.py b/games/tic-tac-toe/data.py new file mode 100644 index 0000000..b83c3ad --- /dev/null +++ b/games/tic-tac-toe/data.py @@ -0,0 +1,15 @@ +def greatest_sequence(match, pattern): + m = match + p = pattern + size = 0 + max_size = 0 + + for p in pattern: + if m == p: + size += 1 + else: + if size > max_size: + max_size = size + size = 0 + + return max_size diff --git a/games/tic-tac-toe/data.pyc b/games/tic-tac-toe/data.pyc new file mode 100644 index 0000000..682dfaa Binary files /dev/null and b/games/tic-tac-toe/data.pyc differ diff --git a/games/tic-tac-toe/e.png b/games/tic-tac-toe/e.png index 4cbf977..1b1d7f7 100644 Binary files a/games/tic-tac-toe/e.png and b/games/tic-tac-toe/e.png differ diff --git a/games/tic-tac-toe/player.py b/games/tic-tac-toe/player.py new file mode 100644 index 0000000..081112c --- /dev/null +++ b/games/tic-tac-toe/player.py @@ -0,0 +1,15 @@ +from pygame.mouse import get_pos +from point import Point + +class Player(object): + def __init__(self, id, shape, board): + self.shape = shape + self.board = board + self.id = id + + + def turn(self): + #Ask mouse for position + board.make_turn(Point(get_pos()[0],get_pos()[1])) + + diff --git a/games/tic-tac-toe/server.py b/games/tic-tac-toe/server.py new file mode 100644 index 0000000..91e32b5 --- /dev/null +++ b/games/tic-tac-toe/server.py @@ -0,0 +1,23 @@ +#server.py +import json +from socket import socket, AF_INET, SOCK_STREAM + + +class server(object): + def __init__(self, port=None): + self.port = port + self.world = GGS.init() + self.socket = socket(AF_INET, SOCK_STREAM) + self.socket.connect(("www.???.com", 80)) + + def turn(self, id, index): + rows = sqrt(board.nr_of_rectangles) + x = int(index / rows) + y = int(index % rows) + + json.dumps({"x": x, "y": y} + world.callCommand("tictactoe", "set", json.dumps({"x": x, "y": y})) + + sent = 0 + length = len( + while sent diff --git a/games/tic-tac-toe/test_data.py b/games/tic-tac-toe/test_data.py new file mode 100644 index 0000000..38282f0 --- /dev/null +++ b/games/tic-tac-toe/test_data.py @@ -0,0 +1,14 @@ +import unittest +import data + + +class TestData(unittest.TestCase): + def setUp(self): + array = [0,1,1,1,3,4,5,2,2,3,3,3,3,3,33,4,2,2] + + self.assertTrue(data.greatest_sequence(array, 3) == 5) + +if __name__ == '__main__': + unittest.main() + + diff --git a/games/tic-tac-toe/tictactoeboard.py b/games/tic-tac-toe/tictactoeboard.py index ee1c057..10562dd 100644 --- a/games/tic-tac-toe/tictactoeboard.py +++ b/games/tic-tac-toe/tictactoeboard.py @@ -4,6 +4,7 @@ from point import Point from pygame.image import load from pygame.rect import Rect from pygame import Surface +from data import greatest_sequence #inherits Board. #Used for displaying the board on the screen and interact with it @@ -47,4 +48,14 @@ class TicTacToeBoard(Board): game_rectangle.state = 'o' self.players_turn = (self.players_turn + 1) % 2 + """ + def turn(self, mouse_point): + if player.id != players_turn: + print "Other players turn" + else: + for game_rectangle in self.game_rectangles: + if (mouse_point.inside(game_rectangle) and + game_rectangle.state == ' '): + server.turn(player.id, game_rectangle.index) + """ diff --git a/games/tic-tac-toe/tictactoeboard.pyc b/games/tic-tac-toe/tictactoeboard.pyc index 1243dc1..8913f16 100644 Binary files a/games/tic-tac-toe/tictactoeboard.pyc and b/games/tic-tac-toe/tictactoeboard.pyc differ diff --git a/mnesia/.gamedb.erl.swp b/mnesia/.gamedb.erl.swp new file mode 100644 index 0000000..469b1f8 Binary files /dev/null and b/mnesia/.gamedb.erl.swp differ diff --git a/Mnesia/gamedb.erl b/mnesia/gamedb.erl similarity index 50% rename from Mnesia/gamedb.erl rename to mnesia/gamedb.erl index 9d6bbbe..751eb92 100644 --- a/Mnesia/gamedb.erl +++ b/mnesia/gamedb.erl @@ -1,26 +1,45 @@ -%Test Mnesia +%%%%---------------------------------------------------- +%%% @author Mattias Pettersson +%%% @copyright 2011 Mattias Pettersson +%%% @doc Database for runtime game variable storage. +%%% @end + + Test Mnesia -module(gamedb). -import(mnesia). -export([init/0,insert_player/1,example_player/0,read_player/1,test_player/0]). -include("gamedb.hrl"). +%%----------------------------------------------------- +%% Creation +%%----------------------------------------------------- init() -> mnesia:create_table(player, [{attributes, record_info(fields, player)}]). +%%----------------------------------------------------- +%% Test +%%----------------------------------------------------- test_player() -> insert_player(example_player()), read_player(0001). +example_player() -> + #player{id = 0001, + name = "Tux"}. + +%%----------------------------------------------------- +%% Insertions +%%----------------------------------------------------- insert_player(Player) -> Fun = fun() -> mnesia:write(Player) end, mnesia:transaction(Fun). -example_player() -> - #player{id = 0001, - name = "Tux"}. +%%----------------------------------------------------- +%% Querries +%%----------------------------------------------------- read_player(Player_Key) -> Fun = fun() -> [P] = mnesia:read(player, Player_Key), diff --git a/Mnesia/gamedb.hrl b/mnesia/gamedb.hrl similarity index 100% rename from Mnesia/gamedb.hrl rename to mnesia/gamedb.hrl diff --git a/Mnesia/gamedb_usage.txt b/mnesia/gamedb_usage.txt similarity index 100% rename from Mnesia/gamedb_usage.txt rename to mnesia/gamedb_usage.txt diff --git a/python_client b/python_client index 9a1f1a9..e265120 100755 --- a/python_client +++ b/python_client @@ -11,7 +11,7 @@ s.connect((HOST, PORT)) print "Saying hello to server" s.send( -"Command: hello\n\ +"Server-Command: hello\n\ Content-Type: text\n\ Content-Length: 0\n\ \n\ @@ -27,7 +27,7 @@ print "Data: ", ' '.join(data.split(" ")[1:]) print "Defining a function called myFun" s.send( "Token: %s\n\ -Command: define\n\ +Server-Command: define\n\ Content-Type: text\n\ Content-Length: 49\n\ \n\ @@ -42,7 +42,7 @@ print "Data: ", ' '.join(data.split(" ")[1:]) print "Calling myFun" s.send( "Token: %s\n\ -Command: call\n\ +Server-Command: call\n\ Content-Type: text\n\ Content-Length: 6\n\ \n\ @@ -64,7 +64,7 @@ s.connect((HOST, PORT)) print "Calling myFun" s.send( "Token: %s\n\ -Command: call\n\ +Server-Command: call\n\ Content-Type: text\n\ Content-Length: 6\n\ \n\ diff --git a/src/.ggs_connection.erl.swp b/src/.ggs_connection.erl.swp new file mode 100644 index 0000000..0c009f8 Binary files /dev/null and b/src/.ggs_connection.erl.swp differ diff --git a/src/ggs_backup.erl b/src/ggs_backup.erl deleted file mode 100644 index 30c80a2..0000000 --- a/src/ggs_backup.erl +++ /dev/null @@ -1,41 +0,0 @@ --module(ggs_backup). --behaviour(gen_server). - -%% API --export([start_link/0 ]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - - --define(SERVER, ?MODULE). - --record(state, {port, lsock, client_vm_map = []}). - -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). - -init([]) -> - {ok, #state{port = -1, lsock = -1, client_vm_map = -1}, 0}. - -handle_call(get_backup, _From, State) -> - BackedUpState = case State of - #state{port = -1, lsock = -1, client_vm_map = -1} -> - not_initialized; - Other -> - Other - end, - {reply, {backup_state, BackedUpState}, State}. - -handle_cast({set_backup, NewState}, _State) -> - {noreply, NewState}. - -handle_info(_Msg, State) -> - {noreply, State}. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -terminate(_Reason, _State) -> - ok. diff --git a/src/ggs_coordinator.erl b/src/ggs_coordinator.erl new file mode 100644 index 0000000..733ff8d --- /dev/null +++ b/src/ggs_coordinator.erl @@ -0,0 +1,100 @@ +-module(ggs_coordinator). + +%% API Exports +-export([start_link/0, stop/1, join_table/1, create_table/1, join_lobby/0, + respawn_player/2, respawn_table/1, remove_player/2]). + +%% gen_server callback exports +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). +-define(SERVER, ?MODULE). + +-record(co_state, + {players = [], % List of all player processes + player_table_map = [], % Players <-> Table map + table_state_map = [], + tables = []}). % Table <-> Table state map + +%% @doc This module act as "the man in the middle". +%% Creates the starting connection between table and players. + +%% @doc Starts the coordinator process. +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +%% @doc Terminates the coordinator process. +stop(Reason) -> + gen_server:cast(ggs_coordinator, {stop, Reason}). + +%% @doc Joins table with specified token, returns {error, no_such_table} +%% if the specified table token does not exist +join_table(Token) -> + gen_server:call(ggs_coordinator, {join_table, Token}). + +%% @doc Create a new table, return {error, Reason} or {ok, TableToken} +create_table(Params) -> + gen_server:call(ggs_coordinator, {create_table, Params}). + +%% @doc This is the first function run by a newly created players. +%% Generates a unique token that we use to identify the player. +join_lobby() -> + gen_server:call(ggs_coordinator, join_lobby). + +%% @doc Act as a supervisor to player and respawns player when it gets bad data. +respawn_player(_Player, _Socket) -> + ggs_logger:not_implemented(). + +%% @doc Act as a supervisor to table and respawns table when it gets bad data. +respawn_table(_Token) -> + ggs_logger:not_implemented(). + +%% @doc Removes a player from coordinator. +remove_player(_From, _Player) -> + ggs_logger:not_implemented(). + +%% gen_server callbacks + +init([]) -> + {ok, #co_state{}}. + +handle_call(join_lobby, _From, State) -> + Token = helpers:get_new_token(), + {reply, {ok, Token}, State}; + +handle_call({join_table, Table}, From, State) -> + {FromPlayer, _Ref} = From, + Tables = State#co_state.tables, + case lists:keyfind(Table, 1, Tables) of + {TableID, TablePID} -> + ggs_table:add_player(TablePID, FromPlayer), + {reply, {ok, TablePID}, State}; + false -> + {reply, {error, no_such_table}, State} + end; + +handle_call({create_table, {force, TableID}}, From, State) -> + TableIDMap = State#co_state.player_table_map, + Tables = State#co_state.tables, + NewTableProc = ggs_table:start_link(), + {reply, {ok, TableID}, State#co_state{ + player_table_map = [{From, TableID} | TableIDMap], + tables = [{TableID, NewTableProc} | Tables] + }}; + +handle_call(_Message, _From, State) -> + {noreply, State}. + +handle_cast({stop, Reason}, State) -> + {stop, normal, state}; + +handle_cast(_Message, State) -> + {noreply, State}. + +handle_info(_Message, State) -> + {noreply, State}. + +terminate(normal, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/src/ggs_dispatcher.erl b/src/ggs_dispatcher.erl new file mode 100644 index 0000000..49dcf4b --- /dev/null +++ b/src/ggs_dispatcher.erl @@ -0,0 +1,68 @@ +-module(ggs_dispatcher). + +-behaviour(gen_server). + +%% API Exports +-export([start_link/1, stop/1]). + +%% gen_server callback exports +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). + +-define(SERVER, ?MODULE). + + +%% @doc This module is the entry-point for clients connecting to GGS. This is +%% the module responsible for: +%% * Greeting a connecting client, and associating a socket for it +%% * Spawning a ggs_player for the connecting client, passing the socket + +%% @doc Starts a new dispatcher with the specified port. Registers this +%% dispatcher under the name "ggs_dispatcher". The pid of the dispatcher +%% is returned. +%% @spec start_link(Port) -> Pid +%% Port = Integer +%% Pid = # +start_link(Port) -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [Port], []). + +%% @doc Stops the dispatcher with the specified reason. +%% @spec stop(Reason) -> ok. +%% Reason = String +stop(_Reason) -> ggs_logger:not_implemented(). + +%% gen_server callbacks + +%% @doc Initiate the dispatcher. This is called from gen_server +init([Port]) -> + {ok, LSock} = gen_tcp:listen(Port, [{active, true}, + {reuseaddr, true}]), + {ok, LSock, 0}. + +handle_call(_Message, _From, State) -> + {noreply, State}. + +handle_cast(_Message, State) -> + {noreply, State}. + +handle_info({tcp, _Socket, RawData}, State) -> + io:format("Got connect request!~n"), + {noreply, State}; + +handle_info({tcp_closed, Socket}, State) -> + gen_tcp:close(Socket), + {stop, "Client closed socket", State}; + +%% @doc This is our function for accepting connections. When a client connects, +%% it will immediately time out due to timing settings set in init and here, +%% and when it does, we accept the connection. +handle_info(timeout, LSock) -> + {ok, Sock} = gen_tcp:accept(LSock), + spawn(ggs_player, start_link, [Sock]), + {noreply, LSock, 0}. + +terminate(normal, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/src/ggs_gamevm.erl b/src/ggs_gamevm.erl new file mode 100644 index 0000000..6dd19a7 --- /dev/null +++ b/src/ggs_gamevm.erl @@ -0,0 +1,130 @@ +%% @doc This module is responsible for running the game VM:s. You can issue +%% commands to a vm using this module. + +-module(ggs_gamevm). +-behaviour(gen_server). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, { port, table } ). + +%% API +-export([start_link/1, define/2, user_command/4, stop/1, call_js/2]). + +-include_lib("eunit/include/eunit.hrl"). + + +%% ---------------------------------------------------------------------- +% API implementation + +%% @doc Create a new VM process. The process ID is returned and can be used +%% with for example the define method of this module. +start_link(Table) -> + erlang_js:start(), %% @TODO: should only be done once + {ok, Pid} = gen_server:start_link(?MODULE, [Table], []), + Pid. + +%% @doc Define some new code on the specified VM, returns the atom ok. +define(GameVM, SourceCode) -> + gen_server:cast(GameVM, {define, SourceCode}). + +%% @doc Execute a user command on the specified VM. This function is +%% asynchronous, and returns ok. +%% @spec user_command(GameVM, User, Command, Args) -> ok +%% GameVM = process IS of VM +%% Player = the player running the command +%% Command = a game command to run +%% Args = arguments for the Command parameter +user_command(GameVM, Player, Command, Args) -> + gen_server:cast(GameVM, {user_command, Player, Command, Args}). + +%% @private +% only for tests +call_js(GameVM, SourceCode) -> + gen_server:call(GameVM, {eval, SourceCode}). + +% @doc stops the gamevm process +stop(GameVM) -> + gen_server:cast(GameVM, stop). + + +%% ---------------------------------------------------------------------- + +%% @private +init([Table]) -> + process_flag(trap_exit, true), + {ok, Port} = js_driver:new(), + %% @TODO: add here default JS API instead + {ok, #state { port = Port, table = Table }}. + +%% private +% only needed for the tests +handle_call({eval, SourceCode}, _From, #state { port = Port } = State) -> + {ok, Ret} = js:eval(Port, list_to_binary(SourceCode)), + {reply, Ret, State}. + +%% @private +handle_cast({define, SourceCode}, #state { port = Port } = State) -> + ok = js:define(Port, list_to_binary(SourceCode)), + {noreply, State}; +handle_cast({user_command, Player, Command, Args}, #state { port = Port } = State) -> + Arguments = string:concat("'", string:concat( + string:join([js_escape(Player), js_escape(Command), js_escape(Args)], "','"), "'")), + Js = list_to_binary(string:concat(string:concat("userCommand(", Arguments), ");")), + js_driver:define_js(Port, Js), + {noreply, State}; +handle_cast(stop, State) -> + {stop, normal, State}; +handle_cast(Msg, S) -> + error_logger:error_report([unknown_msg, Msg]), + {noreply, S}. + +%% @private +handle_info(Msg, S) -> + error_logger:error_report([unknown_msg, Msg]), + {noreply, S}. + +%% @private +terminate(_Reason, _State) -> + ok. + +%% @private +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +js_escape(S) -> + lists:flatmap(fun($\') -> [$\\, $\']; (X) -> [X] end, S). + +%% ---------------------------------------------------------------------- +% Tests + +start_link_test() -> + erlang_js:start(), %% @TODO: should only be done once + GameVM = start_link(test_table), + ?assertNot(GameVM =:= undefined). + +define_test() -> + GameVM = start_link(test_table), + define(GameVM, "function hello(test) { return test; }"), + ?assertMatch(<<"jeena">>, gen_server:call(GameVM, {eval, "hello('jeena')"})). + +stop_test() -> + GameVM = start_link(test_table), + ok = stop(GameVM). + +user_command_test() -> + GameVM = start_link(test_table), + define(GameVM, "var t = '';\nfunction userCommand(user, command, args) { t = user + command + args; }\n"), + user_command(GameVM, "'jeena", "thecommand", "theargs'"), + ?assertMatch(<<"'jeenathecommandtheargs'">>, gen_server:call(GameVM, {eval, "t;"})). + +js_erlang_test() -> + GameVM = start_link(test_table), + define(GameVM, "var t = '';\nfunction userCommand(user, command, args) { t = callErlang('erlang time') + ''; }\n"), + user_command(GameVM, "", "", ""), + {A, B, C} = erlang:time(), + T = "{" ++ integer_to_list(A) ++ ", " ++ integer_to_list(B) ++ ", " ++ integer_to_list(C) ++ "}", + ?assertMatch(T, binary_to_list(gen_server:call(GameVM, {eval, "t;"}))). + diff --git a/src/ggs_gamevm_e.erl b/src/ggs_gamevm_e.erl new file mode 100644 index 0000000..76b9350 --- /dev/null +++ b/src/ggs_gamevm_e.erl @@ -0,0 +1,41 @@ +-module(ggs_gamevm_e). +-export([start_link/1, define/2, user_command/4]). +%% @doc This module is responsible for running the game VM:s. You can issue +%% commands to a vm using this module. + +%% @doc Create a new VM process. The process ID is returned and can be used +%% with for example the define method of this module. +start_link(Table) -> + PortPid = spawn( fun() -> + loop(Table) + end ), + PortPid. + +%% @doc Define some new code on the specified VM, returns the atom ok. +define(GameVM, SourceCode) -> + GameVM ! {define,SourceCode}, + ok. + +%% @doc Execute a user command on the specified VM. This function is +%% asynchronous, and returns ok. +%% @spec user_command(GameVM, User, Command, Args) -> ok +%% GameVM = process IS of VM +%% Player = the player running the command +%% Command = a game command to run +%% Args = arguments for the Command parameter +user_command(GameVM, Player, Command, Args) -> + Ref = make_ref(), + GameVM ! {user_command, Player, Command, Args, self(), Ref}, + ok. + +%% Helper functions + +loop(Table) -> + receive + {define, SourceCode} -> + loop(Table); + {user_command, _User, Command, _Args, _From, _Ref} -> + io:format("GameVM received a message~n"), + ggs_table:notify_all_players(Table, Command), + loop(Table) + end. diff --git a/src/ggs_logger.erl b/src/ggs_logger.erl new file mode 100644 index 0000000..49f5abc --- /dev/null +++ b/src/ggs_logger.erl @@ -0,0 +1,8 @@ +-module(ggs_logger). +-export([not_implemented/0, log/2]). + +not_implemented() -> + exit(not_implemented). + +log(Format, Args) -> + error_logger:info_msg(Format, Args). diff --git a/src/ggs_mnesia_controller_server.erl b/src/ggs_mnesia_controller_server.erl deleted file mode 100644 index c1f8a10..0000000 --- a/src/ggs_mnesia_controller_server.erl +++ /dev/null @@ -1,68 +0,0 @@ --module(ggs_mnesia_controller_server). --behaviour(gen_server). - -%% API --export([start_link/0, - stop/0 - ]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - - --define(SERVER, ?MODULE). - --record(state, {}). - -%%%==================================================== -%%% API -%%%==================================================== - -%%----------------------------------------------------- -%% @doc Starts the server -%% @end -%%----------------------------------------------------- -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). - -%%----------------------------------------------------- -%% @doc Stops the server. -%% @spec stop() -> ok -%% @end -%%----------------------------------------------------- -stop() -> - gen_server:cast(?SERVER, stop). - -%%----------------------------------------------------- -%% gen_server callbacks -%%----------------------------------------------------- - -init([]) -> - mnesia:create_schema([node()]), - mnesia:start(), - {ok, {}, 0}. - -handle_cast(a, State) -> - {noreply, State}. - -% Request a value from the Mnesia database -handle_call({getValue, _Key},_From,State) -> - {reply,value_of_key_requested_goes_here, State}; - -% Set a value in the Mnesia database -handle_call({setValue, _Key, Value},_From,State) -> - {reply,value_set_or_updated, State}. - -handle_info(timeout, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%----------------------------------------------------- -%% Internal functions -%%----------------------------------------------------- diff --git a/src/ggs_player.erl b/src/ggs_player.erl new file mode 100644 index 0000000..af92ad9 --- /dev/null +++ b/src/ggs_player.erl @@ -0,0 +1,66 @@ +-module(ggs_player). +-export([start_link/1, notify/3, get_token/1, stop/2]). +-record(pl_state, + {token, % Player's token + socket, % Player's socket + table}). % Player's table + +%% @doc This module handles communication between a player and GGS. This module is +%%responsible for: +%% * The storage of the player socket, player token and a table token. +%% * Ability to fetch a player token. +%% * Forwarding messages from players to the game +%% * Remove a player from GGS + +%% @doc Spawns a process representing the player in GGS. Takes the player socket as +%% an argument for storage and later usage. Creates a unique player token +%% identifying the player. +%% @spec start_link(Socket::socket()) -> {ok, Pid} | {error, Reason} +start_link(Socket) -> + % The socket is in 'active' mode, and that means we are pushed any data + % that arrives on it, we do not need to recv() manually. Since the socket + % was opened in our parent process, we need to change the owner of it to + % us, otherwise these messages end up in our parent. + erlang:port_connect(Socket, self()), + {ok, Token} = ggs_coordinator:join_lobby(), + TableStatus = ggs_coordinator:join_table(1337), + case TableStatus of + {ok, Table} -> + loop(#pl_state{socket = Socket, token = Token, table = Table}); + {error, no_such_table} -> + ggs_coordinator:create_table({force, 1337}), + {ok, Table} = ggs_coordinator:join_table(1337), + loop(#pl_state{socket = Socket, token = Token, table = Table}) + end. + +%% @doc Handles incoming messages from the GGS and forwards them through the player +%% socket to the player. +%% @spec notify(Player::Pid(), From::Pid(), +%% {Command::String(), Message::string()}) -> ok +notify(Player, From, Message) -> + Player ! {notify, From, Message}. + +%% @doc Get the player token uniquely representing the player. +%% @spec get_token() -> string() +get_token(_Player) -> + ggs_logger:not_implemented(). + +%% @doc Properly terminates the player process. The player token will be lost +%% together with the table token. It should also close the player socket and the +%% process should return in the end. +%% @spec stop(Table::pid()) -> Reason::string() +stop(_Player,_Table) -> + ggs_logger:not_implemented(). + +%% Internals + +loop(#pl_state{token = Token, socket = Socket, table = Table} = State) -> + receive + {tcp, Socket, Data} -> % Just echo for now.. + io:format("Notifying table..~n"), + ggs_table:notify_game(Table, Token, Data), + loop(State); + {notify, From, Message} -> + gen_tcp:send(Socket, Message), + loop(State) + end. diff --git a/src/ggs_protocol.erl b/src/ggs_protocol.erl deleted file mode 100644 index ede5115..0000000 --- a/src/ggs_protocol.erl +++ /dev/null @@ -1,39 +0,0 @@ --module(ggs_protocol). --export([parse/1]). - -parse(Data) -> - Message =string:tokens(Data, "\n"), - % Turn "A: B" pairs into "{A, B}" tuples, for searching. - MsgKV = lists:map((fun(Str) -> - list_to_tuple(string:tokens(Str, ": ")) end - ), Message), - % Hacky way to build a tuple, filter out not_found later on - Processed = { - case lists:keysearch("Command", 1, MsgKV) of - {value,{_, "define"}} -> - define; - {value,{_, "call"}} -> - call; - {value,{_, "hello"}} -> - hello; - false -> - not_found - end, - case lists:keysearch("Token", 1, MsgKV) of - {value,{_, Value}} -> - Value; - false -> - not_found - end, - case lists:keysearch("Content-Length", 1, MsgKV) of - {value,{_, Value}} -> - {Length, _} = string:to_integer(Value), - [_|Cont] = re:split(Data, "\n\n",[{return,list}]), - Content = string:join(Cont, "\n\n"), - Payload = string:substr(Content,1,Length), - Payload; - false -> - not_found - end - }, - gen_server:cast(ggs_server, Processed). diff --git a/src/ggs_server.erl b/src/ggs_server.erl deleted file mode 100644 index aae268e..0000000 --- a/src/ggs_server.erl +++ /dev/null @@ -1,156 +0,0 @@ -%%%---------------------------------------------------- -%%% @author Jonatan Pålsson -%%% @copyright 2010 Jonatan Pålsson -%%% @doc RPC over TCP server -%%% @end -%%%---------------------------------------------------- - --module(ggs_server). --behaviour(gen_server). - -%% API --export([start_link/1, - start_link/0, - stop/0 - ]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - - --define(SERVER, ?MODULE). --define(DEFAULT_PORT, 1055). - --record(state, {port, lsock, client_vm_map = []}). - -%%%==================================================== -%%% API -%%%==================================================== - -%%----------------------------------------------------- -%% @doc Starts the server -%% @end -%%----------------------------------------------------- -start_link(Port) -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [Port], []). - -start_link() -> - start_link(?DEFAULT_PORT). - -%%----------------------------------------------------- -%% @doc Stops the server. -%% @spec stop() -> ok -%% @end -%%----------------------------------------------------- -stop() -> - gen_server:cast(?SERVER, stop). - -%%----------------------------------------------------- -%% gen_server callbacks -%%----------------------------------------------------- - -init([Port]) -> - case gen_server:call(ggs_backup, get_backup) of - {backup_state, not_initialized} -> - {ok, LSock} = gen_tcp:listen(Port, [{active, true}, - {reuseaddr, true}]), - {ok, #state{port = Port, lsock = LSock}, 0}; - {backup_state, State} -> - {ok, LSock} = gen_tcp:listen(Port, [{active, true}, - {reuseaddr, true}]), - {ok, State#state{lsock = LSock}, 0} - end. - -handle_call({backup_state, OldState}, _From, State) -> - io:format("Received old state from backup~n"), - {noreply, OldState}. - - -handle_info({tcp, Socket, RawData}, State) -> - ggs_protocol:parse(RawData), - {noreply, State#state{lsock = Socket}}; - -handle_info({tcp_closed, Socket}, State) -> - gen_tcp:close(Socket), - {stop, "Client closed socket", State}; - -handle_info(timeout, #state{lsock = LSock} = State) -> - {ok, _Sock} = gen_tcp:accept(LSock), - {noreply, State}; - -handle_info(Other, State) -> - erlang:display(Other). - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%----------------------------------------------------- -%% Internal functions -%%----------------------------------------------------- -handle_cast(stop, State) -> - {stop, normal, State}; - -% Handle javascript defines -handle_cast({define, Token, Payload}, State) -> - JSVM = getJSVM(Token, State), - %js_runner:define(JSVM, Payload), - JSVM!{define,self(),Payload}, - send(State#state.lsock, Token, "Okay, defined that for you!"), - {noreply, State}; - -% Handle javascript calls -handle_cast({call, Token, Payload}, State) -> - io:format("test1~n"), - io:format("Got call request: ~p~n", [Payload]), - io:format("test2~n"), - JSVM = getJSVM(Token, State), - JSVM!{get_port, self()}, - receive - {ok, Port} -> erlang:display(erlang:port_info(Port)), - io:format("test1~n") - end, - %erlang:display(erlang:port_info(Port)), - %{ok, Ret} = js_runner:call(JSVM, Payload, []), - JSVM!{call, self(), Payload, []}, - receive - {ok, Ret} -> - send(State#state.lsock, Token, "JS says:", binary_to_list(Ret)), - {noreply, State} - end; -% Set the new state to the reference generated, and JSVM associated -handle_cast({hello, _, _}, State) -> - JSVM = js_runner:boot(), - Client = getRef(), - send(State#state.lsock, Client, "This is your refID"), - OldMap = State#state.client_vm_map, - JSVM!{get_port, self()}, - receive - {ok, Port} -> NewState = State#state{client_vm_map = OldMap ++ [{Client, Port}]}, - gen_server:cast(ggs_backup, {set_backup, NewState}), - {noreply, NewState} - end. -%%----------------------------------------------------- -%% Helpers -%%----------------------------------------------------- -getRef() -> - %{A1,A2,A3} = now(), - %#random:seed(A1, A2, A3), - %random:uniform(1000). - string:strip(os:cmd("uuidgen"), right, $\n ). - -getJSVM(RefID, State) -> - VMs = State#state.client_vm_map, - erlang:display(RefID), - erlang:display(VMs), - {value, {_,VM}} = lists:keysearch(RefID, 1, VMs), - VM. - -send(Socket, RefID, String) -> - gen_tcp:send(Socket, string:join([RefID,String,"\n"], " ")). - -send(Socket, RefID, String1, String2) -> - gen_tcp:send(Socket, string:join([RefID,String1, String2,"\n"], " ")). diff --git a/src/ggs_server_sup.erl b/src/ggs_server_sup.erl deleted file mode 100644 index 23d32f7..0000000 --- a/src/ggs_server_sup.erl +++ /dev/null @@ -1,48 +0,0 @@ --module(ggs_server_sup). --behaviour(supervisor). - -%% API --export([start/1, start_link/1]). - -%% Supervisor callbacks --export([init/1]). --define(SERVER, ?MODULE). - -start(Port) -> - [FirstArg] = Port, - {IntPort, _} = string:to_integer(FirstArg), - start_link(IntPort). - -start_link(Port) -> - supervisor:start_link({local, ?SERVER}, ?MODULE, [Port]). - -init([Port]) -> - GGSServer = {ggs_server, - {ggs_server, start_link, [Port]}, - permanent, - 2000, - worker, - [ggs_server] - }, - Backup = {ggs_backup, - {ggs_backup, start_link, []}, - permanent, - 2000, - worker, - [ggs_backup] - }, - MnesiaServer = {ggs_mnesia_controller_server, - {ggs_mnesia_controller_server, start_link, []}, - permanent, - 2000, - worker, - [ggs_mnesia_controller_server] - }, - Children = [MnesiaServer, Backup, GGSServer], - - RestartStrategy = { one_for_one, % Restart only crashing child - 10, % Allow ten crashes per.. - 1 % 1 second, then crash supervisor. - }, - {ok, {RestartStrategy, Children}}. - diff --git a/src/ggs_sup.erl b/src/ggs_sup.erl index ee6f8cd..05fab65 100644 --- a/src/ggs_sup.erl +++ b/src/ggs_sup.erl @@ -2,29 +2,31 @@ -behaviour(supervisor). %% API --export([start/1, start_link/1]). +-export([start_link/1]). %% Supervisor callbacks -export([init/1]). -define(SERVER, ?MODULE). -start(Port) -> - [FirstArg] = Port, - {IntPort, _} = string:to_integer(FirstArg), - start_link(IntPort). - start_link(Port) -> supervisor:start_link({local, ?SERVER}, ?MODULE, [Port]). init([Port]) -> - Server = {ggs_server_sup, - {ggs_server_sup, start_link, [Port]}, + Dispatcher = {ggs_dispatcher, + {ggs_dispatcher, start_link, [Port]}, permanent, 2000, worker, - [ggs_server_sup] + [ggs_dispatcher] }, - Children = [Server], + Coordinator = {ggs_coordinator, + {ggs_coordinator, start_link, []}, + permanent, + 2000, + worker, + [ggs_coordinator] + }, + Children = [Dispatcher, Coordinator], RestartStrategy = { one_for_one, % Restart only crashing child 10, % Allow ten crashes per.. diff --git a/src/ggs_table.erl b/src/ggs_table.erl new file mode 100644 index 0000000..b2b5c11 --- /dev/null +++ b/src/ggs_table.erl @@ -0,0 +1,199 @@ +%% @doc This module represents a table with players + +-module(ggs_table). +-behaviour(gen_server). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3, notify_all_players/2, notify_game/3, + add_player/2]). + +-record(state, { players, game_vm } ). + +%% API +-export([start_link/0, + add_player/2, + remove_player/2, + stop/1, + notify/3]). + +-include_lib("eunit/include/eunit.hrl"). + +%% ---------------------------------------------------------------------- +% API implementation + +% @doc returns a new table +start_link() -> + {ok, Pid} = gen_server:start_link(?MODULE, [], []), + +%% @private +call(Pid, Msg) -> + gen_server:call(Pid, Msg, infinity). + +% @doc adds a player to a table +add_player(Table, Player) -> + call(Table, {add_player, Player}). + +% @doc removes player form a table +remove_player(Table, Player) -> + call(Table, {remove_player, Player}). + +% @doc stops the table process +stop(Table) -> + gen_server:cast(Table, stop). + +% @doc notifies the table with a message from a player +notify(Table, Player, Message) -> + gen_server:cast(Table, {notify, Player, Message}). + +notify_all_players(Table, Message) -> + gen_server:cast(Table, {notify_all_players, Message}). + +notify_game(Table, From, Message) -> + io:format("Notify game called on"), + erlang:display(Table), + io:format("~n"), + gen_server:cast(Table, {notify_game, Message, From}). + +%% ---------------------------------------------------------------------- + +%% @private +init([]) -> + GameVM = ggs_gamevm_e:start_link(self()), %% @TODO: Temporary erlang gamevm + {ok, #state { + game_vm = GameVM, + players = [] }}. + +%% @private +handle_call({add_player, Player}, _From, #state { players = Players } = State) -> + {reply, ok, State#state { players = [Player | Players] }}; + +handle_call({remove_player, Player}, _From, #state { players = Players } = State) -> + {reply, ok, State#state { players = Players -- [Player] }}; + +handle_call(get_player_list, _From, #state { players = Players } = State) -> + {reply, {ok, Players}, State}; + +handle_call(Msg, _From, State) -> + error_logger:error_report([unknown_msg, Msg]), + {reply, ok, State}. + +%% @private +handle_cast({notify, Player, Message}, #state { game_vm = GameVM } = State) -> + case Message of + {server, define, Args} -> + ggs_gamevm_e:define(GameVM, Args); + {game, Command, Args} -> + ggs_gamevm_e:user_command(GameVM, Player, Command, Args) + end, + {noreply, State}; + +handle_cast({notify_game, Message, From}, #state { game_vm = GameVM } = State) -> + io:format("notify_game message received~n"), + ggs_gamevm_e:user_command(GameVM, From, Message, ""), + {noreply, State}; + +handle_cast({notify_all_players, Message}, #state{players = Players} = State) -> + io:format("Notifying all players... ~p~n", [Players]), + lists:foreach(fun(P) -> + io:format("Notifying ~p~n", [P]), + ggs_player:notify(P, "Server", Message) + end, Players), + {noreply, State}; + +handle_cast(stop, State) -> + {stop, normal, State}; +handle_cast(Msg, S) -> + error_logger:error_report([unknown_msg, Msg]), + {noreply, S}. + +%% @private +handle_info(Msg, S) -> + error_logger:error_report([unknown_msg, Msg]), + {noreply, S}. + +%% @private +terminate(_Reason, _State) -> + ok. + +%% @private +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%% @TODO: Please put these tests in a separate file. We can't compile this file if +%% they contain errors from switching vms +%% ---------------------------------------------------------------------- +% Tests + +%<<<<<<< HEAD +%start_link_test() -> +% Table = start_link(), +% ?assertNot(Table =:= undefined). + +%add_player_test() -> +% Table = start_link(), +% Player = test_player, +% add_player(Table, Player), +% {ok, [Player]} = gen_server:call(Table, get_player_list). + +%remove_player_test() -> +% Table = start_link(), +% Player = test_player, +% Player2 = test_player2, +% add_player(Table, Player), +% {ok, [Player]} = gen_server:call(Table, get_player_list), +% add_player(Table, Player2), +% {ok, [Player2, Player]} = gen_server:call(Table, get_player_list), +% remove_player(Table, Player), +% {ok, [Player2]} = gen_server:call(Table, get_player_list), +% remove_player(Table, Player2), +% {ok, []} = gen_server:call(Table, get_player_list). +% +%stop_test() -> +% Table = start_link(), +% ok = stop(Table). + +% @private +%notify_test() -> +% Table = start_link(), +% Player = test_player, +% Message = {server, define, "function helloWorld(x) { }"}, +% ok = notify(Table, Player, Message). +%======= +%%start_link_test() -> +% Table = start_link("123", none), +% ?assertNot(Table =:= undefined). +% +%add_player_test() -> +% Table = start_link("123", none), +% Player = test_player, +% add_player(Table, Player), +% {ok, [Player]} = gen_server:call(Table, get_player_list). + +%remove_player_test() -> +% Table = start_link("123", none), +% Player = test_player, +% Player2 = test_player2, +% add_player(Table, Player), +% {ok, [Player]} = gen_server:call(Table, get_player_list), +% add_player(Table, Player2), +% {ok, [Player2, Player]} = gen_server:call(Table, get_player_list), +% remove_player(Table, Player), +% {ok, [Player2]} = gen_server:call(Table, get_player_list), +% remove_player(Table, Player2), +% {ok, []} = gen_server:call(Table, get_player_list). +% +%stop_test() -> +% Table = start_link("123", none), +% ok = stop(Table). +% +%% @private +%notify_test() -> +% Table = start_link("123", none), +% Player = test_player, +% Message = {server, define, "function helloWorld(x) { }"}, +% ok = notify(Table, Player, Message). +%>>>>>>> jonte_rewrite + %Message2 = {game, "helloWorld", "test"}, + %ok = notify(Table, Player, Message2). + diff --git a/src/helpers.erl b/src/helpers.erl new file mode 100644 index 0000000..3a42fbf --- /dev/null +++ b/src/helpers.erl @@ -0,0 +1,8 @@ +-module(helpers). +-export([not_implemented/0, get_new_token/0]). + +not_implemented() -> + exit("Not implemented"). + +get_new_token() -> + string:strip(os:cmd("uuidgen"), right, $\n ). diff --git a/src/js_runner.erl b/src/js_runner.erl deleted file mode 100644 index 65511e9..0000000 --- a/src/js_runner.erl +++ /dev/null @@ -1,33 +0,0 @@ --module(js_runner). --export([boot/0]). - -%Mattias -boot() -> - erlang_js:start(), - {ok, Port} = js_driver:new(), - PortPid = spawn(fun() -> port_process(Port) end ), - PortPid. - - -port_process(Port) -> -receive - {get_port, From} -> - From!{ok,Port}, - port_process(Port); - {define, From, Data} -> - ok = js:define(From, list_to_binary(Data)), - From!{ok}, - port_process(Port); - {call, From, Func, Params} -> - {ok,Ret} = js:call(From, list_to_binary(Func), Params), %Port unsure - From!{ok,Ret}, - port_process(Port) -end. - -%These two babies will be ambigiuous -%define(Port, Data) -> -% port_pid!{define,self(),Port,Data}. - - -%call(Port, Func, Params) -> -% port_pid!{call, self(), Port, Func, Params}. diff --git a/start_test b/start_test index 5920151..76050c1 100755 --- a/start_test +++ b/start_test @@ -1,3 +1,3 @@ #!/usr/bin/env bash -erl -boot start_sasl -pa ebin_test -pa erlang_js/ebin/ -pa erlv8/ebin -pa ebin -pa src -eval 'ggs_protocol_test:test_parse().' +erl -boot start_sasl -pa ebin_test -pa erlang_js/ebin/ -pa ebin -pa src -eval 'ggs_coordinator_test:test().' diff --git a/tests/ggs_coordinator_test.erl b/tests/ggs_coordinator_test.erl new file mode 100644 index 0000000..6ec41c6 --- /dev/null +++ b/tests/ggs_coordinator_test.erl @@ -0,0 +1,53 @@ +-module(ggs_coordinator_test). +-include_lib("eunit/include/eunit.hrl"). + +coordinator_test_() -> + {foreach, + fun() -> + {ok, _Coord} = ggs_coordinator:start_link(), + timer:sleep(100) + end, + fun(_X) -> + ggs_coordinator:stop("End of test"), + timer:sleep(100) + end, + [ + fun test_start_link/0, + fun test_stop/0, + fun test_join_bad_table/0, + fun test_join_lobby/0 + ] + }. + +test_start_link() -> + % Check process info + PInfo = whereis(ggs_coordinator), + ?assert((PInfo /= undefined)). % Did the server start? + +test_stop() -> + ok = ggs_coordinator:stop(""), % Extra cleaning + timer:sleep(100), + % Did it stop? + ?assert((whereis(ggs_coordinator)) == undefined). + +test_join_bad_table() -> + Response = ggs_coordinator:join_table("Nonexistant table"), + ?assert(Response == {error, no_such_table}). + +test_join_lobby() -> + {Response, _} = ggs_coordinator:join_lobby(), + ?assert(Response /= error). + +%% 'Manual' tests + +create_table_test() -> + {ok, _Coord} = ggs_coordinator:start_link(), + timer:sleep(100), + % Forcibly create a table. This functionality should be disabled + % in the production system, but is pretty nice for testing. + Response = ggs_coordinator:create_table({force, 1337}), + ?assert(Response == {ok, 1337}). + +join_good_table_test() -> + Response = ggs_coordinator:join_table(1337), + ?assert(Response == {ok, 1337}). diff --git a/tests/ggs_player_test.erl b/tests/ggs_player_test.erl new file mode 100644 index 0000000..be38135 --- /dev/null +++ b/tests/ggs_player_test.erl @@ -0,0 +1,27 @@ +-include_lib("eunit/include/eunit.hrl"). +-import(ggs_player). + +%% @doc start_link should always return ok for any valid socket. A valid socket +%% should always return {ok, Pid} and {error, Reason} otherwise. +start_link_test() -> + ggs_logger:not_implemented(). + +%% @doc Given that start_link returned {ok, Player}. Notify shall always return ok and +%% deliver a specified message through the socket. +notify_test() -> + Player = start_link("bad arg"), + Message = {"something", ""}, + Ret = ggs_player:notify(Player, self(), Message) + ?assertNot(ok =:= Ret). + +%% @doc Given that start_link returned {ok, Player}. get_token shall always return a valid +%% player token. a valid token should be unique. +get_token_test() -> + ggs_logger:not_implemented(). + +%% @doc Given that start_link returned {ok, Pid}. There shouldn't be possible to +%% execute this function with the same Player and Table arguments twice. +stop_test() -> + Player = start_link(something), + Table = test, + ok = stop(Player, Table). diff --git a/tests/ggs_protocol_test.erl b/tests/ggs_protocol_test.erl deleted file mode 100644 index 2230cbf..0000000 --- a/tests/ggs_protocol_test.erl +++ /dev/null @@ -1,6 +0,0 @@ --module(ggs_protocol_test). --export([test_parse/0]). - -test_parse() -> - Ret = ggs_protocol:parse("<> __define JavaScript"), - io:format("~p~n", [Ret]).