refactoring completed. Extracted routes.py. Eliminated both schemas and models. Restructured project runner. All endpoints now GET

This commit is contained in:
Greg Gauthier 2024-07-01 11:26:54 +01:00
parent f3ec123d6b
commit 77aa5f8b69
19 changed files with 118 additions and 361 deletions

View File

@ -18,7 +18,7 @@ app.config['MONSTERS_JSON_PATH'] = MONSTERS_JSON_PATH
def init():
from app.routes import coinflip, roll_chance, roll_dice, table_views, encounter, character, \
ability_check, mental_attack, physical_attack, get_monster
ability_check, mental_attack, physical_attack, get_creature
# root namespace
restx_api.add_namespace(coinflip.namespace)
# dice namespace
@ -27,7 +27,7 @@ def init():
# rules namespace
restx_api.add_namespace(table_views.namespace)
restx_api.add_namespace(character.namespace)
restx_api.add_namespace(get_monster.namespace)
restx_api.add_namespace(get_creature.namespace)
# gameplay namespace
restx_api.add_namespace(encounter.namespace)
restx_api.add_namespace(ability_check.namespace)

View File

@ -2,6 +2,8 @@ from app.functions.roll_dices import roll_dices
def get_attack_roll_outcome(result, modifier=0):
if modifier is None:
modifier = 0
outcome = 'Unknown'
raw_roll = roll_dices(1, 20, False).get('result')
needed = result['needed']

View File

