Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- Controls
- -- Upon starting the game, you will be asked to name your adventurer. After this, you descend into the first level of the dungeon -- and begin your adventure.
- -- The following commands are available:
- -- n, s, e, w: Go north, south, east, or west (if possible)
- -- u, d: Go up, down (if possible)
- -- a: Attack
- -- p: Pick up items
- -- x: Wait one turn
- -- q: Quit
- -- You regain one hitpoint for every turn you wait, but your final score is affected by how many turns you took.
- -- nanohack
- --
- -- A tiny RPG/roguelike
- -- TODO enemy following
- -- TODO kill scoring
- -- TODO spawn weapons, make them equippable
- -- TODO add more enemies
- -- roll is the die roll generator. It takes three arguments, in D&D
- -- format: the number of rolls to make, the die to be rolled, and the
- -- roll modifier.
- --
- -- 2d8 -> roll(2, 8)
- -- 1d4+1 -> roll(1, 4, 1)
- function roll(spec)
- rolls, die, mod = table.unpack(spec)
- total = 0
- for x = 1, rolls do
- total = total + math.random(1, die)
- end
- total = total + mod
- return total
- end
- -- -------------------------------------------------- Player
- -- The player has the following attributes:
- --
- -- name Character name
- -- hp Hit points
- -- hpmax Maximum HP
- -- hpinit Initial HP (for scaling score)
- -- xp Experience points
- -- lvl Character level (max: 9)
- -- gp Treasure
- -- weapon Current weapon
- -- amulet Whether the player has the amulet or not
- --
- -- The player initially has 1d8 HP. This goes up by 1d4 every level.
- player = { name = "", xp=0, lvl=1, gp=0, weapon=1, amulet=false }
- function playergen()
- player.hp = roll({1, 8, 0})
- player.hpmax = player.hp
- player.hpinit = player.hp
- end
- -- -------------------------------------------------- Weapons
- -- The weapons table describes the available weapons:
- --
- -- name
- -- dmgdice How much damage the weapon can do
- -- tohit Chance to hit (modified by charlvl / 3)
- -- So the max is 6, to produce a 90% hit chance
- weapons = {
- { name = "Fists", dmgdice = {1, 2, 0}, tohit = 4 },
- { name = "Dagger", dmgdice = {1, 6, 0}, tohit = 4 },
- { name = "Longsword", dmgdice = {1, 8, 0}, tohit = 5 },
- { name = "Broadsword", dmgdice = {2, 4, 2}, tohit = 5 }
- }
- -- -------------------------------------------------- Monsters
- -- The monster_type table defines the various monsters which may be
- -- found in the dungeon. Its fields are:
- --
- -- name The monster's name
- -- hitdice How many hitpoints it starts with
- -- dmgdice How much damage it does
- -- tohit What the monster has to roll to hit the player
- -- alert The chance a monster will notice the player
- -- floor The highest dungeon floor a monster can spawn on
- -- maxgp How many gp a monster may have
- --
- -- Monsters are generated when rooms are entered for the first time.
- monster_type = {
- { name = "Skeleton", hitdice = {1, 4, 0}, dmgdice = {1, 3, 0}, tohit = 3, floor = 1,
- alert = 3, treasure = {1, 3, 0}, move=true},
- { name = "Big Skelly", hitdice = {2, 2, 2}, dmgdice = {1, 4, 0}, tohit = 3, floor = 3,
- alert = 3, treasure = {2, 4, 0}, move=true },
- { name = "Bugbear", hitdice = {2, 4, 1}, dmgdice = {2, 4, 1}, tohit = 4, floor = 5,
- alert = 4, treasure = {2, 5, 1}, move=true }
- }
- -- The monsters table holds the generated monsters. Its fields are:
- --
- -- type The index of the monster_type field corresponding to this monster
- -- hp The monster's current HP
- -- gp How much treasure the monster has
- -- seen Whether the monster has noticed the player
- monsters = {}
- function monsters_attack(floor, room, list)
- for i, m in ipairs(list) do
- if monsters[m].seen == true then
- local name = monster_type[monsters[m].type].name
- if math.random(1,10) <= monster_type[monsters[m].type].tohit then
- local dmg = roll(monster_type[monsters[m].type].dmgdice)
- player.hp = player.hp - dmg
- table.insert(msgs, string.format("The %s attacks and hits for %dHP!", name, dmg))
- else
- table.insert(msgs, string.format("The %s attacks and misses.", name))
- end
- end
- end
- end
- function monsters_opportunity(floor, room)
- thisroom = floors[floor].rooms[room]
- if #thisroom.monsters == 0 then return end
- live = {}
- for i, m in ipairs(thisroom.monsters) do
- if monsters[m].hp > 0 then table.insert(live, m) end
- end
- for i, m in ipairs(live) do
- if monsters[m].seen == false then
- if math.random(1,10) <= monster_type[monsters[m].type].alert then
- monsters[m].seen = true
- table.insert(msgs, string.format("The %s notices you and moves to attack!", monster_type[monsters[m].type].name))
- end
- end
- end
- monsters_attack(floor, room, live)
- end
- -- --------------------------------------------------Dungeon
- -- Floors of the dungeon are a 3x3 grid of rooms.
- --
- -- 1 2 3
- -- 4 5 6
- -- 7 8 9
- --
- -- One room per floor will be randomly selected to have the down
- -- staircase (which automatically sets the up staircase of the next
- -- floor. The same room can't have both an up and down staircase.
- --
- -- Each room has the following attributes:
- --
- -- monsters Which monsters (if any) are in the room
- -- exits List of avialable exits
- floormap = { {e=2, s=4}, {w=1, s=5, e=3}, {w=2, s=6},
- {n=1, e=5, s=7}, {n=2, w=4, e=6, s=8}, {n=3, w=5, s=9},
- {n=4, e=8}, {w=7, n=5, e=9}, {w=8, n=6} }
- dirname = { n = "north", s = "south", e = "east", w = "west" }
- otherdir = { n="s", s="n", e="w", w="e" }
- floors = {}
- maxfloors = 9
- function dungeongen()
- while #floors <= maxfloors do
- -- randomize random
- -- make a new floor table
- thisfloor = { rooms = {} }
- for i = 1,9 do thisfloor.rooms[i] = {monsters = {}, exits = {}, visited = false} end
- -- set location of up staircase
- if #floors == 0 then
- thisfloor.upstairs = math.random(1, 9)
- else
- thisfloor.upstairs = floors[#floors].downstairs
- end
- -- set location of down staircase
- if #floors < maxfloors then
- repeat
- thisfloor.downstairs = math.random(1, 9)
- until thisfloor.downstairs ~= thisfloor.upstairs
- end
- -- build connections between rooms
- dungeon_mapgen(thisfloor)
- -- stow the finished floor
- table.insert(floors, thisfloor)
- end
- end
- function dungeon_mapgen(floor)
- -- initialize list of rooms (true == accessible; false == not)
- local accessiblerooms = { false, false, false, false, false, false, false, false, false, false }
- local closedneighbors = {}
- -- start in room with up staircase
- curroom = floor.upstairs
- accessiblerooms[curroom] = true
- local accessible = 1
- -- pick a random legal room to build an exit to
- while accessible < 9 do
- -- find neighbors of this room without access. if this room has
- -- no such neighbors, pick a random accessible room until one
- -- with inaccessible neighbors is found
- repeat
- closedneighbors = {}
- for k, v in pairs(floormap[curroom]) do
- if accessiblerooms[v] == false then table.insert(closedneighbors, k) end
- end
- if #closedneighbors == 0 then
- repeat
- curroom = math.random(1, #accessiblerooms)
- until accessiblerooms[curroom]
- end
- until #closedneighbors > 0
- -- now that we have a list of rooms to move to, pick one at random
- local exitdir = closedneighbors[math.random(1, #closedneighbors)]
- -- and add it to the current room
- floor.rooms[curroom].exits[exitdir] = true
- -- then lookup the number of the room we've added an exit to,
- -- mark it as accessible, make it the current room, and add a
- -- reciprocal exit
- curroom = floormap[curroom][exitdir]
- accessiblerooms[curroom] = true
- floor.rooms[curroom].exits[otherdir[exitdir]] = true
- -- increment accessible room count
- accessible = accessible + 1
- end
- end
- function populate_room(floor, room)
- -- see if we have monsters
- if math.random(1, 100) <= 30 + (4 * floor - 1) then
- -- ok, how many?
- local num = 1
- local monster_roll = math.random(1, 100)
- if monster_roll >= 97 then
- num = 3
- elseif monster_roll >= 75 then
- num = 2
- end
- -- generate them
- for x = 1, num do
- repeat
- -- make sure we have a floor-appropriate monster
- mid = math.random(1, #monster_type)
- until floor >= monster_type[mid].floor
- monster = {type=mid, seen=false}
- table.insert(monsters, monster)
- -- now gen its stats
- print(string.format("mid = '%d'", mid))
- monster.hp = roll(monster_type[mid].hitdice)
- monster.gp = roll(monster_type[mid].treasure)
- -- and put it in its room
- table.insert(floors[floor].rooms[room].monsters, #monsters)
- end
- end
- -- if we're on floor, 3, 5, or 7, see if a weapon spawns here
- -- if we're on floor 9, see if the amulet is here
- if floor == 9 then
- if math.random(1,10) <= amuletchance then
- floors[floor].rooms[room].amulet = true
- amuletchance = 0
- else
- amuletchance = amuletchance + 1
- end
- end
- -- finally, set the visited flag
- floors[floor].rooms[room].visited = true
- end
- -- -------------------------------------------------- Output utility functions
- function show_screen(floor, room)
- io.write("\027[2J\027[H")
- show_map(floor, room)
- show_status(floor, room)
- describe_room(floor, room)
- describe_combat()
- describe_monsters(floor, room)
- end
- function show_status()
- print()
- io.write("[ ", player.name," | HP ", player.hp)
- if player.hp == player.hpmax then io.write(" (max)") end
- io.write(" | Lvl ", player.lvl, " | ", weapons[player.weapon].name)
- io.write(" | Gold ", player.gp, " | Floor ", curfloor, " | Turn ", turn, " ]\n\n")
- end
- function describe_room(floor, room)
- if room == floors[floor].upstairs then io.write("There is a staircase leading up here.") end
- if room == floors[floor].downstairs then io.write("There is a staircase leading down here.") end
- if floors[floor].rooms[room].amulet == true then
- print("The amulet is here, gleaming in the torchlight!")
- end
- print()
- show_room_exits(floor, room)
- end
- function describe_monsters(floor, room)
- thisroom = floors[floor].rooms[room]
- -- tell the player about any live monsters
- for i, mid in ipairs(thisroom.monsters) do
- if monsters[mid].hp > 0 then
- io.write("There is a ", monster_type[monsters[mid].type].name, " here. ")
- io.write("It has ", monsters[mid].hp, "HP\n")
- else
- io.write("There is a dead ", monster_type[monsters[mid].type].name, " here.\n")
- end
- end
- end
- function describe_combat()
- for i, line in pairs(msgs) do
- print(line)
- end
- print()
- end
- function show_room_exits(floor, room)
- io.write("\nExits: ")
- for k,v in pairs(floors[floor].rooms[room].exits) do
- io.write(k)
- io.write(" ")
- end
- if room == floors[floor].upstairs then io.write("u") end
- if room == floors[floor].downstairs then io.write("d") end
- io.write("\n")
- print()
- end
- function show_map(floor, room)
- map = { {'','','','',''}, {'','','','',''}, {'','','','',''} }
- for r = 1,9 do
- thisroom = floors[floor].rooms[r]
- if r < 4 then i = 1 elseif r < 7 then i = 2 else i = 3 end
- if thisroom.visited then
- -- north wall
- if thisroom.exits["n"] then map[i][1] = map[i][1] .. "-- --"
- else map[i][1] = map[i][1] .. "-----" end
- -- south wall
- if thisroom.exits["s"] then map[i][5] = map[i][5] .. "-- --"
- else map[i][5] = map[i][5] .. "-----" end
- -- west wall
- if thisroom.exits["w"] then map[i][3] = map[i][3] .. " "
- else map[i][3] = map[i][3] .. "|" end
- -- upstairs/downstairs
- if floors[floor].upstairs == r or floors[floor].downstairs == r then
- if floors[floor].upstairs == r then map[i][3] = map[i][3] .. "<"
- else map[i][3] = map[i][3] .. ">" end
- else map[i][3] = map[i][3] .. " " end
- -- player
- if r == room then map[i][3] = map[i][3] .. "@"
- else map[i][3] = map[i][3] .. " " end
- -- monsters
- if #thisroom.monsters > 0 then map[i][3] = map[i][3] .. "m"
- else map[i][3] = map[i][3] .. " " end
- -- east wall
- if thisroom.exits["e"] then map[i][3] = map[i][3] .. " "
- else map[i][3] = map[i][3] .. "|" end
- -- lines 2, 4
- map[i][2] = map[i][2] .. "| |"
- if thisroom.amulet == true then
- map[i][4] = map[i][4] .. "| A |"
- else
- map[i][4] = map[i][4] .. "| |"
- end
- else
- for j = 1, 5 do
- map[i][j] = map[i][j] .. " "
- end
- end
- end
- for i = 1, 3 do
- for j = 1, 5 do
- print(map[i][j])
- end
- end
- end
- function show_score()
- print("\n\n------------------------------------------ FINAL SCORE")
- score = 0
- -- 10 points for each level over 1
- lvlscore = 10 * (player.lvl - 1)
- io.write(lvlscore, " points for reaching level ", player.lvl, "\n")
- score = score + lvlscore
- io.write(player.gp, " points for your hoard of treasure\n")
- score = score + player.gp
- -- The monsters minimum floor# points for each kill
- -- 50 points for finding the amulet
- if player.amulet then
- print("50 points for looting the amulet!")
- score = score + 50
- end
- -- scale score up by initial HP (difficulty)
- scale = 8 / player.hpinit
- if scale > 1 and player.lvl > 1 then
- str = string.format("Your starting HP was %d, giving a difficulty multiplier of %5.2f", player.hpinit, scale)
- print(str)
- score = score * scale
- end
- -- scale score by turns taken
- if turn > 2 and score > 0 then
- scale = math.log(turn)
- str = string.format("Finally, your elapsed turns score divisor is %5.2f", scale)
- print(str)
- score = score / scale
- end
- str = string.format("YOUR SCORE: %7.2f\n", score)
- print(str)
- os.exit(0)
- end
- -- ---------------------------------------------------------- Command routines
- function player_nsew(floor, room, dir)
- if floors[floor].rooms[room].exits[dir] == true then
- monsters_opportunity(floor, room)
- if player.hp < 1 then return end
- table.insert(msgs, string.format("You move to the %s", dirname[dir]))
- curroom = floormap[room][dir]
- if floors[floor].rooms[curroom].visited == false then populate_room(floor, curroom) end
- monsters_opportunity(floor, curroom)
- else
- table.insert(msgs, string.format("There is no exit to the %s from here.", dirname[dir]))
- end
- end
- function player_ud(floor, room, dir)
- if dir == "u" then
- if floors[floor].upstairs == room then
- monsters_opportunity(floor, room)
- if player.hp < 1 then return end
- if floor == 1 then
- print("\nYou climb the stairs and leave the dungeon, alive if not victorious.")
- show_score()
- else
- table.insert(msgs, "You climb the stairs to the previous floor.")
- curfloor = floor - 1
- curroom = floors[curfloor].downstairs
- monsters_opportunity(curfloor, curroom)
- end
- else
- table.insert(msgs, "There is no up staircase here.")
- end
- else
- if floors[floor].downstairs == room then
- monsters_opportunity(floor, room)
- if player.hp < 1 then return end
- table.insert(msgs, "You descend to the next floor.")
- curfloor = floor + 1
- curroom = floors[curfloor].upstairs
- if floors[curfloor].rooms[curroom].visited == false then populate_room(curfloor, curroom) end
- monsters_opportunity(curfloor, curroom)
- else
- table.insert(msgs, "There is no down staircase here.")
- end
- end
- end
- function player_attack(floor, room)
- thisroom = floors[floor].rooms[room]
- if #thisroom.monsters == 0 then
- table.insert(msgs, "There's nothing to attack.")
- return
- end
- live = {}
- local mid = 999
- for i, m in ipairs(thisroom.monsters) do
- if monsters[m].hp > 0 then table.insert(live, m) end
- end
- if #live == 0 then
- table.insert(msgs, "Everything here is dead.")
- return
- elseif #live == 1 then
- mid = live[1]
- else
- for i, m in pairs(live) do
- local name = monster_type[monsters[m].type].name
- if monsters[m].seen == true then
- print(string.format("%d - %s, %dHP (attacking)", i, name, monsters[m].hp))
- else
- print(string.format("%d - %s, %dHP", i, name, monsters[m].hp))
- end
- end
- repeat
- io.write("\nWhich monster to attack? ")
- mid = io.read()
- mid = tonumber(mid)
- until type(mid) == "number" and mid <= #live
- mid = live[mid]
- end
- -- combat is simultaneous, but we let the monsters go "first"
- -- if you hit a monster, it's going to fight you back
- if monsters[mid].seen == false then
- monsters[mid].seen = true
- table.insert(msgs, string.format("The %s fights back!", monster_type[monsters[mid].type].name))
- end
- monsters_opportunity(floor, room)
- -- now carry on with player attacks and resolution
- local dmg = 0
- if player.weapon == 1 then
- for x = 1,2 do
- if weapons[1].tohit <= math.random(1,10) then
- hit = roll(weapons[1].dmgdice)
- table.insert(msgs, string.format("You swing and... HIT, dealing %dHP of damage", hit))
- dmg = dmg + hit
- else
- table.insert(msgs, "You swing and... MISS!")
- end
- end
- else
- if weapons[player.weapon].tohit <= math.random(1,10) then
- dmg = dmg + roll(weapons[player.weapon].dmgdice)
- table.insert(msgs, string.format("You attack with the %s and... HIT, dealing %dHP of damage", weapons[player.weapon].name, hit))
- else
- table.insert(msgs, "You swing and... MISS!")
- end
- end
- if dmg > 0 then
- -- now handle the bookkeeping
- monsters[mid].hp = monsters[mid].hp - dmg
- if monsters[mid].hp <= 0 then
- table.insert(msgs, string.format("The %s is killed.", monster_type[monsters[mid].type].name))
- player.xp = player.xp + 1
- player.gp = player.gp + monsters[mid].gp
- table.insert(msgs, string.format("You get 1XP and %dGP", monsters[mid].gp))
- end
- if player.xp >= player.lvl then
- player.lvl = player.lvl + 1
- player.xp = 0
- table.insert(msgs, "")
- table.insert(msgs, string.format("You have gained a level! You are now level %i", player.lvl))
- if player.hp > 0 then
- local hpup = roll({1, 4, 0})
- player.hpmax = player.hpmax + hpup
- table.insert(msgs, string.format("Your max HP goes up by %d (new max: %d)", hpup, player.hpmax))
- end
- end
- end
- end
- function player_wait(floor, room)
- table.insert(msgs, "You rest for a moment.")
- if player.hp < player.hpmax then
- player.hp = player.hp + 1
- table.insert(msgs, "You regain 1 hitpoint.")
- end
- monsters_opportunity(floor, room)
- end
- function pickup_item(floor, room)
- thisroom = floors[floor].rooms[room]
- if thisroom.amulet == true then
- player.amulet = true
- print("\nYou pick up the gleaming amulet and power surges through you!")
- print("WINNERS DON'T DO DRUGS!!!")
- show_score()
- end
- monsters_opportunity(floor, room)
- end
- function get_player_input()
- cmd = nil
- repeat
- io.write("Command? ")
- cmd = io.read()
- if cmd == "q" then os.exit(0) end
- if cmds[cmd] == nil then
- print("Invalid command")
- else
- return cmd
- end
- until cmds[cmd] ~= nil
- end
- -- -------------------------------------------------------------- Main routine
- -- When a player leaves the room, all monsters in combat (seen ==
- -- true) with the character roll to see if they follow; the chance
- -- that they do is the percentage of HP they have remaining. Monsters
- -- NOT in combat make a notice roll and automatically follow if they
- -- succeed.
- -- dispatch table
- cmds = { n = player_nsew, s = player_nsew, e = player_nsew, w = player_nsew,
- u = player_ud, d = player_ud, x = player_wait, a = player_attack,
- p = pickup_item }
- -- misc vars
- turn = 1
- curfloor = 1
- amuletchance = 2
- msgs = {}
- math.randomseed( tonumber(tostring(os.time()):reverse():sub(1,6)) )
- print("\nNANOHACK VERSION 1.1.0")
- io.write("\nWelcome, brave explorer! What is your name? ")
- player.name = io.read()
- print("\nHail, ".. player.name .. "! May Kodos bless your quest!\n")
- playergen()
- dungeongen()
- print("You open the trapdoor in the back room of the tavern and descend into")
- print(" the darkness below. There's some kind of amulet down there, and")
- print(" you totally win if you find it!")
- io.write("\n\nPress [Enter] to begin...")
- io.read()
- curroom = floors[curfloor].upstairs
- floors[curfloor].rooms[curroom].visited = true
- while true do
- show_screen(curfloor, curroom)
- if player.hp < 1 then
- print("\nYou have been slain!")
- show_score()
- end
- cmd = get_player_input()
- msgs = {}
- turn = turn + 1
- cmds[cmd](curfloor, curroom, cmd)
- end
Add Comment
Please, Sign In to add comment