Author Topic: I started work on an RPG yesterday, 6/13. Any comments on my early build?  (Read 225 times)

A quick introduction from me: I'm 43 and fooled around with Basic starting around 1984. It lasted until about 1995 or so, when I switched to C. I first programmed in VIC-20 Basic and eventually moved to an early version of QuickBASIC. (version 3.5, I think?) before eventually moving onto 4 then 4.5.  I never made anything too complex in Basic; just really simple games I could bash out in an hour or so.  I spent maybe 3-4 hours on my biggest game. More complex development happened in C. But I've always wanted to make a game that looked like it was from 1986-1987 or so. Real text heavy, few to no graphics. Lots of calculations going on under the hood.

So this is entirely text based. A few notes: I'm eventually going to use a custom window size, so I've made no attempt whatsoever to format the text. It looks awful and I know it. I've worked on it today and yesterday and it's about 375 lines. You can walk between three rooms, pick up a weapon and attack a skeleton. The attack routine is super buggy and no one can die. I just want to get the most basic systems in at this point, even if they're buggy. It's a fairly boring thing to play right now, what with the unkillable monsters.

I'm open to any suggestions. This is the most complex thing I've ever developed in Basic and I'm a bit overwhelmed right now.

Code: [Select]
' Steve's RPG-like thing.
' Written in 2018.
' Version 0.1-dev (pre-alpha)

' Short term to do: figure out what's wrong with attack:, Add npc inventory, recognize death condition for npc, stop respawning npc every time player enters a room.
' Longer term goals: Get the non-weapon slots working. Show inventory. exchange weapons between slots. drop items.

OPTION _EXPLICIT ' Force variable declaration

RANDOMIZE TIMER
DIM res AS STRING
DIM phrase AS STRING
DIM object AS STRING ' For the parser, it's whatever is after the first word.
DIM answer AS STRING
DIM cmderror AS INTEGER ' This is for parse. cmdeerror set to 1 when a command successfully executes.
DIM doneroom AS INTEGER
DIM croom AS INTEGER ' the next room number to create
DIM exitc AS INTEGER ' Exit code.
DIM player AS playertype
DIM WeaponTable(5) AS weapon ' Weapons in the game.
DIM NPCTable(5) AS npc
DIM pcinv AS inventory
DIM curroom AS room ' Room player is currently in.
DIM curnpc AS playertype ' Room NPC is currently in.
DIM gameworld(5) AS room

doneroom = 0
cmderror = 0

GOSUB roomsetup: ' Create starting room, intialize other rooms.
GOSUB weapongen: ' Create the weapon table!
GOSUB npcgen: ' Create the npc table!
GOSUB playergen: ' Player Generation routine
croom = 1 'starts in room 1.
GOSUB setroom: ' Set initial room properties.

Main: ' Main loop

IF doneroom = 1 THEN GOSUB setroom:

PRINT
PRINT RTRIM$(curroom.shortd)
PRINT RTRIM$(curroom.longd)
IF curroom.npc <> 0 THEN PRINT "There is a "; RTRIM$(NPCTable(curroom.npc).name); " here."
IF curroom.weapon <> 0 THEN PRINT "A "; RTRIM$(WeaponTable(curroom.weapon).name); " is here."


PRINT "Exits:"
IF curroom.north <> 0 THEN PRINT "North"
IF curroom.south <> 0 THEN PRINT "South"
IF curroom.east <> 0 THEN PRINT "East"
IF curroom.west <> 0 THEN PRINT "West"


DO
    INPUT "What do you do? ", res
    GOSUB parse:
    IF cmderror = 0 THEN PRINT "Invalid Command." ' This isn't working but it's to minor of a thing for me to dwell on at this stage.
LOOP WHILE doneroom <> 1

GOTO Main: ' Shut up. I know.

END


parse:
phrase = "null"
object = "null"
IF INSTR(res, " ") THEN
    phrase = LEFT$(res, INSTR(res, " ") - 1)
    object = MID$(res, INSTR(res, " ") + 1)
END IF
IF phrase = "null" THEN ' If phrase wasn't changed from null, that means the command is only one word. Set the word to be processed.
    phrase = res