@ -10,8 +10,10 @@ def roll_physical_attack(dac, modifier=0, awc=None, ahd=None):
:param ahd: integer. optional(*). Attacker hit dice. This is required, if weapon attack == False.
:return:
"""
result = {}
if modifier is None:
modifier = 0
result = {}
if isinstance(awc, int):
hit_table = AttackerWeaponClassMatrix()
result["needed"] = hit_table.get_attack_score(awc, dac)

View File

View File

@ -1,133 +0,0 @@
from flask_restx import fields
# Common fields
chartype_field = fields.String(
required=True,
default="human",
description='Character type. Allowed values: "human", "humanoid", "mutant", "cyborg"'
)
conscore_field = fields.Integer(
required=True,
default=10,
min=3,
max=18,
description='The characters constitution score'
)
check_model = {
'ability_score': fields.Integer(
required=True,
default=10,
min=3,
max=21,
description='The score of the ability to check against'
),
'multiplier': fields.Integer(
required=True,
default=4,
min=2, max=8,
description='Sets the threshold for the check. In general, the higher the multiplier, the higher the '
'likelihood of success. Range: 2 - 7'
)
}
# Mutations Model
mutation_model = {
'conscore': conscore_field,
'intscore': fields.Integer(
required=True,
default=10,
min=3,
max=21,
description='The characters intelligence score'
)
}
# Dice model
dice_model = {
'quantity': fields.Integer(required=True, default=1, description='The number of dice to roll'),
'geometry': fields.Integer(required=True, default=2, description='The number of sides on each die'),
'discard_lowest': fields.Boolean(required=False, default=False, description='Drop the lowest score')
}
# Ability model
ability_model = {
'chartype': chartype_field,
'ability': fields.String(
required=False,
default="all",
description='The ability to roll. Not required. Valid options: "m-strength", "p-strength", '
'"intelligence", "charisma", "constitution", "dexterity", "all". Defaults to "all".'),
}
# Hp model
hp_model = {
'chartype': chartype_field,
'conscore': conscore_field
}
pa_model = {
'weapon_attack': fields.Boolean(
required=True,
default=True,
description='Is the attacker using a weapon? If so, To-Hit is based on weapon class.'
),
'dac': fields.Integer(
required=True,
min=1,
max=10,
default=1,
description='The defenders armour class. This is needed for both weapon attacks and non-weapon attacks'
),
'awc': fields.Integer(
required=False,
min=1,
max=16,
default=1,
description='The attackers weapon class. This is needed for weapon attacks only.'
),
'ahd': fields.Integer(
required=False,
min=1,
max=16,
default=1,
description='The attackers hit dice count. This is needed for non-weapon attacks only.'
),
'modifier': fields.Integer(
required=False,
min=-100,
max=100,
default=0,
description='The roll modifier to be applied to the hit roll.'
)
}
ma_model = {
'ams': fields.Integer(
required=True,
min=3,
max=18,
default=10,
description='Attacker Mental Strength'
),
'dms': fields.Integer(
required=True,
min=3,
max=18,
default=10,
description='Defender Mental Strength'
),
'modifier': fields.Integer(
required=False,
min=-100,
max=100,
default=0,
description='Modifier For Mental Attack Roll'),
}
character_model = {
'chartype': chartype_field,
}

View File

@ -1,20 +1,27 @@
from flask import request
from flask_restx import Resource, Namespace
from flask_restx import Resource, Namespace, reqparse
from app.functions.roll_ability_check import roll_ability_check
namespace = Namespace('gameplay', description='Gamma World Rules')
# Define the parser and request args:
parser = reqparse.RequestParser()
parser.add_argument('score', type=int, required=True,
help='The score of the ability to be checked (3 - 21)')
parser.add_argument('multiplier', type=int, required=True,
help='The score multiplier for this check attempt (2 - 10)')
@namespace.route('/ability/check') # resolves to: /gameplay/ability/check
class AbilityCheck(Resource):
@namespace.doc(params={'score': 'The ability score', 'multiplier': 'Score multiplier for the attempt.'})
@namespace.expect(parser)
def get(self):
score = request.args.get('score', type=int)
multiplier = request.args.get('multiplier', type=float)
if not score or not multiplier:
return {'error': 'Both score and multiplier parameters are required.'}, 400
if score < 3 or score > 21:
return {'message': 'Ability score must be between 3 and 21'}, 400
if multiplier < 2 or multiplier > 10:
return {'message': 'Multiplier must be between 2 and 10'}, 400
outcome = roll_ability_check(score, multiplier)
return outcome, 200

View File

@ -1,25 +1,28 @@
from flask import request
from flask_restx import Resource, Namespace
from flask_restx import Resource, Namespace, reqparse
from app.functions.build_character_sheet import build_character_sheet
namespace = Namespace('rules', description='Gamma World Rules')
VALID_CHARTYPES = ["human", "humanoid", "mutant", "cyborg"]
parser = reqparse.RequestParser()
parser.add_argument('chartype', type=str, default='Human',
help='The Character Type for the new character (human, humanoid, mutant, cyborg)')
@namespace.route('/character') # resolves to: /rules/character
class GenerateCharacter(Resource):
@namespace.doc(params={'chartype': 'The Character Type for the new character'})
@namespace.expect(parser)
def get(self):
valid_chartypes = ["human", "humanoid", "mutant", "cyborg"]
chartype = request.args.get('chartype', default='Human', type=str)
if chartype:
if chartype.lower() in VALID_CHARTYPES:
if chartype.lower() in valid_chartypes:
return build_character_sheet(chartype.lower()), 200
else:
return {
'error': 'Invalid character type provided.',
'valid_chartypes': VALID_CHARTYPES
'valid_chartypes': valid_chartypes
}, 400
else:
return {'error': 'No character type provided', 'valid_chartypes': VALID_CHARTYPES}, 400
return {'error': 'No character type provided', 'valid_chartypes': valid_chartypes}, 400

View File

@ -1,26 +1,28 @@
from flask import request
from flask_restx import Resource, Namespace
from flask_restx import Resource, Namespace, reqparse
from app.functions.roll_encounter import roll_encounter
namespace = Namespace('gameplay', description='Gamma World Game Play')
VALID_TERRAINS = ["clear", "mountains", "forest", "desert", "watery", "ruins", "deathlands"]
parser = reqparse.RequestParser()
parser.add_argument('terrain', type=str, default='clear',
help='The terrain being traversed. ("clear", "mountains", "forest", "desert",'
'"watery", "ruins", "deathlands")')
@namespace.route('/encounter') # resolves to: /gameplay/encounter
class RollEncounter(Resource):
@namespace.doc(params={'terrain': 'The terrain type for the encounter'})
@namespace.expect(parser)
def get(self):
terrain = request.args.get('terrain', default=None, type=str)
terrain = request.args.get('terrain', default="clear", type=str)
valid_terrains = ["clear", "mountains", "forest", "desert", "watery", "ruins", "deathlands"]
if terrain:
if terrain.lower() in VALID_TERRAINS:
if terrain.lower() in valid_terrains:
return roll_encounter(terrain.lower()), 200
else:
return {
'error': 'Invalid terrain type provided.',
'valid_terrains': VALID_TERRAINS
'valid_terrains': valid_terrains
}, 400
else:
return {'error': 'No terrain type provided', 'valid_terrains': VALID_TERRAINS}, 400
return {'error': 'No terrain type provided', 'valid_terrains': valid_terrains}, 400

View File

@ -0,0 +1,18 @@
from flask import request
from flask_restx import Resource, Namespace, reqparse
from app.tables.creature import Creatures
namespace = Namespace('rules', description='Gamma World Rules')
parser = reqparse.RequestParser()
parser.add_argument('creature', type=str, help='the name of a Gamma World creature to search for.')
@namespace.route('/creature') # resolves to: /rules/creature
class RollEncounter(Resource):
@namespace.expect(parser)
def get(self):
creature = request.args.get('creature', default=None, type=str)
if creature is None:
return {'error': 'Provide the name of a Gamma World creature to search for'}, 400
creatures = Creatures()
return creatures.get_creature(creature), 200

View File

@ -1,19 +0,0 @@
from flask import request
from flask_restx import Resource, Namespace
from app.tables.monsters import Monsters
namespace = Namespace('rules', description='Gamma World Rules')
@namespace.route('/creature') # resolves to: /gameplay/encounter
class RollEncounter(Resource):
@namespace.doc(params={'creature': 'The terrain type for the encounter'})
def get(self):
creature = request.args.get('creature', default=None, type=str)
if creature is None:
return {'error': 'Provide the name of a Gamma World creature to search for'}, 400
monsters = Monsters()
return monsters.get_monster(creature), 200

View File

@ -1,19 +1,30 @@
from flask import request
from flask_restx import Resource, Namespace
from flask_restx import Resource, Namespace, reqparse
from app.functions.roll_mental_attack import roll_mental_attack
namespace = Namespace('gameplay', description='Gamma World Rules')
# doc(params={'ams': 'Attacker Mental Strength',
# 'dms': 'Defender Mental Strength',
# 'modifier': 'Roll Modifier'})
parser = reqparse.RequestParser()
parser.add_argument('ams', type=int, required=True, help='Attacker Mental Strength (1 - 18)')
parser.add_argument('dms', type=int, required=True, help='Defender Mental Strength (1 - 18)')
parser.add_argument('modifier', type=int, required=True, help='Roll Modifier')
@namespace.route('/attack/mental') # resolves to: /gameplay/attack/mental
class MentalAttack(Resource):
@namespace.doc(params={'ams': 'Attacker Mental Strength',
'dms': 'Defender Mental Strength',
'modifier': 'Roll Modifier'})
@namespace.expect(parser)
def get(self):
ams = request.args.get('ams', type=int)
dms = request.args.get('dms', type=int)
modifier = request.args.get('modifier', type=int)
# Validate params here
if ams < 1 or ams > 18:
return {'message': 'Attacker Mental Strength must be between 1 and 18'}, 400
if dms < 1 or dms > 18:
return {'message': 'Defender Mental Strength must be between 1 and 18'}, 400
return roll_mental_attack(ams, dms, modifier), 200

View File

@ -1,18 +1,20 @@
from flask import request
from flask_restx import Resource, Namespace
from flask_restx import Resource, Namespace, reqparse
from app.functions.roll_physical_attack import roll_physical_attack
namespace = Namespace('gameplay', description='Gamma World Rules')
parser = reqparse.RequestParser()
parser.add_argument('dac', type=int, required=True, help='REQUIRED: Defender Armour Class. Needed for both attacks')
parser.add_argument('awc', type=int, help='OPTIONAL(*): Attacker Weapon Class. Only needed for weapon attacks')
parser.add_argument('ahd', type=int, help='OPTIONAL(*): Attacker Hit Dice. Only needed for non-weapon attacks')
parser.add_argument('modifier', type=int, help='OPTIONAL: Roll Modifier')
@namespace.route('/attack/physical') # resolves to: /gameplay/attack/physical
class PhysicalAttack(Resource):
@namespace.doc(
params={'dac': 'REQUIRED: Needed for both attacks',
'modifier': 'OPTIONAL: Roll Modifier',
'awc': 'OPTIONAL(*): Attacker Weapon Class. Only needed for weapon attacks',
'ahd': 'OPTIONAL(*): Attacker Hit Dice. Only needed for non-weapon attacks'})
@namespace.expect(parser)
def get(self):
dac = request.args.get('dac', type=int)
awc = request.args.get('awc', type=int)

View File

@ -1,24 +1,25 @@
from flask_restx import Resource, Namespace
from flask_restx import Resource, Namespace, reqparse
from app.functions.roll_dices import roll_dices
from app.models.models import dice_model
from app.schemas.schemas import DiceSchema
namespace = Namespace('dice', description='Dice Operations')
dice_model = namespace.model('Dice', dice_model)
dice_schema = DiceSchema()
# Define the parser and request args:
parser = reqparse.RequestParser()
parser.add_argument('quantity', type=int, required=True, help='Quantity of dice to roll')
parser.add_argument('geometry', type=int, required=True, help='Number of faces on the dice')
parser.add_argument('discard_lowest', type=bool, required=True, help='Whether to discard lowest roll')
@namespace.route('/') # resolves to: /dice
class RollDice(Resource):
@namespace.expect(dice_model)
def post(self):
data = namespace.payload
errors = dice_schema.validate(data)
if errors:
return errors, 400
quantity = data.get('quantity')
geometry = data.get('geometry')
discard_lowest = data.get('discard_lowest')
if quantity is None or geometry is None:
return {"message": "Required dice data not provided"}, 400
@namespace.expect(parser)
def get(self):
args = parser.parse_args()
quantity = args['quantity']
geometry = args['geometry']
discard_lowest = args['discard_lowest']
if quantity < 1 or quantity > 100:
return {'message': 'Quantity must be between 1 and 100'}, 400
if geometry < 3 or geometry > 100:
return {'message': 'Geometry must be between 3 and 100'}, 400
return roll_dices(quantity, geometry, discard_lowest), 200

View File

@ -1,130 +0,0 @@
from marshmallow import Schema, fields, validate
chartype_field = fields.String(
required=True,
validate=validate.OneOf(["human", "humanoid", "mutant", "cyborg"]),
description='The characters type of being'
)
conscore_field = fields.Integer(
required=True,
default=10,
validate=validate.Range(min=3, max=18),
description='The constitution score of the character'
)
class MutationSchema(Schema):
conscore = conscore_field
intscore = fields.Integer(
required=True,
validate=validate.Range(min=3, max=18),
description='The characters intelligence score'
)
class HPSchema(Schema):
chartype = chartype_field
conscore = conscore_field
class DiceSchema(Schema):
quantity = fields.Int(
required=True,
default=1,
validate=validate.Range(min=1),
description='The number of dice to roll'
)
geometry = fields.Int(
required=True,
default=2,
validate=validate.Range(min=2),
description='The number of sides on each die'
)
discard_lowest = fields.Bool(required=True, default=False, description='Drop the lowest score')
class CharacterSchema(Schema):
chartype = chartype_field
class AbilitySchema(Schema):
chartype = chartype_field
ability = fields.String(
required=False,
default="generic",
validate=validate.OneOf(
["m-strength", "p-strength", "intelligence", "charisma", "constitution", "dexterity", "all"]),
description='One of the six character attributes from the character sheet'
)
class CheckSchema(Schema):
ability_score = fields.Integer(
required=True,
default=10,
validate=validate.Range(min=3, max=21),
description='The score of the ability to be checked against'
)
multiplier = fields.Integer(
required=True,
default=4,
validate=validate.Range(min=2, max=8),
description='Sets the threshold for the check. In general, the higher the multiplier, the higher the '
'likelihood of success. Range: 2 - 7'
)
class MentalAttackSchema(Schema):
ams = fields.Integer(
required=True,
validate=validate.Range(min=3, max=18),
description='The Attackers Mental Strength'
)
dms = fields.Integer(
required=True,
validate=validate.Range(min=3, max=18),
description='The Defenders Mental Strength'
)
modifier = fields.Integer(
required=False,
default=0,
validate=validate.Range(min=-100, max=100),
description='Roll modifier for mental attack'
)
class PhysicalAttackSchema(Schema):
weapon_attack = fields.Boolean(
required=True,
default=True,
description="Is the attacker using a weapon? If so, To-Hit is based on weapon class."
)
dac = fields.Integer(
required=True,
min=1,
max=10,
default=1,
description='The defenders armour class. This is needed for both weapon attacks and non-weapon attacks'
)
awc = fields.Integer(
required=False,
min=1,
max=16,
default=1,
description='The attackers weapon class. This is needed for weapon attacks only.'
)
ahd = fields.Integer(
required=False,
min=1,
max=16,
default=1,
description='The attackers hit dice count. This is needed for non-weapon attacks only.'
)
modifier = fields.Integer(
required=False,
min=-100,
max=100,
default=0,
description='The roll modifier to be applied to the hit roll.'
)

19
app/tables/creature.py Normal file
View File

@ -0,0 +1,19 @@
import json
class Creatures:
creatures = None
@staticmethod
def load_creature_data():
"""Loads the monsters data from a JSON file"""
with open('app/tables/creatures.json') as f:
Creatures.creatures = json.load(f)
def __init__(self):
if not Creatures.creatures:
self.load_creature_data()
def get_creature(self, creature_name):
"""Returns the dictionary of the specified creature."""
return self.creatures.get(creature_name)

View File

@ -1,28 +0,0 @@
import json
class Monsters:
monsters = None
@staticmethod
def load_monsters_data():
"""Loads the monsters data from a JSON file"""
with open('app/tables/monsters.json') as f:
Monsters.monsters = json.load(f)
def __init__(self):
if not Monsters.monsters:
self.load_monsters_data()
def get_monster(self, monster_name):
"""Returns the dictionary of the specified monster."""
return self.monsters.get(monster_name)
def add_monster(self, monster_name, attributes):
"""Adds a new monster to the monsters dictionary."""
self.monsters[monster_name] = attributes
def remove_monster(self, monster_name):
"""Removes a monster from the monsters dictionary."""
if monster_name in self.monsters:
del self.monsters[monster_name]

View File

@ -3,4 +3,4 @@ import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, 'app/tables')
MONSTERS_JSON_PATH = os.path.join(DATA_DIR, 'monsters.json')
MONSTERS_JSON_PATH = os.path.join(DATA_DIR, 'creatures.json')