# Wizard's Adventure Game in Elixir, Using Factory Function (Rev. 1) # CSci 556: Multiparadigm Programming, Spring 2015 # H. Conrad Cunningham, Professor # Computer and Information Science # University of Mississippi #234567890123456789012345678901234567890123456789012345678901234567890 # 2015-02-27: Developed higher-order factory function version # by refining the original Elixir version # 2015-03-07: Revision 1 encapsulates access to game state # in getter (as well as setter) functions # I adapted the original program from Wizard's Adventure Game in # chapters 5, 6, and 17 of Conrad Barski's _Land of Lisp: Learn to # Program, One Game at a Time_, No Starch Press, 2011. The Land of # Lisp book uses Common Lisp and games to teach Lisp programming. # Barski's Common Lisp program uses macros to generate the game action # functions, representing their common aspects as fixed code and using # parameters to represent the variable aspects. # The original Elixir version just implements the game action # functions directly. # The second Elixir version uses a higher-order factory function, # called set_game_action, to generate the functions. This factory # function takes the variable aspects of the game actions as # first-order and higher-order arguments and returns the game action # function (i.e. closure). The second version also modifies the game # state data structure to store these functions for use during play. # Note: An alternative to the factory function would be to use an # Elixir macro. That is future work. # Barski's Common Lisp program built a textual user interface (a REPL) # for the game using Lisp's ability to evaluate code from input. The # original Elixir version has a hard-coded text interface. The second # version moves toward a more flexible and modular interface, but it # still uses a hard-coded REPL. Further refinement is needed to make # the user interface more modular and flexible. # The program from Land of Lisp uses mutable global variables to store # the game world graph's vertices and edges, the current player # location, the current locations of the movable objects, and various # status variables. # Both the original and second (higher-order factory) versions use # immutable data structures, so they explicitly pass the "game state" # into the the game command functions. These functions return the, # possibly updated, game state. The second version adds the :commands # component to the game state and moves the state variables into a new # :variables component instead of leaving them at the top level. The # revised second version encapsulates all accesses to the game state # within setter and getter functions. # The game state is in the second version is represented by a map with # the following keys (as Elixir atoms): # :world -- a Labeled Digraph representing the game world. # A vertex is a location (e.g., room) in the game world identified # by an Elixir atom. The label on a vertex is the descriptive text # string to be displayed for that location. # The label on an edge consists of a pair {direction, connection} # where direction gives an atom describing the relative # orientation of connected location (:west, :upstairs) and # connection gives the type of connector (:door, :ladder). # # :location -- the vertex in the world where the player is located # :object_locations -- an Elixir map that associates a movable # object (identified by an Elixir atom) with its "current" # location (vertex). The special location :body denotes the # location of items held by the player. # :commands -- an Elixir map associating a user command (an Elixir # atom) with its function. # :variables -- an Elixir map associating a state variable (an # Elixir atom) with its "current" value. # Note: This revised second version uses both setter and getter # functions to access the components of the game state. The second # version used only the setters. # Wizard's Adventure Game Engine defmodule Wizard do import Digraph_List defp describe_location(world, location) do get_vertex(world, location) end defp describe_path( {_,{direction,connection}} ) do # edge "There is a #{connection} going #{direction} from here." end defp describe_paths(world, location) do from_edges_labels(world,location) |> Enum.map_join(" ",&(describe_path(&1))) end defp objects_at(object_locations, location) do Dict.to_list(object_locations) |> Enum.filter_map( &(elem(&1,1) == location), &(elem(&1,0)) ) end defp describe_objects(object_locations, location) do describe_obj = (fn obj -> "You see a #{obj} on the floor." end) objects_at(object_locations,location) |> Enum.map_join(" ", describe_obj) end # General Player Commands # player observes what is at current location def look(game) do world = get_world(game) location = get_location(game) object_locations = get_object_locations(game) [ describe_location(world, location), describe_paths(world, location), describe_objects(object_locations, location) ] |> Enum.join(" ") end # player attempts to move in "direction" from current location def walk(game, direction) do correct_way = fn {_, {dir,_}} -> dir == direction end next = from_edges_labels(get_world(game), get_location(game)) |> Enum.find(nil,correct_way) case next do {newloc, _} -> IO.puts "Walking #{direction}." set_location(game,newloc) nil -> IO.puts "You cannot go that way." game end end # player attempts to pick up "object" def pickup(game, object) do obj_here = objects_at(get_object_locations(game), get_location(game)) |> Enum.member?(object) if obj_here do IO.puts "Picking up #{object}." set_location_object(game,object,:body) else IO.puts "You cannot get that." game end end # what movable objects are the player holding? def inventory(game) do objects_at(get_object_locations(game), :body) end # is the player holding this object? def have?(game, object) do inventory(game) |> Enum.member?(object) end # Special Command Factory # set_game_action creates a special game action function for # "command" and adds it to the list of commands. def set_game_action(the_game, command, subj, obj, place, body) do func = fn (game, subject, object) -> if get_location(game) == place and subject == subj and object == obj and have?(game,subject) do body.(game) else IO.puts "You cannot #{command} like that." game end end set_command(the_game,command,func) end # getters and setters for game state components def set_world(game,new_world) do Dict.put(game,:world,new_world) end def get_world(game) do game.world end def set_location(game,new_location) do Dict.put(game,:location,new_location) end def get_location(game) do game.location end def get_object_locations(game) do game.object_locations end def set_location_object(game,object,new_loc) do put_in(game.object_locations[object], new_loc) end def get_location_object(game,object) do game.object_locations[object] end def set_command(game,command,func) do put_in(game.commands[command], func) end def get_command(game,command) do game.commands[command] end def set_state_var(game,var,val) do put_in(game.variables[var], val) end def get_state_var(game,var) do game.variables[var] end end # Wizard's Adventure Game Initialization defmodule Wizard_Init do import Digraph_List import Wizard # init_game() creates the initial game state def init_game() do # Build game world digraph the_world = new_graph() |> add_vertex( :living_room, ("You are in the living-room. " <> "A wizard is snoring loudly on the couch.")) |> add_vertex( :garden, ("You are in a beautiful garden. " <> "There is a well in front of you.")) |> add_vertex( :attic, ("You are in the attic. " <> "There is a giant welding torch in the corner.")) |> add_edge(:living_room, :garden, {:west,:door}) |> add_edge(:living_room, :attic, {:upstairs,:ladder}) |> add_edge(:garden, :living_room, {:east,:door}) |> add_edge(:attic, :living_room, {:downstairs,:ladder}) # Define functions for special commands (not in Game Engine) help = fn -> """ Commands are: dunk subject object -- attempt to dunk subject in object help -- display this text inventory -- what objects you are holding look -- describe this location pickup object -- add object to inventory quit -- leave program splash subject object -- attempt to splash subject on object walk direction -- move to an adjacent location weld subject object -- attempt to weld subject to object """ end weld_body = fn game -> if have?(game,:bucket) and not (get_state_var(game,:chain_welded)) do IO.puts "The chain is now securely welded to the bucket." set_state_var(game, :chain_welded, true) else IO.puts "You do not have a bucket." game end end dunk_body = fn game -> if get_state_var(game,:chain_welded) do IO.puts "The bucket is now full of water." set_state_var(game, :bucket_filled, true) else IO.puts "The water level is too low to reach." game end end splash_body = fn game -> cond do not get_state_var(game,:bucket_filled) -> IO.puts "The bucket has nothing in it." game have?(game,:frog) -> IO.puts "The wizard awakes and sees that you stole " <> "his frog. He is so upset he banishes you to the " <> "netherworlds. You lose! The end." game |> set_state_var(:end_game, true) |> set_state_var(:win, false) true -> IO.puts "The wizard awakens from his slumber and " <> "greets you warmly. He hands you the magic, " <> "low-carb donut. You win! The end." game |> set_state_var(:end_game, true) |> set_state_var(:win, true) end end # Return initial game state %{ world: nil, location: nil, object_locations: %{}, commands: %{}, variables: %{} } |> set_world(the_world) |> set_location(:living_room) |> set_location_object(:whiskey, :living_room) |> set_location_object(:bucket, :living_room) |> set_location_object(:chain, :garden) |> set_location_object(:frog, :garden ) |> set_state_var(:end_game, false) |> set_command(:help, help) |> set_command(:quit, nil) |> set_command(:inventory, &inventory/1) |> set_command(:look, &look/1) |> set_command(:pickup, &pickup/2) |> set_command(:walk, &walk/2) |> set_state_var(:chain_welded, false) |> set_game_action(:dunk,:bucket,:well,:garden,dunk_body) |> set_state_var(:bucket_filled, false) |> set_game_action(:weld,:chain,:bucket,:attic,weld_body) |> set_state_var(:win, nil) |> set_game_action(:splash,:bucket,:wizard,:living_room, splash_body) end end # Wizard's Adventure Game Commend Line Interface defmodule Wizard_REPL do import Wizard import Wizard_Init def game_repl() do IO.puts "The adventure begins!" game = init_game() IO.puts look(game) cmdloop(game) IO.puts "The adventure ends." end def stop_game?(game) do get_state_var(game,:end_game) end def cmdloop(game) do unless stop_game?(game) do words = IO.gets("> ") |> String.strip |> String.downcase |> String.split(" ", trim: true) |> Enum.map(&(String.to_atom(&1))) case (cmd = List.first(words)) do :help -> help = get_command(game,:help) IO.puts help.() cmdloop(game) :inventory -> inventory = get_command(game,:inventory) case inv = inventory.(game) do [] -> IO.puts "You are holding no objects." _ -> IO.puts ("You are holding the objects: " <> Enum.join(inv, ", ")) end cmdloop(game) :look -> look = get_command(game,:look) IO.puts look.(game) cmdloop(game) :pickup -> case words do [_, object | _] -> pickup = get_command(game,:pickup) game |> pickup.(object) |> cmdloop() _ -> IO.puts "No object was given to pick up." cmdloop(game) end :quit -> game |> set_state_var(:end_game,true) |> cmdloop() :walk -> case words do [_, direction | _] -> walk = get_command(game,:walk) game |> walk.(direction) |> cmdloop() _ -> IO.puts "No direction was given to walk." cmdloop(game) end :weld -> case words do [_, subject, object | _] -> weld = get_command(game,:weld) game |> weld.(game,subject,object) |> cmdloop() _ -> IO.puts "No subject and object were given to weld." cmdloop(game) end :dunk -> case words do [_, subject, object | _] -> dunk = get_command(game,:dunk) game |> dunk.(subject,object) |> cmdloop() _ -> IO.puts "No subject and object were given to dunk." cmdloop(game) end :splash -> case words do [_, subject, object | _] -> splash = get_command(game,:splash) game |> splash.(subject,object) |> cmdloop() _ -> IO.puts "No subject and object were given to splash." cmdloop(game) end nil -> IO.puts "No command entered." cmdloop(game) _ -> IO.puts( "Command '#{cmd}' unknown. Type 'help' for assistance.") cmdloop(game) end end end end