END IF
phrase = LCASE$(phrase) ' Set it to be lowercase to avoid parsing issues.
IF object <> "null" THEN object = LCASE$(object) ' Only set to lowercase if it isn't null. ie, if an object was part of the command.

' This will be replaced with a SELECT CASE eventually.
IF phrase = "attack" THEN cmderror = 1: GOSUB attack:
IF phrase = "dance" THEN cmderror = 1: GOSUB dance:
IF phrase = "help" THEN cmderror = 1: GOSUB help:
IF phrase = "look" THEN cmderror = 1: GOSUB look:
IF phrase = "get" THEN cmderror = 1: GOSUB pickup:
IF phrase = "xyzzy" THEN cmderror = 1: PRINT "Seriously? I don't think so."
IF phrase = "quit" OR phrase = "exit" THEN cmderror = 1: GOSUB quit:
IF phrase = "north" OR phrase = "n" THEN cmderror = 1: GOSUB roomchange:
IF phrase = "south" OR phrase = "s" THEN cmderror = 1: GOSUB roomchange:
IF phrase = "east" OR phrase = "e" THEN cmderror = 1: GOSUB roomchange:
IF phrase = "west" OR phrase = "w" THEN cmderror = 1: GOSUB roomchange: ' I feel like there must be a better way to do this.

RETURN

attack: ' Attacking isn't balanced and needs extensive work, but the very basic framework of a round is in place.
' npc in room 3 seems to have too many HP. Attack is buggy and weird in general.
DIM pcinit AS INTEGER ' Initiative for player
DIM npcinit AS INTEGER ' NPC's initiative. May change one of those names, they're real easy to confuse or mess up.
DIM turn AS INTEGER ' turn = 1 means player goes next, turn = 0 means npc goes next.
DIM attack AS INTEGER ' Attack role. Used for player & npc
DIM defense AS INTEGER ' Defense role. Also used for player & pc
DIM damage AS INTEGER

IF curroom.npc = 0 THEN
    PRINT "There is nothing here to attack."
    RETURN
END IF
IF curroom.npc <> 0 THEN

    pcinit = INT(RND * 18) + 1
    npcinit = INT(RND * 18) + 1
    PRINT "Entering fight mode."
    PRINT "Your Initiative: "; pcinit; " npc's Initiative: "; npcinit


    IF pcinit > npcinit THEN turn = 1
    IF pcinit < npcinit THEN turn = 0

    FightLoop: ' Main fight loop. It's true turn based right now with no auto attack. npcs currently cannot initiate combat.
    IF turn = 1 THEN ' This is always the players' turn
        attack = rolldice%(1, 20)
        ' There'll eventually me modifiers based on class/race
        defense = rolldice%(1, 20)
        ' Just d20 vs d20 now. Higher number wins.
        IF attack > defense THEN
            damage = rolldice%(WeaponTable(pcinv.weapon1).dice, WeaponTable(pcinv.weapon1).sides)
            curnpc.hp = curnpc.hp - damage
        END IF
        IF attack < defense THEN
            damage = 0
            curnpc.hp = curnpc.hp - damage ' Done this way because there may evemtually be modifiers that add damage even if hit misses.
        END IF
        PRINT RTRIM$(curnpc.name); " took "; damage; " and has "; curnpc.hp; " left."
        IF pcinit > npcinit THEN turn = 0 ' pcinit being higher means player went first. If it's lower, it means player is going second and doesn't need to trigger npc's turn.
    END IF
    IF turn = 0 THEN ' npc's turn
        attack = rolldice%(1, 20)
        ' There'll eventually me modifiers based on class/race
        defense = rolldice%(1, 20)
        ' Just d20 vs d20 now. Higher number wins.
        IF attack > defense THEN
            damage = rolldice%(WeaponTable(pcinv.weapon1).dice, WeaponTable(pcinv.weapon1).sides) ' Uses player's weapon for now.
            player.hp = player.hp - damage ' Player damage
        END IF
        IF attack < defense THEN
            damage = 0
            player.hp = player.hp - damage ' Player damage
        END IF
        PRINT "You've taken "; damage; " to your hitpoints! You now have "; player.hp; " left."
        IF pcinit < npcinit THEN turn = 1: GOTO FightLoop: ' I can't figure out how to do this without using goto.
    END IF
