% ag_server7.erl - (Silver) Adventure Game Server -module(ag_server7). -author('Alan G. Labouseur'). -define(else, true). % -- This is to make the if statements (somewhat) readable. % TODO: Add to inventory by visiting locales. % Increase score by visiting locales. % Show ratio of score to turns at the end of the game. % Move more of the gameplay logic into the server. -type direction() :: north | south | east | west. %-------- % Public %-------- -export([start/0]). start() -> % -- Spawn the server process. io:fwrite("Starting AG server.~n",[]), ServerPid = spawn(fun serverLoop/0), % -- Display the initial location description. io:fwrite("~w", [describe(0)]), io:fwrite("~n~n", []), % -- Kick off the game loop with the ServerPID, location = 0, and turn count = 1. gameLoop(ServerPid, 0, 1, 0, []). %--------- % Private %--------- gameLoop(ServerPid, CurrentLocale, TurnCount, Score, InventoryList) -> % -- Showe the map and get input from the player. io:fwrite(showMap(CurrentLocale)), io:fwrite("~nScore=~w Turn ~w ] ", [Score, TurnCount]), {ok, Input} = io:fread("Enter a command (or help) -] ", "~s"), % Input gets returned as a list from io:fread. [Command | _] = Input, % (Because Input is a list.) % % -- Process the player's input/command into a NewLocale and Description. {NewLocale, Description} = processCommand(CurrentLocale, Command, ServerPid, InventoryList), % % -- Update the display. io:fwrite("~n~s~n~n", [Description]), % % -- Quit or Recurse/Loop. if (NewLocale < 0) -> io:fwrite("Goodbye.~n",[]); ?else -> gameLoop(ServerPid, NewLocale, TurnCount+1, Score, InventoryList) % This is tail recursion, so it's really a jump to the top of gameLoop. end. % if processCommand(CurrentLocale, Command, ServerPid, Inventory) -> case Command of % -- Compass directions - Get the new location from the server. "north" -> move(ServerPid, {CurrentLocale, north}); "n" -> move(ServerPid, {CurrentLocale, north}); "south" -> move(ServerPid, {CurrentLocale, south}); "s" -> move(ServerPid, {CurrentLocale, south}); "east" -> move(ServerPid, {CurrentLocale, east}); "e" -> move(ServerPid, {CurrentLocale, east}); "west" -> move(ServerPid, {CurrentLocale, west}); "w" -> move(ServerPid, {CurrentLocale, west}); % -- Other commands - Handle non-movement commands. "quit" -> {-1, "Thank you for playing."}; "q" -> {-1, "Thank you for playing."}; "look" -> {CurrentLocale, describe(CurrentLocale)}; "l" -> {CurrentLocale, describe(CurrentLocale)}; "help" -> {CurrentLocale, helpText()}; "sing" -> {CurrentLocale, itsPitchDark()}; "map" -> {CurrentLocale, showMap(CurrentLocale)}; "show map" -> {CurrentLocale, showMap(CurrentLocale)}; "inventory" -> {CurrentLocale, showInventory(Inventory)}; "i" -> {CurrentLocale, showInventory(Inventory)}; % -- Otherwise... _Else -> {CurrentLocale, "I do not understand."} % Starting _Else with "_" prevents the "unused" warning. end. helpText() -> io:format("You can enter compass directions: [n] or [north], [s] or [south], [e] or [east], ", []), io:format("[w] or [west], as well as [quit], [look], [help], [map], [inventory], and other commands.", []). % Send the move message (a tuple) to the server. -spec move(pid(), {integer(), direction()}) -> integer(). % This is not enforced at runtime. It's for Dializer and Typer. move(ServerPid, MoveTuple) -> ServerPid ! {self(), MoveTuple}, receive {ServerPid, Response} -> Response % This waits for a response from ToPid. end. % This is the process spawned at the start. serverLoop() -> receive {FromPid, {CurrentLocale, Direction}} -> NewLocaleNumber = mapper(CurrentLocale, Direction), if NewLocaleNumber > -1 -> % Valid move. NewLocaleDesciption = describe(NewLocaleNumber), FromPid ! {self(), {NewLocaleNumber, NewLocaleDesciption}}, serverLoop(); ?else -> % Invalid move. FromPid ! {self(), {CurrentLocale, "You cannot go that way."}}, serverLoop() end; {FromPid, _} -> FromPid ! {self(), "Internal error: You are lost. Nice going, Indiana."}, serverLoop() end. % Mapper. Double-chcek with showMap(). mapper(0, north) -> 1; mapper(0, south) -> 2; mapper(0, west) -> 5; mapper(1, south) -> 0; mapper(1, west) -> 5; mapper(2, north) -> 0; mapper(2, east) -> 4; mapper(3, south) -> 4; mapper(4, north) -> 3; mapper(4, west) -> 2; mapper(5, north) -> 1; mapper(5, east) -> 0; mapper(_, _) -> -1. % Show map. Double-check with mapper(). showMap(CurrentLocale) -> io:format("................... ~n", []), io:format(".. +---- ~s ........ ~n", [dispLocale(CurrentLocale, 1)]), io:format(".. | ... | ........ ~n", []), io:format(".. | ... | ........ ~n", []), io:format(".. ~s --- ~s ... ~s .. ~n", [dispLocale(CurrentLocale, 5), dispLocale(CurrentLocale, 0), dispLocale(CurrentLocale, 3)]), io:format("........ | ... | .. ~n", []), io:format("........ | ... | .. ~n", []), io:format("........ ~s --- ~s .. ~n", [dispLocale(CurrentLocale, 2), dispLocale(CurrentLocale, 4)]), io:format("................... ~n", []). dispLocale(CurrentLocale, MapLoc) -> if CurrentLocale == MapLoc -> "@"; ?else -> integer_to_list(MapLoc) % Remember, strings are lists of ASCII/Unicode values in Erlang. end. % Locations % These location descriptions DO NOT end with ~n newlines. The newline is taken care of in the display code. describe(0) -> io:format("0. Inn of the Last Home~nThis tavern is famous for its wonderful food and ale.", []); describe(1) -> io:format("1. Yellow Brick Road~nGold, emerald, rubies...", []); describe(2) -> io:format("2. Depths of the Earth~nYou find yourself in a vast subterranean network of interconnected caverns and tunnels.", []); describe(3) -> io:format("3. Vault of the Drow~nYou have entered a hemispherical cyst in the crust of the earth, a huge domed vault miles long and nearly as wide.", []); describe(4) -> io:format("4. Mouth of the Yawning Cave~nYou step into the inky darkness, a chorus of glowing eyes following your every move.", []); describe(5) -> io:format("5. West Lake~nSomehow you have stumbled onto a freshwater lake in Hangzhou, China. There are temples, pagodas, and gardens all around, and some pretty good tea too.", []); describe(Loc) -> io:format("Oops! Unknown locale: ~w.", [Loc]). % Other Commands showInventory([]) -> io:format("You are not carrying anything of use.", []); showInventory(InventoryList) -> io:format("You are carrying ~w.", [InventoryList]). itsPitchDark() -> io:format("You are likely to be eaten by a grue. ~n", []), io:format("If this predicament seems particularly cruel, ~n", []), io:format("consider whose fault it could be: ~n", []), io:format("not a torch or a match in your inventory. ~n", []), io:format(" - MC Frontalot~n", []), io:format(" https://www.youtube.com/watch?v=4nigRT2KmCE", []).