RCRPG/Phix
Most of the complexity is contained in the get_command() routine. I went with a bespoke auto-select-by-initial-letter affair rather than forcing the full commands to be (exactly) spelt out very time. You get used to it, and can explore all available command options using the arrow keys.
I tried putting commands in a dictionary but sequences proved much easier. In contrast the rooms were always much better off in a dictionary, not least because indexing {0,0,0}/extendible in all directions could be problematic.
Notable features are the test for cyclic references in aliases (which can be individual words or any-length series of other complete commands), the ability to completely remove all the original/builtin commands/words, and showing a warning when first letters of commands/items/directions clash.
Always having a sledge in the start room was just far too dull for words, and I felt it was my sworn duty to invent some way to get yourself killed!
<lang Phix>-- demo\rosetta\RCRPG.exw constant start_text = """ Welcome to RCRPG! Press ? for help
""" -- (nb: the leading space of help_text[1] is intentional) constant help_text = """ Welcome to RCRPG!
=====
Command input is via single characters, eg n -> north, tg-> take gold. You do however need to hit return to confirm your choice. The left and right arrow keys cycle through permitted options. Use the backspace key to erase any accidentally selected commands. The up and down arror keys cycle through command history.
Available directions are north, south, east, east, west, up, and down. Take or drop the items sledge, ladder, and gold, which appear randomly. Make a tunnel by attacking a direction. You need to equip a sledge before you can attack. You need a ladder to go up, but you cannot take it between floors. The inventory command lists the items you are carrying.
You can alias a single word or an entire series of commands - type a(lias)<return> and follow the prompts (rather than all on one line). More help can be found by entering a(lias)<return> then ?<return>
Make your way to {1,1,5} for the glorious prize of ending this torture! There is also a way to kill yourself instead - can you figure out how?
"""
constant alias_help = """
Example of using the alias command ==================================
Command:alias Enter alias name(? for help):shiney_shiney Alias:gold Command:inventory You are carrying one piece of gold. Command:alias Enter alias name(? for help):gold Delete existing command?(Y/N):Y Alias: Command:inventory You are carrying one piece of shiney_shiney. Command:
You can delete an alias or even an original command by setting it as an alias, confirm the delete, then hit return to abandon adding the replacement. A check is made to prevent the deletion of the last of each kind, since that would make the game completely unplayable.
As shown the item gold is no more, you now use/see shiney_shiney, that is everywhere else except for this help text. Deleting an alias or original also expunges it from the command history.
The above aliased an item; if you alias a complete command, you get reprompted with "Alias:", allowing one alias to daisy-chain several.
You can even alias the alias command itself, eg as "rename", but I have not bothered to test that very thoroughly.
While you can alias "smack bitch" to "take gold", obviously you would instead need "smack" to "take" and "bitch" to "gold" as two separate aliases to allow "smack ladder" and "drop bitch".
Note that commands and aliases are automatically selected by their first letter, and there should be no conflicts between shared first letters of any (aliased) actions and directions/items, eg "dig down" might prevent "drop" from being selected, apart from via left/right arrow keys, eg a 'd' changes "dig" -> "dig down" rather than "drop". A warning is displayed if the use of "alias" breaks this rule (and if the warning stops after a delete, you have solved the conflict).
Aliases can invoke other aliases. Circular references are detected and playback is prohibited until any such issues are resolved (by deleting or overiding the offending aliases).
The currently defined commands and aliases are: """
enum SINGLE_WORD_COMMAND, DIRECTION, NEEDS_DIRECTION, ITEM, NEEDS_ITEM, ALIAS -- (nb: NEEDS_DIRECTION==DIRECTION+1, NEEDS_ITEM==ITEM+1)
enum N,W,U,D,E,S,INVENTORY,ATTACK,TAKE,DROP,MAKEALIAS,EQUIP,SLEDGE,LADDER,GOLD,ALL,QUIT -- (nb: N/W/U/D/E/S (tee hee) must be 1..6, N<->S etc via 7-d) -- (nb: SLEDGE/LADDER/GOLD together in that order) -- (nb: This enum is fixed, whereas eg "north" can be aliased away}
-- order must match enum: constant moves = {{+1, 0, 0}, -- N
{ 0,-1, 0}, -- W { 0, 0,+1}, -- U { 0, 0,-1}, -- D { 0,+1, 0}, -- E {-1, 0, 0}} -- S
-- order not important: sequence cmds = {{"north",DIRECTION,N},
{"west", DIRECTION,W}, {"up", DIRECTION,U}, {"down", DIRECTION,D}, {"east", DIRECTION,E}, {"south",DIRECTION,S}, {"inventory",SINGLE_WORD_COMMAND,INVENTORY},
-- {"dig",NEEDS_DIRECTION,ATTACK}, -- no (prevents drop)
{"attack",NEEDS_DIRECTION,ATTACK}, {"take", NEEDS_ITEM,TAKE}, {"drop", NEEDS_ITEM,DROP}, {"alias",SINGLE_WORD_COMMAND,MAKEALIAS}, {"equip",NEEDS_ITEM,EQUIP}, {"sledge",ITEM,SLEDGE},
-- {"pickaxe",ITEM,SLEDGE},
{"ladder",ITEM,LADDER}, {"gold",ITEM,GOLD}, {"all",ITEM,ALL}, {"quit",SINGLE_WORD_COMMAND,QUIT}, {"n",ALIAS,{{"attack","north"},{"north"}}}}, -- (eg) {commands,kinds,data} = columnize(cmds)
procedure clear_prompt(string prompt, sequence words)
string s = prompt&join(words) puts(1,"\r"&repeat(' ',length(s))&"\r")
end procedure
function next_cmd(integer k, shift)
k += shift if k=0 then k = length(cmds) elsif k>length(cmds) then k = 1 end if return k
end function
function begins_with(integer ch, kind, bool isalias)
if kind=ALIAS then ?9/0 end if -- sanity check sequence items = {} for i=1 to length(commands) do if ((kind=0 and (kinds[i]!=ITEM or isalias)) or kinds[i]=kind) and commands[i][1]=ch then items = append(items,commands[i]) end if end for return items
end function
sequence history = {}
function get_command(string prompt="") -- -- Selects by ititial letter, eg tg -> "take gold" (<return> confirms) -- -- Returns eg {"take","gold"}, or {} to quit (already confirmed) -- -- Could probably be improved by allowing full entry of eg "take gold" -- to ignore entered characters that match auto-fills from the t and g. --
string s = "" sequence words = {}, items integer last, this bool show = false, bExtend, isalias = (prompt="Alias:") integer k, shift, hdx = 0 puts(1,prompt) while 1 do integer ch = lower(wait_key()) if ch='\r' then if isalias then exit end if if length(words) then if find(last,{NEEDS_DIRECTION,NEEDS_ITEM}) then clear_prompt(prompt,words) string what = iff(last=NEEDS_ITEM?"item":"direction") printf(1,"missing %s\n",{what}) show = true else exit end if else clear_prompt(prompt,words) puts(1,"Quit?(Y/N):") ch = upper(wait_key()) puts(1,ch) if ch='Y' then exit end if puts(1,"\r \r") show = true end if elsif ch=#1B or ch='\b' then if ch=#1B then -- escape if length(words)=0 then exit end if -- (else treat as \b) end if if length(words)>0 then clear_prompt(prompt,words) words = words[1..$-1] show = true end if elsif ch='?' then clear_prompt(prompt,words) puts(1,help_text) show = true elsif ch='!' then ?9/0 elsif ch>=' ' and ch<='~' then items = {} if length(words) and find(last,{NEEDS_DIRECTION,NEEDS_ITEM}) then items = begins_with(ch,last-1,false) bExtend = true end if if length(items)=0 then items = begins_with(ch,iff(length(words)?last:0),isalias) if (length(items)=0 and length(words)=1) or items=words then items = begins_with(ch,0,isalias) bExtend = false else bExtend = length(words)=0 end if end if if length(items) then k = iff(length(words)=0?0:find(words[$],items)) if k=0 or k=length(items) then k = 1 else k += 1 end if string key = items[k] if bExtend then if length(words) then puts(1," ") end if words = append(words,key) puts(1,key) else clear_prompt(prompt,words) words[$] = key show = true end if last = kinds[find(key,commands)] if last=ALIAS then last = 0 end if end if elsif ch=331 -- leftarrow or ch=333 then -- rightarrow shift = iff(ch=331?-1:+1) clear_prompt(prompt,words) if length(words) then k = find(words[$],commands) if k=0 then ?9/0 end if else k = iff(ch=331?length(cmds)+1:0) end if if length(words)=2 then while true do k = next_cmd(k,shift) if kinds[k]=last then words[$] = commands[k] exit end if end while else while true do k = next_cmd(k,shift) last = kinds[k] if last=ALIAS then last = 0 end if if last!=ITEM then words = {commands[k]} exit end if end while end if show = true elsif ch=328 -- uparrow or ch=336 then -- downarrow clear_prompt(prompt,words) if length(history)=0 then puts(1,"no history\n") else shift = iff(ch=328?-1:+1) hdx += shift if hdx<=0 then hdx = length(history) elsif hdx>length(history) then hdx = 1 end if words = history[hdx] end if show = true end if if show then show = false s = prompt&join(words) puts(1,s) if length(words) then last = kinds[find(words[$],commands)] if last=ALIAS then last = 0 end if end if end if end while printf(1,"\n") if length(words) then k = find(words,history) if k then history[k..k] = {} end if history = append(history,words) end if return words
end function
bool aliases_banned = false
function check_circular(string name, integer k, sequence seen={}) -- -- eg 1) alias "yy" to something benign, such as inventory. -- 2) alias "zz" to "yy" (quite possibly indirectly) -- 3) re-alias "yy" to "zz", oh dear, infinite loop... -- -- solution: ban the running of aliases until such resolved. --
if kinds[k]!=ALIAS then return false end if sequence kk = data[k] for i=1 to length(kk) do string cmd = kk[i][1] k = find(cmd,commands) if not find(k,seen) then seen &= k if cmd=name or k=0 or check_circular(name,k,seen) then return true end if end if end for return false
end function
procedure check_conflicts() -- -- warn if eg "dig down" is going to conflict with "drop" -- (potentially preventing selection of "drop" via 'd') -- unlike check_circular, this does not ban anything. --
string needs = "", ids = "" sequence needn = {} integer ch, k, ki for i=1 to length(kinds) do ch = commands[i][1] ki = kinds[i] if find(ki,{NEEDS_DIRECTION,NEEDS_ITEM}) then k = find(ch,needs) if k=0 then needs &= ch needn &= 1 else needn[k] += 1 end if elsif find(ki,{DIRECTION,ITEM}) then k = find(ch,ids) if k=0 then ids &= ch end if end if end for for i=1 to length(ids) do ch = ids[i] k = find(ch,needs) if k and needn[k]>1 then printf(1,"warning: command/direction/item conflict over '%c'\n",ch) end if end for
end procedure check_conflicts()
procedure alias() -- -- single word aliases are added to "cmds" as per the originals. -- whole command [set]s are added to "cmds" as type ALIAS. -- -- note this is open to all kinds of abuse I had no time to test for, -- eg you can alias the alias command itself as rename, which is fine, -- but you could also specify rename as attack north/alias/take gold, -- which makes no proper sense, and would re-prompt at every mid-run. --
string name = lower(prompt_string("Enter alias name(? for help):")) if name="?" then puts(1,alias_help) sequence sets = {{"DIRECTION",DIRECTION}, {"INVENTORY",INVENTORY}, {"MAKEALIAS",MAKEALIAS}, {"ATTACK",ATTACK}, {"TAKE",TAKE}, {"DROP",DROP}, {"EQUIP",EQUIP}, {"SLEDGE",SLEDGE}, {"LADDER",LADDER}, {"GOLD",GOLD}, {"ALL",ALL}, {"QUIT",QUIT}} for i=1 to length(sets) do {string kindstr, integer kind} = sets[i] sequence set = {} for k=1 to length(cmds) do if iff(kind=DIRECTION?kinds[k]:data[k])=kind then set = append(set,commands[k]) end if end for printf(1,"%s: %s\n",{kindstr,join(set,",")}) end for for i=1 to length(kinds) do if kinds[i]=ALIAS then sequence di = data[i] for j=1 to length(di) do di[j] = join(di[j]," ") end for printf(1,"%s : %s\n",{commands[i],join(di,", ")}) end if end for printf(1,"\n") elsif name!="" then integer k = find(name,commands) if k then -- note: no check is made as to whether a command is still -- being referenced, just "not the last of its kind". if kinds[k]!=ALIAS then integer k2 = 0 for i=1 to length(kinds) do if i!=k and kinds[i]=kinds[k] and data[i]=data[k] then k2 = i exit end if end for if k2=0 then -- user needs to copy(/alias), then delete... printf(1,"last of kind, cannot delete\n") return end if end if printf(1,"Delete existing command?(Y/N):") integer ch = upper(wait_key()) printf(1,"%c\n",ch) if ch!='Y' then return end if cmds[k..k] = {} commands[k..k] = {} kinds[k..k] = {} data[k..k] = {} for i=length(history) to 1 by -1 do if find(name,history[i]) then history[i..i] = {} end if end for end if sequence set = {} while true do object one = get_command("Alias:") if one={} then exit end if k = find(one[1],commands) integer kk = kinds[k] if length(one)=1 and kk!=ALIAS then -- single word if find(kk,{NEEDS_DIRECTION,NEEDS_ITEM,ITEM}) then if length(set) then puts(1,"invalid - whole commands only, in a set\n") else cmds = append(cmds,{name,kk,data[k]}) commands = append(commands,name) kinds = append(kinds,kk) data = append(data,data[k]) check_conflicts() exit end if else set = append(set,one) end if else -- whole command(s) set = append(set,one) end if end while if length(set) then sequence kind = {name,ALIAS,set} cmds = append(cmds,kind) commands = append(commands,name) kinds = append(kinds,ALIAS) data = append(data,set) end if aliases_banned = false for k=1 to length(cmds) do if check_circular(commands[k],k) then puts(1,"invalid - circular or undefined reference\n") aliases_banned = true exit end if end for end if
end procedure
constant rooms = new_dict() sequence location = {0,0,0}
constant room_descriptions = {"cold dark room",
"very stinky room", "small but comfortable room", "room with an incredible echo! Yahollaydoo!", "grim room resembling a window-less jail", "green room", "black room", "blue room", "brown sad room", "room which breathes pain"}
bool game_over = false,
equipped = false -- (possibly a bit too simple)
integer others = 1 -- ensure a SLEDGE is available somewhere,
-- set to -1 when that's all done & dusted
procedure make_room(sequence location, integer hole=0)
integer desc = rand(length(room_descriptions)) sequence items = {}, -- (nb no quantities, as yet...) exits = repeat(false,S) if hole then exits[hole] = true end if for d=N to S do -- No up until we've placed a sledge, otherwise would -- need to ensure ladder was present on each floor. if others=-1 or d!=U then sequence next_door = sq_add(location,moves[d]) integer node = getd_index(next_door,rooms) if node=NULL then if rand(5)=1 then exits[d] = true if others!=-1 then others += 1 end if end if else sequence {?,?,nexits} = getd_by_index(node,rooms) exits[d] = nexits[7-d] end if end if end for others -= 1 for item=SLEDGE to GOLD do -- (LADDER in the middle) if rand(3)=1 or others=0 -- ensure sledge if last room or (exits[U] and item=LADDER) then -- no death traps items &= item if others=0 then others = -1 end if end if end for sequence room = {desc,items,exits} setd(location,room,rooms)
end procedure if getd_index(location,rooms)=NULL then make_room(location) end if
sequence carryable = {SLEDGE,LADDER,GOLD},
carrying = repeat(0,length(carryable))
procedure inventory()
string totes = "" if sum(carrying)=0 and not game_over then totes = "nothing" else for i=1 to length(carrying) do integer ci = carryable[i], cq = carrying[i] if cq!=0 then if length(totes) then totes &= ", " end if string name = commands[find(ci,data)] if cq=1 then totes &= iff(ci=GOLD?"one piece of ":"a ")&name else totes &= sprintf("%d ",cq)&iff(ci=GOLD?"pieces of "&name:name&"s") end if end if end for if game_over then if length(totes) then totes &= ", and " end if totes &= "a printed A4 certificate of achievement as given to all winners" end if end if printf(1,"You are carrying %s.\n",{totes})
end procedure
procedure show_room()
{integer desc, sequence items, sequence exits} = getd(location,rooms) printf(1, "You are in a %s %v\n",{room_descriptions[desc],location}) if length(items) then for i=1 to length(items) do items[i] = commands[find(items[i],data)] end for if length(items)=1 and items!={"gold"} then items = "a "&items[1] else items = join(items,", ") end if printf(1,"You see %s.\n",{items}) end if if find(true,exits)!=0 then for i=length(exits) to 1 by -1 do if exits[i] then exits[i] = commands[find(i,data)] else exits[i..i] = {} end if end for string isare = iff(length(exits)=1?"is an exit":"are exits") printf(1,"There %s %s.\n",{isare,join(exits,", ")}) end if printf(1,"\n") if location={1,1,5} then game_over = true printf(1,"You win! Game over.\n\n") inventory() end if
end procedure
procedure move(integer direction)
{integer desc, sequence items, sequence exits} = getd(location,rooms) if not exits[direction] then printf(1,"There is no passage that way.\n") elsif direction=U and not find(LADDER,items) then printf(1,"There is no ladder in the room.\n") else location = sq_add(location,moves[direction]) if getd_index(location,rooms)=NULL then make_room(location,7-direction) end if {desc, items, exits} = getd(location,rooms) if exits[D] then sequence below = sq_add(location,moves[D]) integer node = getd_index(below,rooms) if node!=NULL then {desc, items, exits} = getd_by_index(node,rooms) if find(LADDER,items)=0 then printf(1,"You fall down a hole in the floor and die.\n") game_over = true return end if end if end if show_room() end if
end procedure
procedure dig(integer direction)
{integer desc, sequence items, sequence exits} = getd(location,rooms) if not equipped then printf(1,"You claw at the rock with your bare hands until your fingers bleed.\n") elsif rnd()<0.1 then -- (if it is the last one then game over...) printf(1,"Your sledge has broken.\n") equipped = false carrying[find(SLEDGE,carryable)] -= 1 elsif exits[direction] then printf(1,"There is already a passage that way, you make it a little bigger.\n") else sequence new_location = sq_add(location,moves[direction]) if getd_index(new_location,rooms)=NULL then make_room(new_location) end if exits[direction] = true sequence die = {desc,items,exits} -- (save before overwriting) {desc,items,exits} = getd(new_location,rooms) integer hole = 7-direction if exits[hole] then ?9/0 end if if direction=D and not find(LADDER,items) then printf(1,"You dig a tiny hole no bigger than your fist.\n") printf(1,"Seeing no ladder, you decide it is too risky to continue.\n") else exits[hole] = true setd(location,die,rooms) setd(new_location,{desc,items,exits},rooms) string way = commands[find(direction,data)] printf(1,"You dig a passage %s.\n",{way}) end if end if
end procedure
procedure take(integer item)
{integer desc, sequence items, sequence exits} = getd(location,rooms) if item=ALL then for i=1 to length(items) do take(items[i]) end for else string name = commands[find(item,data)] integer k = find(item,items) if k=0 then printf(1,"There isn't any %s here.\n",{name}) else items[k..k] = {} setd(location,{desc,items,exits},rooms) k = find(item,carryable) carrying[k] += 1 printf(1,"You pick up the %s.\n",{name}) end if end if
end procedure
procedure drop(integer item, integer qty=1) -- note: dropping (all) 8 pieces of gold results in one on the floor...
if item=ALL then if sum(carrying)=0 then printf(1,"You aren't carrying anything.\n") else for i=1 to length(carrying) do qty = carrying[i] drop(carryable[i],qty) end for end if elsif qty then string name = commands[find(item,data)] integer k = find(item,carryable) if carrying[k]=0 then printf(1,"You aren't carrying any %s.\n",{name}) else carrying[k] -= qty {integer desc, sequence items, sequence exits} = getd(location,rooms) if not find(item,items) then items &= item setd(location,{desc,items,exits},rooms) end if printf(1,"You drop the %s.\n",{name}) if item=SLEDGE then equipped = false end if end if end if
end procedure
procedure equip(integer item)
string name = commands[find(item,data)] integer k = find(item,carryable) if carrying[k]=0 then printf(1,"You aren't carrying %s %s.\n",{iff(item=GOLD?"any":"a"),name}) elsif item!=SLEDGE then printf(1,"You can't equip %s%s.\n",{iff(item=GOLD?"":"a "),name}) elsif equipped then printf(1,"already equipped.\n") else equipped = true printf(1,"You equip the %s.\n",{name}) end if
end procedure
procedure process_command(sequence c) -- -- tip: replace the inner switches with tables of routine_ids, -- that is should they start to get a little too unwieldy. --
integer k = find(c[1],commands) switch kinds[k] do case ALIAS: -- (the nested variety) if aliases_banned then printf(1,"Sorry, aliases banned until circular references resolved.\n") else for i=1 to length(data[k]) do printf(1,"Running:%s\n",{join(data[k][i])}) process_command(data[k][i]) if game_over then exit end if end for end if case DIRECTION: move(data[k]) case NEEDS_DIRECTION, NEEDS_ITEM: integer dk = data[find(c[2],commands)] switch data[k] do case ATTACK: dig(dk) case TAKE: take(dk) case DROP: drop(dk) case EQUIP: equip(dk) default: ?9/0 end switch case SINGLE_WORD_COMMAND: switch data[k] do case INVENTORY: inventory() case MAKEALIAS: alias() case QUIT: game_over = true default: ?9/0 end switch default: ?9/0 end switch
end procedure
puts(1,start_text) show_room() while not game_over do
sequence c = get_command("Command:") if c={} then exit end if process_command(c)
end while</lang>