END IF


RETURN

dance:
PRINT "You do that weird pointy dance people in leisure suits did in the 70s. Is this really becoming of an adventurer such as yourself?"
RETURN

help:
PRINT "Game in Development. Move with standard cardinal directions. Do not, under any circmstances, attempt to dance. Quit or Exit to quit."
cmderror = 1
RETURN

look:
PRINT curroom.longd
cmderror = 1
RETURN

quit:
INPUT "Are you sure? (y/N) ", answer
IF LCASE$(answer) = "y" OR LCASE$(answer) = "yes" THEN END
RETURN


TYPE playertype
    name AS STRING * 25
    str AS INTEGER ' Strength.
    dur AS INTEGER ' Durability, aka basis of hitpoints
    hp AS INTEGER ' hit points
END TYPE

TYPE inventory
    weapon1 AS INTEGER ' Corresponds to the WeaponTable entry.
    weapon2 AS INTEGER
    slot1 AS INTEGER ' Eventually there'll be a General Inventory table, not implemented yet.
    slot2 AS INTEGER
    slot3 AS INTEGER
END TYPE

TYPE weapon
    name AS STRING * 20
    sides AS INTEGER ' number of sides of damage dice. d4, d6, etc.
    dice AS INTEGER ' number of dice.
    desc AS STRING * 20 ' not used yet, but maybe some kind of descriptive name, such as "gleaming"
END TYPE

TYPE npc ' This is totally cosmetic. I'll figure out better stuff to put here later.
    name AS STRING * 25 ' Race name. eg, hobgoblin, kobold, dragon, etc.
    str AS INTEGER ' Strength.
    dur AS INTEGER ' Durability, aka basis of hitpoints
    hp AS INTEGER ' hit points
END TYPE

TYPE room
    roomid AS INTEGER ' used for moving
    shortd AS STRING * 30 ' short desc
    longd AS STRING * 180 ' long desc
    npc AS INTEGER ' Right now it supports 1 NPC per room. The number references the NPC Table. Will expand this in the future.
    weapon AS INTEGER ' Game currently supports one weapon and one item in each room.
    item AS INTEGER ' Item table not implemented yet.
    north AS INTEGER ' Leads to this room number.
    south AS INTEGER
    east AS INTEGER
    west AS INTEGER
END TYPE


playergen:
' INPUT "What is your name? ", player.name
player.str = rolldice%(3, 6)
player.dur = rolldice%(3, 6)
player.hp = rolldice%(1, 10) ' Roll 1d10
PRINT "Your stats are -"
PRINT "Strength: "; player.str; " Stamina: "; player.dur; " Hitpoints: "; player.hp
pcinv.weapon1 = 2 ' Starting weapon is a short sword.
PRINT "Press a key to start playing."
DO
    _LIMIT 5 ' Don't need it eating up all the CPU.
LOOP WHILE INKEY$ = ""
CLS
RETURN

weapongen:
' I really want this to be a Constant so it can't be modified accidentally, but that doesn't seem possible.

WeaponTable(1).name = "Long Sword"
WeaponTable(1).sides = 8
WeaponTable(1).dice = 1

WeaponTable(2).name = "Short Sword"
WeaponTable(2).sides = 6
WeaponTable(2).dice = 1

WeaponTable(3).name = "Mace"
WeaponTable(3).sides = 4
WeaponTable(3).dice = 1

WeaponTable(4).name = "Dagger"
WeaponTable(4).sides = 3
WeaponTable(4).dice = 1

WeaponTable(5).name = "Lance"
WeaponTable(5).sides = 6
WeaponTable(5).dice = 2
RETURN

npcgen: ' These numbers are all made up and are changing at some point.
NPCTable(1).name = "Kobold"
NPCTable(1).str = rolldice%(3, 6) ' 3d6 is standard ability roll
NPCTable(1).dur = rolldice%(3, 6) ' 3d6
NPCTable(1).hp = rolldice%(1, 6) ' 1d6 for HP


