RCRPG/Oz

From Rosetta Code
RCRPG/Oz is part of RCRPG. You may find other members of RCRPG at Category:RCRPG.

This Oz version of RCRPG implements a text interface.

It is a purely functional implementation, i.e. the state of the world is passed as an argument to functions. If a function modifies the world, a new version is returned.

It implements the same commands as the other implementations (no "alias", "name" or "help" commands, though).

Code

declare
  %% Basic data structures

  fun {CreateWorld Goal}
     world(goal:Goal
           position:[0 0 0]
           equipped:empty
           inventory:nil
           status:nil
           %% initially we only have one room
           %% coords: X      Y      Z
           rooms:unit(0:unit(0:unit(0:{CreateRoom sledge})))
          )
  end
  
  fun {CreateRoom InitialItem}
     room(items:[InitialItem]
          north:wall
          south:wall
          east:wall
          west:wall
          up:wall
          down:wall)
  end
  
  %% COMMANDS (and shortcuts)
  fun {CreateCommandTable}
     Commands =
     unit(north: {Curry1of2 Go north}  n:Commands.north
          south: {Curry1of2 Go south}  s:Commands.south
          east:  {Curry1of2 Go east}   e:Commands.east
          west:  {Curry1of2 Go west}   w:Commands.west
          up:    {Curry1of2 Go up}     u:Commands.up
          down:  {Curry1of2 Go down}   d:Commands.down
          attack:Attack                a:Commands.attack
          drop:  Drop
          take:  Take
          inventory:Inventory         i:Commands.inventory
          inv:   Commands.inventory
          equip: Equip
         )
  in
     Commands
  end
  CommandTable = {Value.byNeed CreateCommandTable}
  
  %% The game loop.
  proc {Loop World0}
     {PrintStatus World0}
     World = {SetStatus World0 nil}
  in
     if World.position == World.goal then
        {System.showInfo "You are now in the goal room. You have won the game!"}
     else
        {System.printInfo ">"}
        %% Read and parse user input
        Tokens = {Map {String.tokens {ReadLine} & } String.toAtom}
     in
        if Tokens == nil then %% repeat room description when user just presses enter
           {Loop World}
        else
           ComName|Args = Tokens
        in
           if {Not {HasFeature CommandTable ComName}} then
              {Loop {SetStatus World "Unknown command."}}
           else
              ComFunction = CommandTable.ComName
           in
              %% check for number of args (+2: "World" in and out)
              if {Procedure.arity ComFunction} \= 2 + {Length Args} then
                 {Loop {SetStatus World "Wrong number of arguments."}}
              else
                 WorldAfterCommand
              in
                 %% dynamically call ComFunction
                 %% (statically unknown number of arguments)
                 {Procedure.apply ComFunction World|{Append Args [WorldAfterCommand]}}
                 {Loop WorldAfterCommand}
              end
           end
        end
     end
  end

  %% IMPLEMENTATION OF COMMANDS
  %% All commands take the "world" as their first argument.
  %% They might take additional arguments (from the user).
  %% All commands return the new world.

  %% Try to go one step into the specified direction.
  %% Direction: atom like 'north'
  %% (Go is not a real command; it only becomes one after currying.)
  fun {Go Direction World}
     CR = {GetCurrentRoom World}
  in
     if CR.Direction == open then
        if Direction == up andthen {Not {Member ladder CR.items}} then
           {SetStatus World "There is no ladder in this room."}
        else
           {AdjoinAt World position {MovePos World.position Direction}}
        end
     else
        {SetStatus World "There is a wall in that direction."}
     end
  end

  fun {Attack World Direction}
     if {Not {IsDirection Direction}} then
        {SetStatus World "Unknown direction"}
     elseif World.equipped == sledge then
        case {GetCurrentRoom World}.Direction
        of wall then
           {SetStatus {ConnectCurrentRoomTo World Direction}
            "I made a hole in the wall."}
        [] open then
           {SetStatus World "There is already a connection in that direction."}
        end
     else
        {SetStatus World "Can't attack with this item."}
     end
  end

  %% Item: name of item as atom
  fun {Drop World Item}
     if Item == all then {FoldL World.inventory Drop World}
     else
        if {Member Item World.inventory} then
           CR = {GetCurrentRoom World}
           %% add to room
           NewRoom = {AdjoinAt CR items Item|CR.items}
           World2 = {SetRoom World World.position NewRoom}
           %% remove from inventory
           RemainingItems = {Remove World2.inventory Item}
           World3 = {AdjoinAt World3 inventory RemainingItems}
           %% possibly update "equipped"
           World4 = if {Not {Member World3.equipped RemainingItems}} then
                       {AdjoinAt World3 equipped empty}
                    else
                       World3
                    end
        in
           {Inventory World4}
        else
           {SetStatus World "Not carrying such an item."}
        end
     end
  end

  %% Item: item name as an atom
  fun {Take World Item}
     CR = {GetCurrentRoom World}
  in
     if Item == all then {FoldL CR.items Take World}
     else
        if {Member Item CR.items} then
           %% remove from room
           NewRoom = {AdjoinAt CR items {Remove CR.items Item}}
           World2 = {SetRoom World World.position NewRoom}
           %% add to inventory
           World3 = {AdjoinAt World2 inventory Item|World2.inventory}
        in
           {Inventory World3}
        else
           {SetStatus World "There is no such item in this room."}
        end
     end
  end

  fun {Equip World Item}
     if {Member Item World.inventory} then
        {SetStatus {AdjoinAt World equipped Item}
         "Equipped with "#Item}
     else
        {SetStatus World "No such item in inventory."}
     end
  end

  %% Shows the inventory as the status.
  fun {Inventory World}
     {SetStatus World "inventory: "#{ListToString World.inventory}}
  end

  
  %% Operations on basic data structures (the "world" and rooms)

  proc {PrintStatus World}
     if World.status == nil then %% describe room if no status
        [X Y Z] = World.position
     in
        {System.showInfo
         "You are in room ("#X#", "#Y#", "#Z#").\n"#
         "The following items are in this room: "#
         {ListToString {GetCurrentRoom World}.items}#"."}
     else
        {System.showInfo World.status}
     end
  end

  fun {SetStatus World Status}
     {AdjoinAt World status Status}
  end

  fun {NewRoom}
     {CreateRoom {RandomlySelect [sledge gold ladder]}}
  end

  %% Returns the room at the given position.
  %% Might modify the world (if the room does not yet exist) and returns the new world
  %% in 'NewWorld'.
  fun {GetRoom World Position ?NewWorld}
     {CondGet World rooms|Position NewRoom ?NewWorld}
  end
  
  fun {GetCurrentRoom World}
     [X Y Z] = World.position
  in
     World.rooms.X.Y.Z
  end

  %% Replaces a room. Returns the new world.
  fun {SetRoom World Pos NewRoom}
     {Set World rooms|Pos NewRoom}
  end

  local
     %% Maps directions to movement deltas.
     DirPos = unit(east: [~1 0 0] west: [1 0 0]
                   north:[0 ~1 0] south:[0 1 0]
                   up:   [0 0 ~1] down: [0 0 1])
  in
     %% Returns the position that results from going one step
     %% into the specified direction.
     fun {MovePos Pos Dir}
        {List.zip Pos DirPos.Dir Number.'+'}
     end
  end

  local
     OppositeDir = unit(east:west west:east
                        north:south south:north
                        up:down down:up)
  in
     fun {IsDirection X} {HasFeature OppositeDir X} end

     %% Connects the current room to a neighbouring room.
     %% Returns the new world.
     fun {ConnectCurrentRoomTo World Direction}
        World2 = {OpenRoom World World.position Direction}
        OtherPos = {MovePos World2.position Direction}
     in
        {OpenRoom World2 OtherPos OppositeDir.Direction}
     end
  end
  
  fun {OpenRoom World RoomPosition Direction}
     World2
     Room = {GetRoom World RoomPosition ?World2}
  in
     {SetRoom World2 RoomPosition {AdjoinAt Room Direction open}}
  end
  
  
  %% general helpers

  %% Creates a one-argument function from a two-argument function.
  fun {Curry1of2 Fun Arg1}
     fun {$ Arg2}
        Return
     in
        {Procedure.apply Fun [Arg1 Arg2 Return]}
        Return
     end
  end

  %% Removes the first occurance of Y from the list Xs.
  fun {Remove Xs Y}
     case Xs of !Y|Yr then Yr
     [] X|Xr then X|{Remove Xr Y}
     [] nil then nil
     end
  end

  %% Returns a randomly picked element of Xs.
  fun {RandomlySelect Xs}
     Idx = {OS.rand} * {Length Xs} div {OS.randLimits _} + 1
  in
     {Nth Xs Idx}
  end

  %% Reads a line from stdin.
  local
     StdIn = {New class $ from Open.file Open.text end init(name:stdin)}
  in
     fun {ReadLine}
        {StdIn getS($)}
     end
  end

  fun {ListToString Xs}
     {Value.toVirtualString Xs 1000 1000}
  end
  
  
  %% Support for using nested records as multidimensional immutable arrays

  Nothing = {NewName}

  %% Returns a value from an array where Fs is a list of array indices.
  %% If the specified entry does not exist, it is created by calling Otherwise
  %% and the new array is returned in NewArr.
  %% Example: Val = {CondGet a(1:b(3:42)) [1 3] _ _} == 42
  fun {CondGet Arr Fs Otherwise ?NewArr}
     case Fs of F|Fr then
        Arr2 = if Arr == Nothing then unit else Arr end
        NewArrF
        Res = {CondGet {CondSelect Arr2 F Nothing} Fr Otherwise ?NewArrF}
     in
        NewArr = {AdjoinAt Arr2 F NewArrF}
        Res
     [] nil then
        NewArr = if Arr == Nothing then {Otherwise} else Arr end
        NewArr
     end
  end

  %% Sets a (new or existing) entry in Arr to V.
  %% Returns the new array.
  fun {Set Arr Fs V}
     case Fs of F|Fr then
        Arr2 = if Arr == Nothing then unit else Arr end
     in
        {AdjoinAt Arr2 F {Set {CondSelect Arr2 F Nothing} Fr V}}
     [] nil then
        V
     end
  end
in
  %% Start game
  {Loop {CreateWorld [1 1 5]}}