RCRPG/Oz
< RCRPG
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]}}