Compare commits

...

2 Commits

Author SHA1 Message Date
Pat Wick c3b2775a80 added a monster spawner and modified lvl mechanics
Monsters have a weighting on how many to spawn and roll which
specific monster to spawn from the pool. I have not added the
ability to advance levels yet, though that should work to
add monsters if the Floor number advances. I did notice a bug
where the player cannot be harmed by the monsters unless the
player moves into them, not the other way around. The monsters
seem to treat the player as a wall that cannot be passed through.
This is confusing because the code for the player and monsters match
as far as their ability to move into each other...
2024-03-12 20:13:02 -04:00
Pat Wick 12d0763a95 added char attributes, enemies, combat
I used the strategy/movement system for NPCs from Beast and added
in some of my own mechanics for melee and projectile combat. A leveling
system has also been established which modifies damage as the player
levels up. Next order of business is level design, monster spawning, and
game progression.
2024-03-09 21:25:51 -05:00
18 changed files with 543 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

49
angband.py Normal file
View File

@ -0,0 +1,49 @@
# angband.py
# ------------
# By Pat Wick
# This game is a redevelopment of the retro game "Angband". Named after
# the stronghold of Morgoth, the Sauron before Sauron in the Lord of the Rings,
# the game is a dungeon-crawler adventure game where the player is tasked with
# delving into Angband to confront Morgoth. Defeating monsters earns the player
# experience points (xp) which allow for more power as the player levels up.
# In this, v0.2, a general movement and combat system exists, but level
# generation, items, and monster spawning won't happen until future versions.
from retro.game import Game
from player import Player
from dungeon import Dungeon
from random import sample
from wall import Wall
from map import (
board_edges,
inner_board,
level_one,
random_empty_position
)
from monster_spawner import MonsterSpawner
print("Welcome to AngBAD (a poor representation of Angband)!\n")
race = input("Choose your race (Human, Elf, Dwarf): ").capitalize()
while race not in ["Human", "Elf", "Dwarf"]:
print("Invalid race. Please choose Human, Elf, or Dwarf.")
race = input("Choose your race (Human, Elf, Dwarf): ").capitalize()
class_ = input("Choose your class (Warrior, Mage, Rogue): ").capitalize()
while class_ not in ["Warrior", "Mage", "Rogue"]:
print("Invalid class. Please choose Warrior, Mage, or Rogue.")
class_ = input("Choose your class (Warrior, Mage, Rogue): ").capitalize()
print(f"\nYou've chosen to play as a {race} {class_}.")
input("Press Enter to continue. Good luck!")
board_size = (50,25)
x,y = board_size
walls = [Wall(position) for position in board_edges(board_size)]
level = [Wall(position) for position in level_one(board_size)]
game = Game(walls + level, {"Race":race, "Class":class_,"CharLevel":1,"Floor":1}, board_size = board_size)
game.add_agent(MonsterSpawner())
game.add_agent(Player((x//2,y//2),race,class_))
game.play()

27
dungeon.py Normal file
View File

@ -0,0 +1,27 @@
# dungeon.py
# ------------
# By Pat Wick
# This module defines a dungeon generation algorithm. I
# still need to figure out what that might actually mean
class Dungeon:
board_size = (10,10)
board_width = 10
board_height = 10
position = (0,0)
dungeon_map = [["."] * board_width] * board_height
for row in range(board_height):
for col in range(board_width):
if row == 0 or row == board_height-1:
dungeon_map[row][col] = "#"
else:
if col == 0 or col == board_width-1:
dungeon_map[row][col] = "#"
else:
dungeon_map[row][col] = "."
def __init__(self, position):
self.position = position
self.name = "dungeon"

85
enemies.py Normal file
View File

@ -0,0 +1,85 @@
from strategy import (
random_move,
move_toward_player,
)
class Orc:
"""Scary.
"""
character = "O"
maxHp = 20
hp = maxHp
deadly = True
speed = 25
def __init__(self,position):
self.position = position
def play_turn(self, game):
if game.turn_number % self.speed == 0:
move = move_toward_player(self.position, game)
if move:
x, y = self.position
dx, dy = move
self.position = (x + dx, y + dy)
if self.position == game.get_agent_by_name("player").position:
game.state['message'] = "Yum."
game.end()
if self.hp <= 0:
game.remove_agent(self)
game.get_agent_by_name("player").xp += self.maxHp
class Rat:
"""Not so scary.
"""
character = "R"
maxHp = 2
hp = maxHp
deadly = True
speed = 15
def __init__(self, position):
self.position = position
def play_turn(self, game):
if game.turn_number % self.speed == 0:
move = random_move(self.position, game)
if move:
x, y = self.position
dx, dy = move
self.position = (x + dx, y + dy)
if self.position == game.get_agent_by_name("player").position:
game.state['message'] = "Eep."
game.end()
if self.hp <= 0:
game.remove_agent(self)
game.get_agent_by_name("player").xp += self.maxHp
class Spider:
"""Creepy-crawly.
"""
character = "S"
maxHp = 5
hp = maxHp
deadly = True
speed = 5
def __init__(self,position):
self.position = position
def play_turn(self, game):
if game.turn_number % self.speed == 0:
move = random_move(self.position, game)
if move:
x, y = self.position
dx, dy = move
self.position = (x + dx, y + dy)
if self.position == game.get_agent_by_name("player").position:
game.state['message'] = "Hsssss."
game.end()
if self.hp <= 0:
game.remove_agent(self)
game.get_agent_by_name("player").xp += self.maxHp

70
map.py Normal file
View File

@ -0,0 +1,70 @@
from retro.game import Game
from random import sample
from player import Player
from wall import Wall
from random import randint
def board_edges(board_size):
"""The outline of the generated board. Used in angband to surround
the level with immovable objects to keep the enemies and player inside
"""
x,y = board_size
positions = []
top = [(i,0) for i in range(x)]
bottom = [(i,y-1) for i in range(x)]
left = [(0,j) for j in range(1,y-1)]
right = [(x-1,j) for j in range(1,y-1)]
return top + bottom + left + right
def inner_board(board_size):
x,y = board_size
positions = []
for i in range(1,x-1):
for j in range(1,y-1):
positions.append((i,j))
return positions
def random_empty_position(game):
"""Returns a random empty position.
"""
agents_by_position = game.get_agents_by_position()
while True:
x, y = game.board_size
i = randint(1, x-2)
j = randint(1, y-2)
if not agents_by_position[(i,j)]:
return (i,j)
def level_one(board_size):
x,y = board_size
positions = []
for i in range(1,x-1):
for j in range(1,y//4):
if i <= x // 4 or i >= x - (x // 4):
positions.append((i,j))
for i in range(1,x//4):
for j in range((y - (y // 4)), y-1):
positions.append((i,j))
# Introduce randomness within predefined pattern
for _ in range(10): # Example: Add 10 random obstacles
rand_i = randint(1, x - 2)
rand_j = randint(1, y - 2)
positions.append((rand_i, rand_j))
# for i in range(1,x-1):
# for j in range(1,y-1):
# if i >=4 and i <= 7 or i >= 13 and i <= 16:
# if j >= 4 and j <= 7 or j >= 13 and j <= 16:
# positions.append((i,j))
return positions
def level_two(board_size):
x,y = board_size
positions = []
for i in range(1,x-1):
for j in range(1,y-1):
if i >=4 and i <= 7 or i >= 13 and i <= 16:
if j >= 4 and j <= 7 or j >= 13 and j <= 16:
positions.append((i,j))
return positions

46
monster_spawner.py Normal file
View File

@ -0,0 +1,46 @@
# asteroid_spawner.py
# -------------------
# By MWC Contributors
# This module defines an AsteroidSpawner agent class.
from random import (
randint,
choices,
)
from enemies import *
from map import random_empty_position
class MonsterSpawner:
display = False
floor = 0
def __init__(self):
pass
def play_turn(self, game):
"""Places each of the monsters on the board for that level.
"""
toSpawn = self.should_spawn_monsters(game.state["Floor"])
for i in range(toSpawn):
monster = self.choose_monster()(random_empty_position(game))
game.add_agent(monster)
def should_spawn_monsters(self, floor_number):
"""Returns the number of monsters to spawn, given the player
advanced a floor.
"""
numMonsters = 0
if floor_number != self.floor:
numMonsters = randint(1, floor_number // 10 + 3)
self.floor = floor_number
return numMonsters
def choose_monster(self):
"""Picks a random monster out of a weighted list of monsters.
"""
monsters = [Orc, Rat, Spider]
monster = choices(monsters, weights = (10, 30, 60))
monster = monster[0]
return monster

120
player.py Normal file
View File

@ -0,0 +1,120 @@
# player.py
# ------------
# By Pat Wick
# This module defines a player agent class. This is intended
# to be used in an implementation of an adventure game but could
# generally be adapted for other player character uses.
from retro.agent import ArrowKeyAgent
from retro.game import Game
from projectile import Projectile
class Player:
name = "player"
level = 1
xp = 0
direction = (1,0)
class_ = ""
speed = 0
damage = 0
def __init__(self, position, race, class_):
"""Class and race will determine player stats and abilities.
"""
self.position = position
self.race = race
self.class_ = class_
if class_.capitalize() == "Warrior":
self.color = "red"
self.class_ == class_
elif class_.capitalize() == "Rogue":
self.color = "green"
self.class_ == class_
else:
self.color = "blue"
self.class_ == class_
if race.capitalize() == "Human":
self.character = "H"
self.speed = 3
self.damage = int(2 + (self.level / 3))
elif race.capitalize() == "Elf":
self.character = "E"
self.speed = 1
self.damage = int(1 + (self.level / 4))
else:
self.character = "D"
self.speed = 6
self.damage = int(3 + (self.level / 2))
def attack(self,game):
"""Warrior is a melee character, mage and rogue use ranged attacks.
"""
# if self.class_ == "Warrior":
# if game.turn_number % self.speed == 0:
# agent = self.get_agent_in_position((self.position[0] + self.direction[0],self.position[1] + self.direction[1]),game)
# if agent:
# if agent.deadly:
# agent.hp -= game.get_agent_by_name("player").damage * 2
# else:
projectile = Projectile((self.position[0] + self.direction[0],self.position[1] + self.direction[1]), self.direction, self.speed, game)
game.add_agent(projectile)
#print("pew pew pew")
def handle_keystroke(self, keystroke, game):
x, y = self.position
if keystroke.name in ("KEY_LEFT", "KEY_RIGHT"):
if keystroke.name == "KEY_LEFT":
new_position = (x - 1, y)
self.direction = (-1,0)
else:
new_position = (x + 1, y)
self.direction = (1,0)
if game.on_board(new_position):
self.try_to_move(new_position,game)
if game.is_empty(new_position):
self.position = new_position
if keystroke.name in ("KEY_DOWN", "KEY_UP"):
if keystroke.name == "KEY_DOWN":
new_position = (x, y + 1)
self.direction = (0,1)
else:
new_position = (x, y - 1)
self.direction = (0,-1)
if game.on_board(new_position):
self.try_to_move(new_position,game)
if game.is_empty(new_position):
self.position = new_position
if keystroke == " ":
self.attack(game)
def try_to_move(self, position, game):
"""Check if player moved into an enemy and loses.
"""
agent = self.get_agent_in_position(position,game)
if agent:
if agent.deadly:
game.state['message'] = "Monsters can be deadly..."
game.end()
def get_agent_in_position(self, position, game):
"""Checks a location for current agents.
"""
agents = game.get_agents_by_position()[position]
if agents:
return agents[0]
def level_up(self):
"""Player levelup is managed by an xp curve.
"""
xpToLevel = (self.level + self.level - 1) * 20
if self.xp >= xpToLevel:
self.xp -= xpToLevel
self.level += 1
def play_turn(self,game):
self.level_up()
game.state["Level"] = self.level

7
poetry.lock generated Normal file
View File

@ -0,0 +1,7 @@
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
package = []
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "53f2eabc9c26446fbcc00d348c47878e118afc2054778c3c803a0a8028af27d9"

75
projectile.py Normal file
View File

@ -0,0 +1,75 @@
# projectile.py
# ------------
# By Pat Wick
# This module defines a "casted" projectile. This is the basis
# for ranged character types' attacks.
from retro.game import Game
class Projectile:
character = "*"
deadly = False
def __init__(self, position, direction, speed, game):
self.position = position
self.direction = direction
self.speed = speed
if game.get_agent_by_name("player").class_ == "Rogue":
if self.direction in [(1,0), (-1,0)]:
self.character = "-"
elif self.direction in [(0,1), (0,-1)]:
self.character = "|"
if game.get_agent_by_name("player").class_ == "Warrior":
if self.direction in [(0,1), (0,-1)]:
self.character = "|"
elif self.direction == (1,0):
self.character = "/"
else:
self.character = "\\"
def move(self, game):
"""Try to move in direction set by player when launched. If blocked,
disappear. If projectile hits an enemy, lower hp by damage.
"""
dx, dy = self.direction
new_position = (self.position[0] + dx, self.position[1] + dy)
if game.on_board(new_position):
if game.is_empty(new_position):
self.position = new_position
else:
agent = self.get_agent_in_position(new_position,game)
if agent:
if agent.deadly:
agent.hp -= game.get_agent_by_name("player").damage
game.remove_agent(self)
else:
game.remove_agent(self)
else:
game.remove_agent(self)
def play_turn(self,game):
"""Speed of projectiles depends on character race.
"""
if game.turn_number % self.speed == 0:
self.move(game)
try:
if game.get_agent_by_name("player").class_ == "Warrior":
game.remove_agent(self)
except:
pass
def get_agent_in_position(self, position, game):
"""Returns an agent at the position, or returns None.
game.get_agents_by_position always returns a list, which may
contain zero, one, or multiple agents at the given position.
In the Beast game, we never allow more than one agent to be in
a position.
"""
agents = game.get_agents_by_position()[position]
if agents:
return agents[0]
def handle_collision(self, game):
# need to fix this at some point
pass

57
strategy.py Normal file
View File

@ -0,0 +1,57 @@
from random import choice
direction_vectors = [(0, 1), (1, 0), (0, -1), (-1, 0)]
def possible_moves(position, game):
"Returns a list of vectors to empty spaces"
agents_by_position = game.get_agents_by_position()
possible_moves = []
for vector in direction_vectors:
x, y = position
dx, dy = vector
new_position = (x + dx, y + dy)
if not agents_by_position[new_position]:
possible_moves.append(vector)
return possible_moves
def random_move(position, game):
"Returns a random vector representing a move to an empty space from position"
moves = possible_moves(position, game)
if moves:
return choice(moves)
def distance(p0, p1):
"""Returns the 'manhattan distance' from one position to another
The 'manhattan distance' describes the distance from one point to another
on a city grid, where you can only go horizontally and vertically, not
diagonally.
"""
x0, y0 = p0
x1, y1 = p1
return abs(x1 - x0) + abs(y1 - y0)
def move_toward_player(position, game):
"Returns a move which will come closest to the player"
player_position = game.get_agent_by_name("player").position
moves = possible_moves(position, game)
moves_with_distance = []
for vector in moves:
x, y = position
dx, dy = vector
new_position = (x + dx, y + dy)
distance_to_player = distance(new_position, player_position)
moves_with_distance.append((distance_to_player, vector))
if moves_with_distance:
shortest_distance, best_move = sorted(moves_with_distance)[0]
return best_move
def move_to_player(position, game):
player_position = game.get_agent_by_name("player").position
for vector in direction_vectors:
x, y = position
dx, dy = vector
new_position = (x + dx, y + dy)
if new_position == player_position:
return vector

7
wall.py Normal file
View File

@ -0,0 +1,7 @@
class Wall:
#name = "wall"
character = ""
deadly = False
def __init__(self,position):
self.position = position