Skip to content

Commit 98687ef

Browse files
committed
Add YAML validator in setup.py
Added a YAML validator for both YAML files into the setup.py. Now it will not attempt to create the initial database without first confrirming a valid pair of YAML's.
1 parent ca45d63 commit 98687ef

File tree

3 files changed

+291
-21
lines changed

3 files changed

+291
-21
lines changed

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,24 +55,22 @@ Make sure you already started your server. If you just did your setup you are f
5555
python manage.py runserver
5656
```
5757

58-
## First run `setup.py` once
58+
## Run `setup.py`
5959

60-
The first time you will need to have your YAML files ready and edit the `setup.py` to point to the absolute paths to those files on your computer or the relative paths from the code repository, like this:
60+
To get set up you will need to have your YAML files ready and edit the `setup.py` to point to the absolute paths to those files on your computer or the relative paths from the code repository, like this:
6161

6262
```python
6363
skillstats_yaml = 'Examples/system_stats_skills.yaml'
6464
history_yaml = 'Examples/system_history.yaml'
6565
```
6666

67-
Only run `setup.py` once to initialize your database's history rolling and roles, stats, and skills.
68-
6967
In another different terminal navigated to the same folder from the "Getting Set Up" section above, type:
7068

7169
```
7270
python setup.py
7371
```
7472

75-
It will print out a few messages about setting things up in your database.
73+
`setup.py` has a built-in YAML validator that tells you about any errors in your YAML files. Make sure to repeat running `setup.py` and correcting any YAML errors until you get the `Successfully completed setup.py! Proceed to makecharacter.py...` message. This initializes your database's history rolling and roles, stats, and skills.
7674

7775
## From then on
7876

docs/roles_skills_stats.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,19 @@ roles:
3737

3838
`stats`
3939

40-
The `stats` indented keyword list is an indented list of character statistics in the gaming system you're defining. Indented under `stats` are the stat names as you want them to appear in character sheets. Under each stat are the keywords `stat` and `skills`.
40+
The `stats` indented keyword list is an indented list of character statistics in the gaming system you're defining. Indented under `stats` are the stat names as you want them to appear in character sheets. Under each stat is the keyword `stat`. `skills` is also allowed and optional for stats that boast skills specific to that stat.
4141

4242
In skill and stat ranges, a minimum value greater than `0` means that minimum number of points will always be spent on that stat or skill from the stat or skill points.
4343

4444
`stat`
4545

4646
The minimum to maximum range of possible values of that stat.
4747

48-
`skills`
48+
# Optional
4949

50-
An indented list of keys and values where the key is the skill name and the value is the minimum to maximum range of possible values of that skill.
50+
`skills`
5151

52-
# Optional
52+
Nested under a named stat, `skills` is an indented list of keys and values where the key is the skill name and the value is the minimum to maximum range of possible values of that skill.
5353

5454
`roles`
5555

setup.py

Lines changed: 284 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,264 @@
66

77
from CharacterCreator.models import *
88

9+
10+
# class HistoryEventError(Exception):
11+
# def __init__(self, name):
12+
# self.name = name
13+
# print(self.name + ' not present')
14+
# raise
15+
16+
17+
def validate_skillstats(yaml_file):
18+
print('Validating Skills & Stats YAML: ' + yaml_file)
19+
20+
with open(yaml_file, 'r') as yamlfile:
21+
tree = yaml.load(yamlfile)
22+
23+
error_flag = False
24+
25+
if 'stats' not in tree.keys():
26+
print('ERROR: Missing required stats keyword up top')
27+
error_flag = True
28+
29+
skill_collection = []
30+
for stat in tree['stats'].keys():
31+
substat = tree['stats'][stat]
32+
33+
if 'stat' not in substat:
34+
print('ERROR: stat keyword not in [stats > ' + stat + ']')
35+
error_flag = True
36+
else:
37+
# improve with re non-hyphen + non-numeric tests
38+
value_range = [int(value) for value in substat['stat'].split('-')]
39+
prefix = 'ERROR: stat range (' + substat['stat'] + ') in [stats > ' + stat + ' > stat]'
40+
if len(value_range) < 2:
41+
print(prefix + ' has less than two hyphenated values')
42+
error_flag = True
43+
elif len(value_range) > 2:
44+
print(prefix + ' has greater than two hyphenated values')
45+
error_flag = True
46+
elif value_range[0] > value_range[1]:
47+
print(prefix + ' contains a larger first number (' + str(value_range[0]) + ') than second number (' + str(value_range[1]) + ')')
48+
error_flag = True
49+
50+
if 'skills' not in substat:
51+
print('WARNING: skills keyword not in [stats > ' + stat + ']')
52+
else:
53+
for skill in substat['skills'].keys():
54+
subskill = substat['skills'][skill]
55+
value_range = [int(value) for value in subskill.split('-')]
56+
prefix = 'ERROR: skill range (' + subskill + ') in [stats > ' + stat + ' > skills > ' + skill + ']'
57+
if len(value_range) < 2:
58+
print(prefix + ' has less than two hyphenated values')
59+
error_flag = True
60+
elif len(value_range) > 2:
61+
print(prefix + ' has greater than two hyphenated values')
62+
error_flag = True
63+
elif value_range[0] > value_range[1]:
64+
print(prefix + ' contains a larger first number (' + str(value_range[0]) + ') than second number (' + str(value_range[1]) + ')')
65+
error_flag = True
66+
skill_collection.append(skill)
67+
68+
if 'roles' in tree.keys():
69+
for role in tree['roles'].keys():
70+
subrole = tree['roles'][role]
71+
if 'special' not in subrole:
72+
print('ERROR: special keyword not in [roles > ' + role + ']')
73+
error_flag = True
74+
if 'common' not in subrole:
75+
print('ERROR: common keyword not in [roles > ' + role + ']')
76+
error_flag = True
77+
78+
for special in subrole['special'].keys():
79+
subspecial = subrole['special'][special]
80+
value_range = [int(value) for value in subspecial.split('-')]
81+
prefix = 'ERROR: special range (' + subspecial + ') in [roles > ' + role + ' > special > ' + special + ']'
82+
if len(value_range) < 2:
83+
print(prefix + ' has less than two hyphenated values')
84+
error_flag = True
85+
elif len(value_range) > 2:
86+
print(prefix + ' has greater than two hyphenated values')
87+
error_flag = True
88+
elif value_range[0] > value_range[1]:
89+
print(prefix + ' contains a larger first number (' + str(value_range[0]) + ') than second number (' + str(value_range[1]) + ')')
90+
error_flag = True
91+
92+
for common in subrole['common']:
93+
if common not in skill_collection:
94+
print('ERROR: [roles > ' + role + ' > common > ' + common + '] not among all common skills:')
95+
print(skill_collection)
96+
error_flag = True
97+
98+
if error_flag:
99+
return False
100+
else:
101+
print('Valid Skills & Stats YAML!')
102+
return True
103+
104+
105+
def validate_history(yaml_file):
106+
print('Validating History YAML: ' + yaml_file)
107+
108+
with open(yaml_file, 'r') as yamlfile:
109+
tree = yaml.load(yamlfile)
110+
111+
events = list(tree.keys())
112+
error_flag = False
113+
114+
if 'START' not in events:
115+
print('ERROR: Missing required START keyword up top')
116+
error_flag = True
117+
118+
if 'NPC' not in events:
119+
print('ERROR: Missing required NPC keyword up top')
120+
error_flag = True
121+
122+
events.remove('START')
123+
events.remove('NPC')
124+
125+
reroll_list = []
126+
127+
for event in events:
128+
event_dict = tree[event]
129+
130+
if 'dice' not in event_dict:
131+
print('ERROR: dice keyword not in event ' + event)
132+
error_flag = True
133+
if 'roll' not in event_dict:
134+
print('ERROR: roll keyword not in event ' + event)
135+
error_flag = True
136+
137+
dice = event_dict['dice']
138+
search = re.match(r'([0-9]+)d([0-9]+)\ *(\+|-)*\ *([0-9]*)', dice)
139+
if search.group(1) == None and search.group(2) == None:
140+
print('ERROR: Invalid dice (' + dice + ') for event ' + event)
141+
error_flag = True
142+
143+
quantity = int(search.group(1))
144+
sides = int(search.group(2))
145+
if search.group(3) == None:
146+
offset = 0
147+
else:
148+
offset = int(search.group(3)+search.group(4))
149+
150+
dice_min = quantity + offset
151+
dice_max = quantity * sides + offset
152+
dice_span = list(range(dice_min, dice_max+1))
153+
154+
rolls = event_dict['roll']
155+
156+
if 'EVEN' in rolls and 'ODD' not in rolls and len(rolls) == 2:
157+
print('ERROR: EVEN in roll, but no ODD in roll for event ' + event)
158+
error_flag = True
159+
elif 'EVEN' not in rolls and 'ODD' in rolls and len(rolls) == 2:
160+
print('ERROR: ODD in roll, but no EVEN in roll for event ' + event)
161+
error_flag = True
162+
elif 'EVEN' in rolls and 'ODD' in rolls and len(rolls) > 2:
163+
print('ERROR: EVEN and ODD keywords used, but there are more than two keywords in event ' + event)
164+
error_flag = True
165+
elif 'EVEN' not in rolls and 'ODD' not in rolls:
166+
possible_rolls = []
167+
# a dictionary of roll value lists whose keys are roll strings
168+
for element in rolls.keys():
169+
roll_str = str(element)
170+
171+
if '-' not in roll_str and ',' not in roll_str:
172+
# a single value
173+
values = int(roll_str)
174+
possible_rolls.append(values)
175+
elif ',' in roll_str:
176+
# multiple values
177+
for partial in roll_str.split(','):
178+
if '-' in partial:
179+
# a span of comma-separated values
180+
minimum,maximum = (int(number) for number in partial.split('-'))
181+
values = list(range(minimum,maximum+1))
182+
possible_rolls += values
183+
else:
184+
# a single comma-separated value
185+
values = int(partial)
186+
possible_rolls.append(values)
187+
elif '-' in roll_str:
188+
# a span of values
189+
minimum,maximum = (int(number) for number in roll_str.split('-'))
190+
values = list(range(minimum,maximum+1))
191+
possible_rolls += values
192+
193+
if len(possible_rolls) < len(dice_span):
194+
print('ERROR: Too few possible rolls in ' + str(rolls) + ' for event ' + event)
195+
error_flag = True
196+
elif len(possible_rolls) > len(dice_span):
197+
print('ERROR: Too many possible rolls in ' + str(rolls) + ' for event ' + event)
198+
error_flag = True
199+
elif sorted(possible_rolls) != dice_span:
200+
print('ERROR: possible rolls (' + possible_rolls + ') does not match dice span (' + dice_span + ') for event ' + event)
201+
error_flag = True
202+
203+
if 'next' in event_dict:
204+
if event_dict['next'] not in events:
205+
print('ERROR: next event (' + event_dict['next'] + ') does not exist for event ' + event)
206+
error_flag = True
207+
208+
for roll in rolls.keys():
209+
outcome = rolls[roll]
210+
if type(outcome) is dict:
211+
for key in outcome.keys():
212+
if key != 'next':
213+
print('ERROR: Invalid keyword (' + key + ') for roll ' + str(roll) + ' in event ' + event)
214+
error_flag = True
215+
else:
216+
outcome = outcome['next']
217+
218+
if '<NPC' in outcome:
219+
npc_match = re.match(r'(.*)<NPC\ *(.+)>\ *(.+)', outcome)
220+
outcome = npc_match.group(1) + npc_match.group(3)
221+
222+
if '<ROLL X' in outcome:
223+
roll_x = re.match(r'(.*)<ROLL\ *X([0-9]+)>\ *(.+)', outcome)
224+
outcome = roll_x.group(1) + roll_x.group(3)
225+
reroll_list.append(outcome)
226+
227+
if outcome not in events:
228+
print('ERROR: next event (' + outcome + ') does not exist for roll ' + str(roll) + ' in event ' + event)
229+
error_flag = True
230+
231+
elif type(outcome) is str:
232+
if '<NPC' in outcome:
233+
npc_match = re.match(r'(.*)<NPC\ *(.+)>\ *(.+)', outcome)
234+
outcome = npc_match.group(1) + npc_match.group(3)
235+
236+
if '<ROLL X' in outcome:
237+
roll_x = re.match(r'(.*)<ROLL\ *X([0-9]+)>\ *(.+)', outcome)
238+
outcome = roll_x.group(1) + roll_x.group(3)
239+
reroll_list.append(outcome)
240+
241+
else:
242+
print('ERROR: Outcome is neither a next key-value pair nor a string for roll ' + str(roll) + ' in event ' + event)
243+
error_flag = True
244+
245+
reroll_set = set(reroll_list)
246+
for event in events:
247+
event_dict = tree[event]
248+
if 'reroll' in event_dict:
249+
reroll_event = event_dict['reroll']
250+
if reroll_event in reroll_set:
251+
reroll_set.remove(reroll_event)
252+
else:
253+
print('ERROR: reroll event (' + reroll_event + ') does not exist in any <ROLL X#> tag for event ' + event)
254+
error_flag = True
255+
256+
if len(reroll_set) != 0:
257+
print('ERROR: Unmatched reroll(s) among all <ROLL X#> tags: ' + str(reroll_set))
258+
error_flag = True
259+
260+
if error_flag:
261+
return False
262+
else:
263+
print('Valid History YAML!')
264+
return True
265+
266+
9267
def setup_skillstats(yaml_file):
10268
# needs error handling
11269
with open(yaml_file,'r') as yamlfile:
@@ -238,15 +496,29 @@ def setup_history(yaml_file):
238496

239497

240498
if __name__ == '__main__':
241-
skillstats_yaml = 'Examples/classless_cyberpunk_test/classless_cyberpunk_test_stats_skills.yaml'
242-
history_yaml = 'Examples/classless_cyberpunk_test/classless_cyberpunk_test_history.yaml'
243-
244-
# ONE-TIME roles, stats, and skills definitions ONLY HAPPENS ONCE
245-
# This should only be run once
246-
# If it fails somehow, you should empty your database, adjust your YAML's, and try again
247-
setup_skillstats(skillstats_yaml) # comment this out when you're done with it
248-
249-
# ONE-TIME history events and rolls definitions ONLY HAPPENS ONCE
250-
# This should only be run once
251-
# If it fails somehow, you should empty your database, adjust your YAML's, and try again
252-
setup_history(history_yaml) # comment this out when you're done with it
499+
skillstats_yaml = 'Examples/system_stats_skills.yaml'
500+
history_yaml = 'Examples/system_history.yaml'
501+
502+
valid_skillstats = validate_skillstats(skillstats_yaml)
503+
valid_history = validate_history(history_yaml)
504+
505+
if not valid_skillstats:
506+
print('Skils & Stats YAML had at least one ERROR, review this output and correct all errors')
507+
508+
if not valid_history:
509+
print('History YAML had at least one ERROR, review this output and correct all errors')
510+
511+
if not valid_skillstats or not valid_history:
512+
print('Quitting without setting up game system database')
513+
else:
514+
print('Setting up game system database')
515+
# ONE-TIME roles, stats, and skills definitions ONLY HAPPENS ONCE
516+
# This should only be run once
517+
# If it fails somehow, you should empty your database, adjust your YAML's, and try again
518+
setup_skillstats(skillstats_yaml) # comment this out when you're done with it
519+
520+
# ONE-TIME history events and rolls definitions ONLY HAPPENS ONCE
521+
# This should only be run once
522+
# If it fails somehow, you should empty your database, adjust your YAML's, and try again
523+
setup_history(history_yaml) # comment this out when you're done with it
524+
print('Successfully completed setup.py! Proceed to makecharacter.py...')

0 commit comments

Comments
 (0)