NPCTable(2).name = "Hobgoblin"
NPCTable(4).str = rolldice%(3, 6) ' 3d6 is standard ability roll
NPCTable(4).dur = rolldice%(3, 6) ' 3d6
NPCTable(4).hp = rolldice%(1, 6) ' 1d6

NPCTable(3).name = "Orc"
NPCTable(4).str = rolldice%(3, 6) ' 3d6 is standard ability roll
NPCTable(4).dur = rolldice%(3, 6) ' 3d6
NPCTable(4).hp = rolldice%(1, 6) ' 1d6

NPCTable(4).name = "Skeleton"
NPCTable(4).str = rolldice%(3, 6) ' 3d6 is standard ability roll
NPCTable(4).dur = rolldice%(3, 6) ' 3d6
NPCTable(4).hp = rolldice%(1, 6) ' 1d6

NPCTable(5).name = "Gold Dragon"
NPCTable(4).str = rolldice%(3, 6) + 2 ' Dragon gets 3d6 + 2
NPCTable(4).dur = rolldice%(3, 6) + 2
NPCTable(4).hp = rolldice%(4, 8) + 5 ' 4d8+5 because dragons be strong.

RETURN

createnpc: ' Sets up NPCs based off NPCTable.

curnpc.name = NPCTable(curroom.npc).name
curnpc.hp = NPCTable(curroom.npc).hp
curnpc.dur = NPCTable(curroom.npc).dur
curnpc.str = NPCTable(curroom.npc).str

RETURN

roomsetup:

gameworld(1).roomid = 1 ' Starting room. You always start in 1.
gameworld(1).shortd = "The start of a dungeon of fear"
gameworld(1).longd = "You see bones strewn about. This is surely the entrance to somewhere pretty horrible."
gameworld(1).south = 2
gameworld(1).west = 3

gameworld(2).roomid = 2
gameworld(2).shortd = "Another room of roominess"
gameworld(2).longd = "It's pretty scary here. You see a boogie woogie skeleton in the corner."
gameworld(2).npc = 4 ' Skeleton
gameworld(2).north = 1

gameworld(3).roomid = 3
gameworld(3).shortd = "The Armory"
gameworld(3).longd = "You stand in an old forgotten armory. Most the weapons look old and unusuable. This place hasn't been properly tended to in decades, if not centuries."
gameworld(3).weapon = 5
gameworld(3).east = 1

RETURN

setroom:
curroom.roomid = gameworld(croom).roomid
curroom.shortd = gameworld(croom).shortd
curroom.longd = gameworld(croom).longd
curroom.npc = gameworld(croom).npc
curroom.weapon = gameworld(croom).weapon
curroom.item = gameworld(croom).item
curroom.north = gameworld(croom).north
curroom.south = gameworld(croom).south
curroom.east = gameworld(croom).east
curroom.west = gameworld(croom).west
IF curroom.npc <> 0 THEN GOSUB createnpc: ' Bring the room's npc, if any, into existence. Drawback is npc respawns every time you enter room. Will fix that later.
doneroom = 0
RETURN

roomchange:
doneroom = 0 ' Make sure it's set to 0, which effectively means you can't exit.
IF LEFT$(phrase, 1) = "n" AND curroom.north <> 0 THEN croom = curroom.north: doneroom = 1 ' Done this way as the parser accepts either "north" or "n" to move.
IF LEFT$(phrase, 1) = "s" AND curroom.south <> 0 THEN croom = curroom.south: doneroom = 1
IF LEFT$(phrase, 1) = "e" AND curroom.east <> 0 THEN croom = curroom.east: doneroom = 1
IF LEFT$(phrase, 1) = "w" AND curroom.west <> 0 THEN croom = curroom.west: doneroom = 1
IF doneroom <> 1 THEN PRINT "You cannot go that way."
RETURN

pickup:
IF curroom.weapon = 0 AND curroom.item = 0 THEN PRINT "There is nothing here to get."

