DECLARE SUB initExitNames () DECLARE SUB initRooms () DECLARE SUB initObjects () DECLARE SUB mainLoop () DECLARE SUB strFormat (msg AS STRING) DECLARE SUB strStripNoise (msg AS STRING) DECLARE FUNCTION strSanitize$ (inString AS STRING) DECLARE FUNCTION cmdExec% (cmdVerb AS STRING, cmdNoun AS STRING) DECLARE FUNCTION getExitIndex% (msg AS STRING) DECLARE FUNCTION getObjectIndex% (msg AS STRING) DECLARE SUB roomSwitch (nextRoom AS INTEGER) DECLARE SUB printExits () DECLARE SUB printObjects (eRoom AS INTEGER) DECLARE FUNCTION storyDoDrop% (theObject AS INTEGER) DECLARE FUNCTION storyDoGo% (nextRoom AS INTEGER) DECLARE FUNCTION storyDoTake% (theObject AS INTEGER) DECLARE FUNCTION storyDoUse% (theObject AS INTEGER) CONST True = -1 CONST False = 0 CONST cNumExits = 6 CONST cNumRooms = 5 CONST cNumObjects = 4 CONST cNowhere = -1 CONST cInInventory = -2 CONST cExitNorth = 0 CONST cExitSouth = 1 CONST cExitWest = 2 CONST cExitEast = 3 CONST cExitUp = 4 CONST cExitDown = 5 CONST cRoomPorch = 0 CONST cRoomMainHall = 1 CONST cRoomLivingRoom = 2 CONST cRoomDiningRoom = 3 CONST cRoomKitchen = 4 CONST cNothing = -1 CONST cObjKey = 0 CONST cObjDoor = 1 CONST cObjSpoon = 2 CONST cObjBook = 3 CONST cFlagUnmovable = &H1 CONST cFlagOpenClose = &H2 CONST cFlagIsOpen = &H4 CONST cFlagLocked = &H8 DIM SHARED gExitName(cNumExits - 1) AS STRING DIM SHARED gRoomName(cNumRooms - 1) AS STRING DIM SHARED gRoomExit(cNumRooms - 1, cNumExits - 1) AS INTEGER DIM SHARED gObjName(cNumObjects - 1) AS STRING DIM SHARED gObjRoom(cNumObjects - 1) AS INTEGER DIM SHARED gObjFlag(cNumObjects - 1) AS INTEGER DIM SHARED gPlWhere AS INTEGER initExitNames initRooms initObjects mainLoop '' '' EXECUTE COMMAND (VERSION 3) '' This function returns -1 if the player wants to quit. Otherwise, it just '' executes the command we provided. '' FUNCTION cmdExec% (cmdVerb AS STRING, cmdNoun AS STRING) DIM idx AS INTEGER SELECT CASE (cmdVerb) CASE "" PRINT "WHAT NOW?" CASE "QUIT", "EXIT" cmdExec% = -1 EXIT FUNCTION CASE "DANCE" PRINT "YOU DANCE LIKE NO ONE IS WATCHING. AS EXPECTED, IT DIDN'T" PRINT "SOLVE ANYTHING BUT IT MAKES YOU FEEL JUST A TINY LITTLE BIT" PRINT "BETTER ABOUT YOUR ORDEAL." CASE "INVENTORY" printObjects cInInventory CASE "USE" idx = getObjectIndex%(cmdNoun) IF (idx = cNothing) THEN PRINT "I DON'T KNOW WHAT "; cmdNoun; " IS." ELSEIF ((gObjRoom(idx) <> gPlWhere) AND (gObjRoom(idx) <> cInInventory)) THEN PRINT "THERE'S NO "; gObjName(idx); " NEARBY." ELSEIF (storyDoUse%(idx) = False) THEN PRINT "WHAT YOU EXPECTED DID NOT HAPPEN." END IF CASE "OPEN" idx = getObjectIndex%(cmdNoun) IF (idx = cNothing) THEN PRINT "I DON'T KNOW WHAT "; cmdNoun; " IS." ELSEIF ((gObjRoom(idx) <> gPlWhere) AND (gObjRoom(idx) <> cInInventory)) THEN PRINT "THERE'S NO "; gObjName(idx); " NEARBY." ELSEIF ((gObjFlag(idx) AND cFlagOpenClose) = 0) THEN PRINT gObjName(idx); " CAN NOT BE OPENED." ELSEIF (gObjFlag(idx) AND cFlagIsOpen) THEN PRINT gObjName(idx); " IS ALREADY OPEN." ELSEIF (gObjFlag(idx) AND cFlagLocked) THEN PRINT gObjName(idx); " IS LOCKED." ELSE gObjFlag(idx) = gObjFlag(idx) OR cFlagIsOpen PRINT gObjName(idx); " IS OPEN." END IF CASE "CLOSE" idx = getObjectIndex%(cmdNoun) IF (idx = cNothing) THEN PRINT "I DON'T KNOW WHAT "; cmdNoun; " IS." ELSEIF ((gObjRoom(idx) <> gPlWhere) AND (gObjRoom(idx) <> cInInventory)) THEN PRINT "THERE'S NO "; gObjName(idx); " NEARBY." ELSEIF ((gObjFlag(idx) AND cFlagOpenClose) = 0) THEN PRINT gObjName(idx); " CAN NOT BE CLOSED." ELSEIF ((gObjFlag(idx) AND cFlagIsOpen) = 0) THEN PRINT gObjName(idx); " IS ALREADY CLOSED." ELSE gObjFlag(idx) = gObjFlag(idx) AND NOT cFlagIsOpen IF (gObjFlag(idx) AND cFlagLocked) THEN PRINT gObjName(idx); " LOCKED ITSELF AS IT CLOSED." ELSE PRINT gObjName(idx); " IS CLOSED." END IF END IF CASE "DROP" idx = getObjectIndex%(cmdNoun) IF (idx = cNothing) THEN PRINT "I DON'T KNOW WHAT "; cmdNoun; " IS." ELSEIF (gObjRoom(idx) <> cInInventory) THEN PRINT "YOU DON'T HAVE ANY "; gObjName(idx); "." ELSEIF (storyDoDrop%(idx) = False) THEN gObjRoom(idx) = gPlWhere PRINT "YOU DROPPED "; gObjName(idx); "." END IF CASE "TAKE", "GET" idx = getObjectIndex%(cmdNoun) IF (idx = cNothing) THEN PRINT "I DON'T KNOW WHAT "; cmdNoun; " IS." ELSEIF (gObjRoom(idx) <> gPlWhere) THEN PRINT "THERE'S NO "; gObjName(idx); " NEARBY." ELSEIF (gObjFlag(idx) AND cFlagUnmovable) THEN PRINT "YOU CAN'T TAKE "; gObjName(idx); "." ELSEIF (gObjRoom(idx) = cInInventory) THEN PRINT "YOU ALREADY HAVE "; gObjName(idx); "." ELSEIF (storyDoTake%(idx) = False) THEN gObjRoom(idx) = cInInventory PRINT "YOU GOT "; gObjName(idx); "." END IF CASE "GO", "WALK" idx = getExitIndex%(cmdNoun) IF (idx = cNowhere) THEN PRINT "WHERE IS THAT EXACTLY?" ELSEIF (gRoomExit(gPlWhere, idx) = cNowhere) THEN PRINT "YOU CAN'T GO "; cmdNoun; "." ELSEIF (storyDoGo%(gRoomExit(gPlWhere, idx)) = False) THEN roomSwitch gRoomExit(gPlWhere, idx) END IF CASE ELSE PRINT "I DON'T KNOW HOW TO "; CHR$(34); cmdVerb; CHR$(34); "." END SELECT END FUNCTION '' '' GET THE EXIT INDEX, BY NAME '' FUNCTION getExitIndex% (msg AS STRING) DIM e AS INTEGER ' Look for a match in the exit name list. FOR e = 0 TO cNumExits - 1 IF (msg = gExitName(e)) THEN getExitIndex% = e EXIT FUNCTION END IF NEXT e ' Not an exit. getExitIndex% = cNowhere END FUNCTION '' '' GET THE OBJECT INDEX, BY NAME '' FUNCTION getObjectIndex% (msg AS STRING) DIM o AS INTEGER ' Look for a match in the object name list. FOR o = 0 TO cNumObjects - 1 IF (msg = gObjName(o)) THEN getObjectIndex% = o EXIT FUNCTION END IF NEXT o ' Not an object. getObjectIndex% = cNothing END FUNCTION '' '' INITIALIZE EXIT NAMES '' SUB initExitNames gExitName(cExitNorth) = "NORTH" gExitName(cExitSouth) = "SOUTH" gExitName(cExitWest) = "WEST" gExitName(cExitEast) = "EAST" gExitName(cExitUp) = "UP" gExitName(cExitDown) = "DOWN" END SUB '' '' INITIALIZE OBJECTS '' SUB initObjects gObjName(cObjKey) = "KEY" gObjRoom(cObjKey) = cRoomPorch gObjName(cObjDoor) = "FRONT DOOR" gObjRoom(cObjDoor) = cRoomPorch gObjFlag(cObjDoor) = cFlagUnmovable OR cFlagOpenClose OR cFlagLocked gObjName(cObjSpoon) = "SPOON" gObjRoom(cObjSpoon) = cRoomKitchen gObjName(cObjBook) = "BOOK" gObjRoom(cObjBook) = cRoomLivingRoom gObjFlag(cObjBook) = cFlagOpenClose END SUB '' '' INITIALIZE ROOMS '' '' [ KITCHEN ] -------+ '' | | '' [ LIVING ] - [ HALL ] ---- [ DINING ] '' | '' [ PORCH ] '' SUB initRooms DIM r AS INTEGER, e AS INTEGER ' Set all exits to dead ends, for convenience's sake. FOR r = 0 TO cNumRooms - 1 FOR e = 0 TO cNumExits - 1 gRoomExit(r, e) = cNowhere NEXT e NEXT r gRoomName(cRoomPorch) = "THE PORCH" gRoomExit(cRoomPorch, cExitNorth) = cRoomMainHall gRoomName(cRoomMainHall) = "THE MAIN HALL" gRoomExit(cRoomMainHall, cExitNorth) = cRoomKitchen gRoomExit(cRoomMainHall, cExitSouth) = cRoomPorch gRoomExit(cRoomMainHall, cExitWest) = cRoomLivingRoom gRoomExit(cRoomMainHall, cExitEast) = cRoomDiningRoom gRoomName(cRoomLivingRoom) = "THE LIVING ROOM" gRoomExit(cRoomLivingRoom, cExitEast) = cRoomMainHall gRoomName(cRoomDiningRoom) = "THE DINING ROOM" gRoomExit(cRoomDiningRoom, cExitNorth) = cRoomKitchen gRoomExit(cRoomDiningRoom, cExitWest) = cRoomMainHall gRoomName(cRoomKitchen) = "THE KITCHEN" gRoomExit(cRoomKitchen, cExitSouth) = cRoomMainHall gRoomExit(cRoomKitchen, cExitEast) = cRoomDiningRoom END SUB '' '' MAIN LOOP '' Where the magic happens: this is where the player enters his prompts and '' the parser attempts to process them. The loop only ends when the player '' types "QUIT" or "EXIT". '' SUB mainLoop DIM cmdStr AS STRING, cmdVerb AS STRING, cmdNoun AS STRING DIM cmdStrOld AS STRING, cmdNounOld AS STRING DIM usrPrompt AS STRING, ofsThis AS INTEGER, ofsNext AS INTEGER ' Clear screen CLS ' Put the player in the first room. roomSwitch cRoomPorch DO ' Get initial prompt, sanitize right away. LINE INPUT "> "; usrPrompt usrPrompt = strSanitize$(usrPrompt) ' Now let's break the whole prompt into individual commands. Reset the ' position of the "last" period we "found." ofsThis = 0 DO ' The next command should start right after the last period. ofsThis = ofsNext + 1 ' Get the position of the next period. ofsNext = INSTR(ofsThis, usrPrompt, ".") ' If there's no period left, the whole string has been processed. IF (ofsNext = 0) THEN EXIT DO ' Meanwhile, here's a command we found... cmdStr = MID$(usrPrompt, ofsThis, ofsNext - ofsThis) ' Recall the last command. IF (cmdStr = "AGAIN") THEN IF (LEN(cmdStrOld)) THEN cmdStr = cmdStrOld END IF END IF cmdStrOld = cmdStr ' The first word is the verb, the rest is the noun (an object or ' an exit.) ofsTemp = INSTR(cmdStr, " ") IF (ofsTemp = 0) THEN cmdVerb = cmdStr cmdNoun = "" ELSE cmdVerb = LEFT$(cmdStr, ofsTemp - 1) cmdNoun = MID$(cmdStr, ofsTemp + 1) END IF ' Recall the last object. IF (cmdNoun = "IT") THEN IF (LEN(cmdNounOld)) THEN cmdNoun = cmdNounOld END IF END IF cmdNounOld = cmdNoun ' An now, we submit both parts of the command for execution. IF cmdExec%(cmdVerb, cmdNoun) THEN EXIT SUB LOOP LOOP END SUB '' '' PRINT ROOM EXITS '' SUB printExits DIM e AS INTEGER, count AS INTEGER ' Count exits. FOR e = 0 TO cNumExits - 1 count = count - (gRoomExit(gPlWhere, e) <> cNowhere) NEXT e COLOR 7: PRINT "EXITS: "; IF (count = 0) THEN COLOR 15: PRINT "NONE"; ELSE FOR e = 0 TO cNumExits - 1 IF (gRoomExit(gPlWhere, e) <> cNowhere) THEN count = count - 1 COLOR 15: PRINT gRoomName(gRoomExit(gPlWhere, e)); " ("; gExitName(e); ")"; COLOR 7 IF (count) THEN PRINT ", "; END IF NEXT e END IF COLOR 7: PRINT "." END SUB '' '' PRINT ROOM OBJECTS, CAN ALSO PRINT THE INVENTORY '' SUB printObjects (eRoom AS INTEGER) DIM o AS INTEGER, count AS INTEGER ' Count objects. FOR o = 0 TO cNumObjects - 1 count = count - (gObjRoom(o) = eRoom) NEXT o COLOR 7 IF (eRoom = cInInventory) THEN PRINT "YOU HAVE: "; ELSE PRINT "YOU SEE: "; END IF IF (count = 0) THEN COLOR 15: PRINT "NOTHING"; ELSE FOR o = 0 TO cNumObjects - 1 IF (gObjRoom(o) = eRoom) THEN count = count - 1 COLOR 15: PRINT gObjName(o); COLOR 7 IF (gObjFlag(o) AND cFlagOpenClose) THEN IF (gObjFlag(o) AND cFlagIsOpen) THEN PRINT " (OPEN)"; ELSE PRINT " (CLOSED)"; END IF END IF IF (count) THEN PRINT ", "; END IF NEXT o END IF COLOR 7: PRINT "." END SUB '' '' ROOM SWITCH '' This routine is used to move the player to another room. It shows the '' room name, a short description, the exits, and the objects nearby. '' SUB roomSwitch (nextRoom AS INTEGER) DIM e AS INTEGER ' Move the player to the proper room. gPlWhere = nextRoom ' Where is the player? COLOR 7: PRINT "YOU ARE IN: "; COLOR 15: PRINT gRoomName(gPlWhere) COLOR 7: PRINT ' Short description, custom for each room. SELECT CASE (gPlWhere) CASE cRoomPorch PRINT TAB(3); "THE PAINTJOB ON THE STEPS HAS SEEN BETTER DAYS." CASE cRoomMainHall PRINT TAB(3); "THE LIGHT PIERCING THROUGH THE WINDOWS BARELY LITS THE HALL." CASE cRoomLivingRoom PRINT TAB(3); "THERE'S A MUSTY SMELL LINGERING IN THE AIR, THE COUCH IS DUSTY." CASE cRoomDiningRoom PRINT TAB(3); "YOU CAN TELL NOONE HAS EATEN IN THE DINING ROOM IN A LONG TIME." CASE cRoomKitchen PRINT TAB(3); "KITCHEN UTTENSILS ARE SCATTERED ON THE COUNTER." END SELECT ' Where are the exits, what's in the room? PRINT printExits printObjects gPlWhere END SUB '' '' STORY OVERRIDE: DROP '' This function returns True if it resolved the event, or False if the '' default code must resolve it itself. '' FUNCTION storyDoDrop% (theObject AS INTEGER) ' Nothing special, allow any object to be dropped anywhere. storyDoDrop% = False END FUNCTION '' '' STORY OVERRIDE: GO, WALK '' This function returns True if it resolved the event, or False if the '' default code must resolve it itself. '' FUNCTION storyDoGo% (nextRoom AS INTEGER) ' We're unlikely going to handle it ourselve, unless... storyDoGo% = False SELECT CASE (gPlWhere) ' If the player is on the porch... CASE cRoomPorch ' ...and the player wants to go inside the house... IF (nextRoom = cRoomMainHall) THEN ' ...but the door is still closed... IF ((gObjFlag(cObjDoor) AND cFlagIsOpen) = 0) THEN ' Tell the player to open it first! PRINT "THE FRONT DOOR IS CLOSED, OPEN IT FIRST." ' We handled the event. storyDoGo% = True END IF END IF END SELECT END FUNCTION '' '' STORY OVERRIDE: TAKE, GET '' This function returns True if it resolved the event, or False if the '' default code must resolve it itself. '' FUNCTION storyDoTake% (theObject AS INTEGER) ' If the code got this far, the player may take it. Let the default code ' handle the event. storyDoTake% = False END FUNCTION '' '' STORY OVERRIDE: USE '' This function returns True if it resolved the event, or False if the '' default code must resolve it itself. '' FUNCTION storyDoUse% (theObject AS INTEGER) ' We're unlikely going to handle it ourselve, unless... storyDoUse% = False SELECT CASE (gPlWhere) ' If the player is on the porch... CASE cRoomPorch ' ...and is using the key (if we got this far, he can use it.) IF (theObject = cObjKey) THEN ' Unlock the door. gObjFlag(cObjDoor) = gObjFlag(cObjDoor) AND NOT cFlagLocked ' Banish the key to the shadow realm. gObjRoom(theObject) = cNowhere ' Tell the player the door has been unlocked. PRINT "YOU UNLOCKED THE FRONT DOOR." ' We handled the event. storyDoUse% = True END IF END SELECT END FUNCTION '' '' SANITIZE STRING - PART 1: STRICT FORMATTING. '' This function takes , a user prompt, and returns a formalized '' representation of it: it is uppercase, without excess spaces, without '' unusual characters and always ends with a period. '' SUB strFormat (msg AS STRING) DIM readOfs AS INTEGER, writeOfs AS INTEGER DIM prevChar AS INTEGER ' Make the string uppercase and append a period (it's easier to remove ' excess periods than test for the presence of one.) msg = UCASE$(msg) + "." ' Define the previous character read as "space;" this will clip leading ' spaces from the string. We may treat some characters as space even ' though they are not; that's why we're not using LTRIM$() and RTRIM$(). prevChar = &H20 ' Set writing offset. writeOfs = 1 ' Parse the string, one character at a time. We're going to remove ' consecutive spaces, convert some characters to period (which is used to ' separate individual commands in the prompt.) FOR readOfs = 1 TO LEN(msg) ' Take the ASCII value of the character at ; it's faster to ' process INTEGERs than STRINGs. SELECT CASE ASC(MID$(msg, readOfs, 1)) ' We got an uppercase letter (0x41 to 0x5A,) a number (0x30 to 0x39,) ' or an apostrophe (0x27:) copy as is. CASE &H27, &H41 TO &H5A, &H30 TO &H39 MID$(msg, writeOfs, 1) = MID$(msg, readOfs, 1) prevChar = ASC(MID$(msg, writeOfs, 1)) writeOfs = writeOfs + 1 ' Exclamation point (0x21,) comma (0x2C,) period (0x2E,) semi-colon ' (0x3B,) and question mark (0x3F:) convert to period and ignore ' following spaces. If the character before the punctuation mark was a ' space, remove it too! We also pretend the character we wrote is space ' to prevent following spaces and repeated periods. CASE &H21, &H2C, &H2E, &H3B, &H3F IF ((writeOfs > 1) AND (prevChar = &H20)) THEN MID$(msg, writeOfs - 1, 1) = CHR$(&H2E) prevChar = &H20 ELSE MID$(msg, writeOfs, 1) = CHR$(&H2E) prevChar = &H20 writeOfs = writeOfs + 1 END IF ' Everything else ("everything" includes tabs (0x07) and spaces (0x20,) ' obviously:) treat as spaces. Remove duplicates/leading spaces. CASE ELSE ' We are allowed to preserve this "space" (whatever it is) only if ' the previous character was not already a space. IF (prevChar <> &H20) THEN MID$(msg, writeOfs, 1) = CHR$(&H20) prevChar = &H20 writeOfs = writeOfs + 1 END IF END SELECT NEXT readOfs ' We may have some junk left behind the writing offset, clip it. msg = LEFT$(msg, writeOfs - 1) END SUB '' '' SANITIZE USER PROMPT. '' Most of the heavy load is split in two routines for clarity's sake. You '' must invoke only this routine to get a clean user prompt. '' FUNCTION strSanitize$ (inString AS STRING) DIM cpyString AS STRING ' QuickBASIC passes arguments BYREF. If we want to preserve the source ' string (and we do,) we should make a copy we can wreck-- I mean, "work" ' on. cpyString = inString ' Format and strip noise (in that order.) strFormat cpyString strStripNoise cpyString ' Return simplified prompt. strSanitize$ = cpyString END FUNCTION '' '' SANITIZE STRING - PART 2: STRIP NOISE. '' This routine takes (must be properly formatted), and discards all '' "noise" words, including indefinite (A, AN) and definite (THE) articles, '' adverbs such as "AND", "THEN" and "FINALLY", and prepositions "AT" and '' "TO". Some words are replaced by spaces, others by periods. '' SUB strStripNoise (msg AS STRING) DIM headOfs AS INTEGER, headChr AS INTEGER DIM tailChr AS INTEGER, clipChr AS INTEGER DIM readOfs AS INTEGER ' Reset the replacement character (unnecessary since QuickBASIC always ' initializes variables to 0, but I want the emphasis.) The value won't ' remain null if we got a match. clipChr = 0 ' went through the first part of the sanitization so we can safely ' assume the string is at least one byte long and doesn't start with a ' space; if the string starts with a period, it's also the only character ' it contains. So here's the thing: if we assume the 1st character has to ' be the beginning of the first word... headOfs = 1 ' ...then the first separator cannot appear before the 2nd character... readOfs = 2 ' ...thus, the loop won't trigger if the string only contains a period, ' which is exactly what we want! Nice! DO UNTIL (readOfs > LEN(msg)) ' If we found a separator (period or space,) we may check the word. SELECT CASE ASC(MID$(msg, readOfs, 1)) CASE &H20, &H2E ' Only check the whole word if it begins with an A, F or T. SELECT CASE ASC(MID$(msg, headOfs, 1)) CASE &H41, &H46, &H54 ' Compare the word with our "noise" list, some words are replaced ' by spaces, others by periods. SELECT CASE MID$(msg, headOfs, readOfs - headOfs) CASE "AND", "FINALLY", "THEN" clipChr = &H2E CASE "AN", "AT", "A", "THE", "TO" clipChr = &H20 END SELECT ' We got a match! Crush the string to remove the word (including ' the separators on both sides) and write the replacement character ' instead. IF (clipChr) THEN ' Get offset of the leading separator, as well as its type. headOfs = headOfs - 1 IF (headOfs < 1) THEN headOfs = 1 headChr = &H20 ELSE headChr = ASC(MID$(msg, headOfs, 1)) END IF ' Get trailing character type. tailChr = ASC(MID$(msg, readOfs, 1)) ' If the word is located at the very beginning of the string, it ' should normally not be replaced by a space nor a period, unless ' the word is also located at the end of the string (it can ' happen if the whole prompt is made of "noise" words.) IF (headOfs = 1) THEN IF (readOfs = LEN(msg)) THEN msg = "." ELSE msg = RIGHT$(msg, LEN(msg) - readOfs) END IF ' If either separator is a period, the word must be replaced by ' a period. ELSEIF ((headChr = &H2E) OR (tailChr = &H2E)) THEN msg = LEFT$(msg, headOfs - 1) + "." + RIGHT$(msg, LEN(msg) - readOfs) ' For any other case, just use the replacement character we set ' earlier. ELSE msg = LEFT$(msg, headOfs - 1) + CHR$(clipChr) + RIGHT$(msg, LEN(msg) - readOfs) END IF ' Reset the replacement character now, so we don't falsly assume ' the next word as a match. clipChr = 0 ' Reset the reading offset to the beginning of the first ' separator, minus 1 (the offset is incremented by 1 at the end ' of the loop.) readOfs = headOfs - 1 END IF END SELECT ' Next word begins next character. headOfs = readOfs + 1 END SELECT ' Move to next character. readOfs = readOfs + 1 LOOP END SUB