From 9811a6abf9daba9bdf59696c0c01a236f76236e9 Mon Sep 17 00:00:00 2001 From: Brandon Cornejo Date: Tue, 3 Dec 2019 21:37:38 -0600 Subject: [PATCH] Armour/Wep in database --- Makefile | 14 +- acks/commands.py | 4 + acks/npc/commands.py | 39 ++- acks/npc/default_classes.csv | 12 - acks/npc/models.py | 54 +++- acks/npc/npc_party.py | 275 ++++-------------- alembic.ini | 85 ++++++ alembic/README | 1 + alembic/env.py | 86 ++++++ alembic/script.py.mako | 24 ++ ...287451b8bce_create_characterclass_table.py | 24 ++ 11 files changed, 383 insertions(+), 235 deletions(-) delete mode 100644 acks/npc/default_classes.csv create mode 100644 alembic.ini create mode 100644 alembic/README create mode 100644 alembic/env.py create mode 100644 alembic/script.py.mako create mode 100644 alembic/versions/7287451b8bce_create_characterclass_table.py diff --git a/Makefile b/Makefile index b389f6c..2b7e4de 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: run debug clean +.PHONY: run debug clean shell bin/activate: requirements.txt test -f bin/activate || $(shell which python3) -m venv . @@ -13,3 +13,15 @@ debug: bin/activate clean: rm -rf bin/ include/ lib/ lib64/ __pycache__/ share/; rm pyvenv.cfg + +shell: bin/activate + . bin/activate; FLASK_SETTINGS_FILE=../config/dev.cfg FLASK_APP=start.py flask shell + +cli: bin/activate + . bin/activate; FLASK_SETTINGS_FILE=../config/dev.cfg FLASK_APP=start.py flask ${COMMAND} + +recreatedb: bin/activate + . bin/activate; FLASK_SETTINGS_FILE=../config/dev.cfg FLASK_APP=start.py flask acks dropdb + . bin/activate; FLASK_SETTINGS_FILE=../config/dev.cfg FLASK_APP=start.py flask acks initdb + . bin/activate; FLASK_SETTINGS_FILE=../config/dev.cfg FLASK_APP=start.py flask npc populate + diff --git a/acks/commands.py b/acks/commands.py index cb59d0f..32ba1c9 100644 --- a/acks/commands.py +++ b/acks/commands.py @@ -11,3 +11,7 @@ def initialize_database(): from acks.npc import models db.create_all() +@default_cli.command('dropdb') +def destroy_database(): + db.drop_all() + db.session.commit() diff --git a/acks/npc/commands.py b/acks/npc/commands.py index 03cb3ed..698be87 100644 --- a/acks/npc/commands.py +++ b/acks/npc/commands.py @@ -6,16 +6,37 @@ from ..models import db npc_cli = AppGroup('npc') -@npc_cli.command('create classes') -def create_class(): +@npc_cli.command('populate') +def populate_npc_database(): import csv - from .models import CharacterClass + from .models import ( + CharacterClass, + EquipmentArmour, + EquipmentRangedWeapon, + EquipmentMeleeWeapon + ) - classes = [] + def load_csv_data(file_name, cls): + rows = [] + with open('acks/npc/data/{}'.format(file_name), newline='') as data: + reader = csv.DictReader(data) + for row in reader: + rows.append(cls(**row)) + return rows + + # Character Classes + db.session.bulk_save_objects(load_csv_data('default_classes.csv', CharacterClass)) + + # Equipment Armour + db.session.bulk_save_objects(load_csv_data('default_armours.csv', EquipmentArmour)) + + # Ranged Weapons + db.session.bulk_save_objects(load_csv_data('default_ranged.csv', EquipmentRangedWeapon)) + + # Melee Weapons + melee_weps = load_csv_data('default_melee.csv', EquipmentMeleeWeapon) + for wep in melee_weps: + wep.two_handed = (wep.two_handed == 'True') + db.session.bulk_save_objects(melee_weps) - with open('acks/npc/default_classes.csv', newline='') as data: - reader = csv.DictReader(data) - for row in reader: - classes.append(CharacterClass(**row)) - db.session.bulk_save_objects(classes) db.session.commit() diff --git a/acks/npc/default_classes.csv b/acks/npc/default_classes.csv deleted file mode 100644 index 2a10347..0000000 --- a/acks/npc/default_classes.csv +++ /dev/null @@ -1,12 +0,0 @@ -name,bucket,prime_requisite,hit_die_size,maximum_level,armour_modifier,melee_light,melee_medium,melee_heavy,ranged_light,ranged_heavy -Elven Nightblade,Demi-Human,D+I,6,11,-4,1,2,0,0,0 -Elven Spellsword,Demi-Human,S+I,6,10,-4,0,1,0,0,0 -Explorer,Campaign,S+D,6,14,-3,1,1,0,0,1 -Bladedancer,Campaign,W+D,6,14,-2,0,1,0,0,0 -Cleric,Core,WIS,6,14,0,1,3,1,1,3 -Fighter,Core,STR,8,14,1,1,4,1,1,1 -Thief,Core,DEX,4,14,-6,1,1,0,1,0 -Mage,Core,INT,4,14,-8,1,0,0,0,0 -Assassin,Campaign,S+D,6,14,-6,1,2,0,1,0 -Bard,Campaign,D+H,6,14,-3,1,2,0,1,0 -Dwarven Vaultguard,Demi-Human,STR,8,13,-1,0,3,1,1,1 diff --git a/acks/npc/models.py b/acks/npc/models.py index fac9689..151540c 100644 --- a/acks/npc/models.py +++ b/acks/npc/models.py @@ -7,6 +7,7 @@ class CharacterClass(BaseModel): name = db.Column(db.String(50), unique=True, nullable=False) bucket = db.Column(db.String(50)) + frequency_modifier = db.Column(db.Integer, default=1) prime_requisite = db.Column(db.String(3)) hit_die_size = db.Column(db.Integer) @@ -22,5 +23,56 @@ class CharacterClass(BaseModel): def __repr__(self): return ''.format(self.name) +class EquipmentArmour(BaseModel): + __tablename__ = 'eq_armour' -admin_models = [CharacterClass] + name = db.Column(db.String(50), unique=True, nullable=False) + gp_value = db.Column(db.Integer, nullable=False) + ac_mod = db.Column(db.Integer, nullable=False) + + def __repr__(self): + return ''.format(self.name) + +class EquipmentRangedWeapon(BaseModel): + __tablename__ = 'eq_ranged_wep' + + name = db.Column(db.String(50), unique=True, nullable=False) + bucket = db.Column(db.String(20), nullable=False) + gp_value = db.Column(db.Integer, nullable=False) + damage_die = db.Column(db.String(10), nullable=False) + + def __repr__(self): + return ''.format(self.name) + +class EquipmentMeleeWeapon(BaseModel): + __tablename__ = 'eq_melee_wep' + + name = db.Column(db.String(50), unique=False, nullable=False) + bucket = db.Column(db.String(20), nullable=False) + gp_value = db.Column(db.Integer, nullable=False) + damage_die = db.Column(db.String(10), nullable=False) + two_handed = db.Column(db.Boolean, nullable=False) + + def __repr__(self): + return ''.format(self.name) + +class CharacterNPC(BaseModel): + __tablename__ = 'npcs' + + name = db.Column(db.String(50), unique=False, nullable=True) + level = db.Column(db.Integer, nullable=False) + hit_points = db.Column(db.Integer, nullable=False) + + guild_id = db.Column(db.Integer, db.ForeignKey('guild.id'), nullable=False) + guild = db.relationship('CharacterClass', backref=db.backref('npcs', lazy=True)) + + melee_id = db.Column(db.Integer, db.ForeignKey('melee.id'), nullable=False) + melee = db.relationship('EquipmentMeleeWeapon') + + ranged_id = db.Column(db.Integer, db.ForeignKey('ranged.id'), nullable=False) + ranged = db.relationship('EquipmentRangedWeapon') + + def __repr__(self): + return ''.format(self.name) + +admin_models = [CharacterClass, EquipmentArmour, EquipmentRangedWeapon, EquipmentMeleeWeapon, CharacterNPC] diff --git a/acks/npc/npc_party.py b/acks/npc/npc_party.py index e08474e..9318a95 100644 --- a/acks/npc/npc_party.py +++ b/acks/npc/npc_party.py @@ -1,35 +1,20 @@ -from random import randint +from random import randint, choice + +from .models import ( + CharacterClass, + EquipmentArmour, + EquipmentRangedWeapon, + EquipmentMeleeWeapon, + CharacterNPC +) def number_encountered(): return (randint(1, 4) + 2) def npc_class(): - roll = (randint(1, 6) + randint(1, 6) + randint(1, 6)) - if roll == 3 or roll == 4: - return 'Elven Nightblade' - elif roll == 5: - return 'Elven Spellsword' - elif roll == 6: - return 'Explorer' - elif roll == 7: - return 'Bladedancer' - elif roll == 8: - return 'Cleric' - elif roll == 9 or roll == 10 or roll == 11: - return 'Fighter' - elif roll == 12: - return 'Thief' - elif roll == 13: - return 'Mage' - elif roll == 14: - return 'Assassin' - elif roll == 15: - return 'Bard' - elif roll == 16: - return 'Dwarven Vaultguard' - elif roll == 17 or roll == 18: - return 'Dwarven Craftpriest' + classes = [c for cls in CharacterClass.query.all() for c in ([cls] * cls.frequency_modifier)] + return choice(classes) def npc_alignment(): roll = randint(1, 6) @@ -61,7 +46,9 @@ def npc_baselevel(base_level): return base_level def npc_abilities(): - return [(randint(1, 6) + randint(1, 6) + randint(1, 6)) for x in range(0,6)] + ability_list = ['str', 'int', 'wis', 'dex', 'con', 'chr'] + abilities = [(randint(1, 6) + randint(1, 6) + randint(1, 6)) for x in range(0,6)] + return zip(ability_list, abilities) def attribute_mod(atr): mod = 0 @@ -76,214 +63,78 @@ def attribute_mod(atr): return mod - -npc_class_data = { - 'Elven Nightblade': { - 'hd': [1, 6], - 'armour': -4, - 'melee': [0, 2, 1], - 'ranged': [0, 0, 1] - }, - - 'Elven Spellsword': { - 'hd': [1, 6], - 'armour': -4, - 'melee': [0, 1, 0], - 'ranged': [0, 0, 1] - }, - - 'Explorer': { - 'hd': [1, 6], - 'armour': -3, - 'melee': [0, 1, 1], - 'ranged': [0, 1, 2] - }, - - 'Bladedancer': { - 'hd': [1, 6], - 'armour': -2, - 'melee': [0, 1, 0], - 'ranged': [0, 0, 1] - }, - - 'Cleric': { - 'hd': [1, 6], - 'armour': 0, - 'melee': [1, 3, 1], - 'ranged': [1, 3, 4] - }, - - 'Fighter': { - 'hd': [1, 8], - 'armour': 1, - 'melee': [1, 4, 1], - 'ranged': [1, 1, 4], - }, - - 'Thief': { - 'hd': [1, 4], - 'armour': -6, - 'melee': [0, 1, 1], - 'ranged': [1, 0, 6] - }, - - 'Mage': { - 'hd': [1, 4], - 'armour': -8, - 'melee': [0, 0, 1], - 'ranged': [0, 0, 1] - }, - - 'Assassin': { - 'hd': [1, 6], - 'armour': -6, - 'melee': [0, 2, 1], - 'ranged': [1, 0, 5] - }, - - 'Bard': { - 'hd': [1, 6], - 'armour': -3, - 'melee': [0, 2, 1], - 'ranged': [1, 0, 4] - }, - - 'Dwarven Vaultguard': { - 'hd': [1, 8], - 'armour': -1, - 'melee': [1, 3, 0], - 'ranged': [1, 1, 6] - }, - - 'Dwarven Craftpriest': { - 'hd': [1, 6], - 'armour': -2, - 'melee': [0, 1, 0], - 'ranged': [1, 0, 5] - } -} - -npc_armour_list = [ - ['Clothing Only', 0, 0], - ['Hide and Fur Armour', 10, 1], - ['Leather Armour', 20, 2], - ['Ring Mail Armour', 30, 3], - ['Scale Mail Armour', 30, 3], - ['Chain Mail Armour', 40, 4], - ['Banded Plate', 50, 5], - ['Lamellar Armour', 50, 5], - ['Plate Armour', 60, 6], -] - -npc_melee_weapons_data = { - 'light': [ - ['Club', 1, '1d4'], - ['Dagger', 3, '1d4'], - ['Sap', 1, '1d4'], - ['Staff (1h)', 1, '1d4'], - ['Staff (2h)', 1, '1d6'], - ['Whip', 5, '1d2'] - ], - 'medium': [ - ['Battle Axe (1h)', 7, '1d6'], - ['Battle Axe (2h)', 7, '1d8'], - ['Hand Axe', 4, '1d6'], - ['Flail (1h)', 5, '1d6'], - ['Flail (2h)', 5, '1d8'], - ['Mace (1h)', 5, '1d6'], - ['Mace (2h)', 5, '1d8'], - ['War Hammer (1h)', 5, '1d6'], - ['War Hammer (2h)', 5, '1d8'], - ['Spear (1h)', 3, '1d6'], - ['Spear (2h)', 3, '1d8'], - ['Short Sword', 7, '1d6'], - ['Sword (1h)', 10, '1d6'], - ['Sword (2h)', 10, '1d8'] - ], - 'heavy': [ - ['Great Axe (2h)', 10, '1d10'], - ['Morning Star (2h)', 10, '1d10'], - ['Pole Arm (2h)', 7, '1d10'], - ['Two-Handed Sword (2h)', 15, '1d10'] - ] -} - -npc_ranged_weapons_data = { - 'light': [ - ['Crossbow', 30, '1d6'], - ['Shortbow', 3, '1d6'], - ['Javelin', 1, '1d6'], - ], - 'heavy': [ - ['Arbalest', 50, '1d8'], - ['Composite Bow', 40, '1d6'], - ['Longbow', 7, '1d6'], - ] -} - -def select_melee_weapon(guild): - melee = npc_class_data[guild]['melee'] +def select_melee_weapon(guild, data): weapons = [] - for x in range(0, melee[0]): - weapons.extend(npc_melee_weapons_data['heavy']) - for x in range(0, melee[1]): - weapons.extend(npc_melee_weapons_data['medium']) - for x in range(0, melee[2]): - weapons.extend(npc_melee_weapons_data['light']) + for x in range(0, guild.melee_heavy): + weapons.extend(data['melee']['heavy']) + for x in range(0, guild.melee_medium): + weapons.extend(data['melee']['medium']) + for x in range(0, guild.melee_light): + weapons.extend(data['melee']['light']) selection = randint(0, len(weapons) - 1) return weapons[selection] -def select_ranged_weapon(guild): - ranged = npc_class_data[guild]['ranged'] +def select_ranged_weapon(guild, data): weapons = [] - for x in range(0, ranged[0]): - weapons.extend(npc_ranged_weapons_data['light']) - for x in range(0, ranged[1]): - weapons.extend(npc_ranged_weapons_data['heavy']) - for x in range(0, ranged[2]): - weapons.append(None) + for x in range(0, guild.ranged_light): + weapons.extend(data['ranged']['light']) + for x in range(0, guild.ranged_heavy): + weapons.extend(data['ranged']['heavy']) + + if not weapons: + return None selection = randint(0, len(weapons) - 1) return weapons[selection] -def generate_npc(base_level): - attributes = npc_abilities() +def generate_npc(base_level, data): + guild = npc_class() npc = {} - npc['guild'] = npc_class() + npc['guild'] = guild.name + npc['level'] = npc_baselevel(base_level) - npc['str'] = attributes[0] - npc['int'] = attributes[1] - npc['wis'] = attributes[2] - npc['dex'] = attributes[3] - npc['con'] = attributes[4] - npc['chr'] = attributes[5] + npc.update(npc_abilities()) npc['hp'] = 0 conmod = attribute_mod(npc['con']) - for x in range(0, npc['level']): - hitdice = [randint(1, npc_class_data[npc['guild']]['hd'][1]) for x in range(0, npc_class_data[npc['guild']]['hd'][0])] - for die in hitdice: - die += conmod - if die < 1: - die = 1 - npc['hp'] += die - - armourval = randint(0, len(npc_armour_list) - 1) + npc_class_data[npc['guild']]['armour'] + hitdice = [randint(1, guild.hit_die_size) for x in range(0, npc['level'])] + for die in hitdice: + die += conmod + if die < 1: + die = 1 + npc['hp'] += die + + armourval = randint(0, len(data['armours']) - 1) + guild.armour_modifier if armourval < 0: armourval = 0 - if armourval > len(npc_armour_list) - 1: - armourval = len(npc_armour_list) - 1 - npc['armour'] = npc_armour_list[armourval] - npc['melee'] = select_melee_weapon(npc['guild']) - npc['ranged'] = select_ranged_weapon(npc['guild']) + if armourval > len(data['armours']) - 1: + armourval = len(data['armours']) - 1 + armour = data['armours'][armourval] + npc['armour'] = [armour.name, armour.gp_value, armour.ac_mod] + melee = select_melee_weapon(guild, data) + npc['melee'] = [melee.name, melee.gp_value, melee.damage_die] if melee else None + ranged = select_ranged_weapon(guild, data) + npc['ranged'] = [ranged.name, ranged.gp_value, ranged.damage_die] if ranged else None return npc def create_party(base_level): + data = { + 'armours': EquipmentArmour.query.all(), + 'ranged': { + 'light': EquipmentRangedWeapon.query.filter_by(bucket='Light').all(), + 'heavy': EquipmentRangedWeapon.query.filter_by(bucket='Heavy').all() + }, + 'melee': { + 'light': EquipmentMeleeWeapon.query.filter_by(bucket='Light').all(), + 'medium': EquipmentMeleeWeapon.query.filter_by(bucket='Medium').all(), + 'heavy': EquipmentMeleeWeapon.query.filter_by(bucket='Heavy').all() + }, + } party_size = number_encountered() - return [generate_npc(base_level) for x in range(0, party_size)] + return [generate_npc(base_level, data) for x in range(0, party_size)] def print_party(party): def print_npc(npc): diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..bfcc3c7 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,85 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks=black +# black.type=console_scripts +# black.entrypoint=black +# black.options=-l 79 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..941647f --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,86 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + + +from acks import app +from acks.models import db + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# set the sqlalchemy url to the one defined for flask-sqlalchemy +config.set_main_option('sqlalchemy.url', app.config['SQLALCHEMY_DATABASE_URI']) + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from acks.models import BaseModel +target_metadata = BaseModel.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/7287451b8bce_create_characterclass_table.py b/alembic/versions/7287451b8bce_create_characterclass_table.py new file mode 100644 index 0000000..46b30b7 --- /dev/null +++ b/alembic/versions/7287451b8bce_create_characterclass_table.py @@ -0,0 +1,24 @@ +"""create CharacterClass table + +Revision ID: 7287451b8bce +Revises: +Create Date: 2019-12-02 23:01:13.613091 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '7287451b8bce' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass