Mar 7 12:35 2015 wizards_game2.ex Page 1 # Wizard's Adventure Game in Elixir, Using Factory Function # CSci 556: Multiparadigm Programming, Spring 2015 # H. Conrad Cunningham, Professor # Computer and Information Science # University of Mississippi #234567890123456789012345678901234567890123456789012345678901234567890 # 2015-02-27: Developed by refining the original Elixir version # 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. # My original Elixir version just implemented the game action # functions directly. # This 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). This 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. This # version takes steps 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 this Elixir program uses immutable data # structures, so they explicitly pass the "game state" into the the # game command functions. These functions return the, possibly # updated, game state. This version adds the commands component and # moves the state variables into a component instead of leaving them # at the higher level. # The game state is in this version is represented by a map with the # following keys (as Elixir atoms): Mar 7 12:35 2015 wizards_game2.ex Page 2 # 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 version still does not encapsulate the data # representation of the game state sufficiently to support convenient # modification of the structure. More attention to this issue is # needed. # 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 Mar 7 12:35 2015 wizards_game2.ex Page 3 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 [ describe_location(game.world, game.location), describe_paths(game.world, game.location), describe_objects(game.object_locations, game.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(game.world, game.location) |> 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(game.object_locations, game.location) |> 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(game.object_locations, :body) end # is the player holding this object? def have?(game, object) do inventory(game) |> Enum.member?(object) end Mar 7 12:35 2015 wizards_game2.ex Page 4 # 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 game.location == 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 # not all used currently 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 Mar 7 12:35 2015 wizards_game2.ex Page 5 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 Mar 7 12:35 2015 wizards_game2.ex Page 6 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 game.variables.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: %{}, Mar 7 12:35 2015 wizards_game2.ex Page 7 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 game.variables.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))) Mar 7 12:35 2015 wizards_game2.ex Page 8 case (cmd = List.first(words)) do :help -> IO.puts game.commands.help.() cmdloop(game) :inventory -> case inv = game.commands.inventory.(game) do [] -> IO.puts "You are holding no objects." _ -> IO.puts ("You are holding the objects: " <> Enum.join(inv, ", ")) end cmdloop(game) :look -> IO.puts game.commands.look.(game) cmdloop(game) :pickup -> case words do [_, object | _] -> game |> game.commands.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 | _] -> game |> game.commands.walk.(direction) |> cmdloop() _ -> IO.puts "No direction was given to walk." cmdloop(game) end :weld -> case words do [_, subject, object | _] -> game |> game.commands.weld.(subject,object) |> cmdloop() _ -> IO.puts "No subject and object were given to weld." cmdloop(game) Mar 7 12:35 2015 wizards_game2.ex Page 9 end :dunk -> case words do [_, subject, object | _] -> game |> game.commands.dunk.(subject,object) |> cmdloop() _ -> IO.puts "No subject and object were given to dunk." cmdloop(game) end :splash -> case words do [_, subject, object | _] -> game |> game.commands.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