IF curroom.weapon <> 0 THEN
    IF object = RTRIM$(LCASE$(WeaponTable(curroom.weapon).name)) THEN ' Such ugly code. make it all lower case and cut the extra spaces off so it (potentially) matches object.
        IF pcinv.weapon1 AND pcinv.weapon2 <> 0 THEN
            PRINT "You have no room for the "; RTRIM$(WeaponTable(curroom.weapon).name)
            RETURN
        END IF
        IF pcinv.weapon1 = 0 THEN
            pcinv.weapon1 = curroom.weapon
            PRINT "You have placed the "; RTRIM$(WeaponTable(curroom.weapon).name); " in slot 1."
            RETURN
        END IF
        IF pcinv.weapon1 <> 0 AND pcinv.weapon2 = 0 THEN
            pcinv.weapon2 = curroom.weapon
            PRINT "You have placed the "; RTRIM$(WeaponTable(curroom.weapon).name); " in slot 2."
        END IF
    ELSE PRINT "There is no "; object; " here to pick up."
    END IF
END IF
RETURN

FUNCTION rolldice% (dice%, sides%)
    DIM counter AS INTEGER
    DIM total(dice%) AS INTEGER

    FOR counter = 1 TO dice%
        total(counter) = INT(RND * sides%) + 1
    NEXT counter
    ' total should now contain an array of all dice rolls. Now we add them up.

    FOR counter = 1 TO dice%
        rolldice% = rolldice% + total(counter)
    NEXT counter
    ' I feel like there's probably a much better way of doing this.

END FUNCTION

Hi astrosteve!, I've been trying it, you're on the right track! Will it only be text mode? Do you have a story? It would be good images illustrating each room. If you like the idea, I could take some time to draw them (then I will show you what I mean with the images ...)

Offline johnno56

  • Live long and prosper.
Astrosteve,

I have alway been fascinated by 'text based' adventure type games. Just like a book, it conjurers up images that stirs up the imagination.

Many, many years ago, I used to read a magazine called "Proteus". It was purely an entire RPG that included a page for keeping track of your status. I had tried to make a Basic version, but back then, lacked the skill... actually, not much has changed... lol

I would encourage you to continue. Whether you use images or not, the MOST important part of any RPG, is the story. If the storyline is solid, you will hold the interest of any player, or until they either finish or mum tells them to go to bed... lol

Do not be afraid of lengthy descriptions of rooms, objects or characters. Although TOO lengthy may also be a turn off. If you want to stick with text, be daring and non conformist, try a little colour... Room title; Description; Commands; Speech etc... Unless you are a purist the white is just fine... lol

I think I will look for my first issue of Proteus....

Code on.

J
Logic is the beginning of wisdom.

Offline Omerta7486

  • √𝜋²
You will have to build a strong command parser. Those text based games sometimes had libraries of dozens or even hundreds of commands. But, I'm sure you new that! Also, if you have leveling, or any stats, I recommend looking at the stat/combat calculations of your favorite RPGs. Especially, those from the 80s, and 90s. This will give you an idea of how to balance combat, even for a text RPG.

But, nice start! Very nostalgic. I got killed by the skeleton in the second room, because I wondered if I could fight it. -_- I could, but I couldn't win. Then, I danced after I died! XD I shall try it some more.
The knowledge that's seeking the favor of another means the murder of self.

Offline johnno56

  • Live long and prosper.
I liked it... but I had one "bone" of contention... Fighting the skeleton with the lance.... The fight did not go well or for very long... When my status almost bottomed out, I was expecting to die with the next hit, but my 'hitpoints' went negative... I thought, "At last! A game where I did NOT get killed off!". Figured it might be a glitch and to just let you know... Joy short lived... *sigh*

J
Logic is the beginning of wisdom.

Offline Omerta7486

  • √𝜋²
1. How the heck did you pick up the lance? Lol.

2. I beat the skeleton by pure luck. I had got lucky and had 17 strength, 13 stamina, and 10 HP.

3. It is not a bug, the game keeps going. I don't think there is a lose condition, yet.
The knowledge that's seeking the favor of another means the murder of self.

Offline johnno56

  • Live long and prosper.
When the game finally starts.... From the first room: The skeleton is to the south but the lance is to the west.

I too had pure luck! Bad!

I figured hitpoint issue was either a glitch or incomplete... But it was good not dying... lol
Logic is the beginning of wisdom.