Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 13
feat: add some basic classes#13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base:dev
Are you sure you want to change the base?
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
c54cc2d9a3d3562ae83b790223bc078158b99413f3d768341732a873c93dc5626c31cfFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # | ||
| from itertools import chain | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer importing full modules. E.g.: importitertoolsThis helps in understanding where imported obhects come from. fromtypingimportUnionWrong: importtypingThis helps in keeping type signatures readable. | ||
| _dependencies = {} | ||
| _dependent_vars = set() | ||
| def addDependency(dep1: str, dep2: str): | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Functions, variables and also modules should be named in | ||
| global _dependencies, _dependent_vars | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid global variables where possible. Generally, you can always rewrite your code to share objects by other means instead. That said, in this case this seems fine for a prototype. Note that you only need to declare variables as global of you want to change them globally from a local scope. You can always read variables from outer scopes, so it is unnecessary to declare | ||
| if dep1 in _dependencies.keys(): | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When iterating over a | ||
| if not dep2 in _dependencies[dep1]: | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Membership tests should use ifdep2notin_dependencies[dep1]: | ||
| _dependencies[dep1].append(dep2) | ||
| #print(f"Appending DEP: {dep1} -> {dep2}") | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid commiting commented out code. It does not age well. In fact, it doesn't age at all. 😉 | ||
| else: | ||
| _dependencies[dep1] = [dep2] | ||
| #print(f"Adding DEP: {dep1} -> {dep2}") | ||
| _dependent_vars = set(chain.from_iterable(_dependencies.values())) | ||
| #print("DV: \n", _dependent_vars) | ||
Comment on lines
+8
to
+18
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Starting from this code, defadd_dependency(dep1: str, dep2: str) ->None:
global_dependent_varsifdep1in_dependencies:
ifdep2notin_dependencies[dep1]:
_dependencies[dep1].append(dep2)
else:
_dependencies[dep1] = [dep2]
_dependent_vars=set(itertools.chain.from_iterable(_dependencies.values()))we could refactor it like so: defadd_dependency(dep1: str, dep2: str) ->None:
global_dependent_varsifdep1notin_dependencies:
_dependencies[dep1] = []
ifdep2notin_dependencies[dep1]:
_dependencies[dep1].append(dep2)
_dependent_vars=set(itertools.chain.from_iterable(_dependencies.values()))Which makes it more obvious that we want to use a importcollections_dependencies=collections.defaultdict(list)
defadd_dependency(dep1: str, dep2: str) ->None:
global_dependent_varsifdep2notin_dependencies[dep1]:
_dependencies[dep1].append(dep2)
_dependent_vars=set(itertools.chain.from_iterable(_dependencies.values()))Now, since our dependencies don't need to be ordered, we can change the default importcollections_dependencies=collections.defaultdict(set)
defadd_dependency(dep1: str, dep2: str) ->None:
global_dependent_vars_dependencies[dep1].add(dep2)
_dependent_vars=set(itertools.chain.from_iterable(_dependencies.values())) | ||
| def getDependentVars(): | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid methods and functions starting with | ||
| return _dependent_vars | ||
| def getDependencies(): | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of having these "getter" functions, it's preferable to have public module members and refactor the code if you would need to change them significantly. Refactoring code is easy in Python, so "future proofing" of this kind is not needed and detrimental to code clarity. | ||
| return _dependencies | ||
| class Dependency: | ||
| def _reset_dependent_vars(self, name: str): | ||
| #print(f"DEP\n{_dependencies}") | ||
| for var in _dependencies[name]: | ||
| #print(f"Resetting: {var} (due to {name})") | ||
| try: | ||
| super().__delattr__(f"{var}") | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
| ||
| except: | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bare except clauses are an antipattern. They mask real errors that affect other parts of the program, since all exceptions are captured, even ones we're not interested in. exceptAttributeError: | ||
| #print(f"FAIL") | ||
| pass | ||
| if var in _dependencies: | ||
| self._reset_dependent_vars(var) | ||
| def __setattr__(self, name: str, value): | ||
| global _dependencies, _dependent_vars | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, these globals are not needed. | ||
| if name in _dependent_vars: | ||
| raise AttributeError("Cannot set this value.") | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This error would be more helpful if it specified the attribute that cannot be set. | ||
| if name in _dependencies: | ||
| self._reset_dependent_vars(name) | ||
| name = f"_{name}" | ||
| #print(f"Setting {name} to {value}") | ||
| super().__setattr__(name, value) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| # | ||
| from Modifiers import Modifier | ||
| class ModifierDatabase: | ||
| def __init__(self, owner = None): | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
| ||
| self.owner = owner | ||
| self.clear() | ||
| def addEntry(self, entry: Modifier): | ||
| name = entry.name | ||
| type = entry.type | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
| ||
| if name not in self.db.keys(): | ||
| self.db[name] = dict() | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer literals where possible. In this case: self.db[name] = {} | ||
| if type not in self.db[name].keys(): | ||
| self.db[name][type] = [entry] | ||
| else: | ||
| if not entry in self.db[name][type]: | ||
| self.db[name][type].append(entry) | ||
Comment on lines
+10
to
+19
| ||
| def getBase(self, name: str): | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To come back to this after reviewing the Implementation: importmathfromtypingimportUniondefbase(self, name: str) ->int:
returnsum(entry.valueforentryinself.db[name]["BASE"])
defflat(self, name: str) ->int:
returnsum(entry.valueforentryinself.db[name]["FLAT"])
definc(self, name: str) ->Union[float, int]:
returnsum((entry.valueforentryinself.db[name]["INC"]), 1) /100defmore(self, name: str) ->Union[float, int]:
returnmath.prod(1+entry.value/100forentryinself.db[name]["MORE"])
``` | ||
| retVal = 0 | ||
| if name in self.db.keys() and "BASE" in self.db[name].keys(): | ||
| for entry in self.db[name]["BASE"]: | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stringly typed variables are a problem for gradually typed code bases. Consider refactoring "BASE", "FLAT", "INC" and "MORE" into an enum. | ||
| retVal += entry.getValue(self.owner) | ||
| return retVal | ||
| return 0 | ||
Comment on lines
+21
to
+27
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we have defbase(self, name: str) ->int:
returnsum(entry.valueforentryinself.db[name]["BASE"]) | ||
| def getFlat(self, name: str): | ||
| retVal = 0 | ||
| if name in self.db.keys() and "FLAT" in self.db[name].keys(): | ||
| for entry in self.db[name]["FLAT"]: | ||
| retVal += entry.getValue(self.owner) | ||
| return retVal | ||
| return 0 | ||
| def getInc(self, name: str): | ||
| retVal = 0 | ||
| if name in self.db.keys() and "INC" in self.db[name].keys(): | ||
| for entry in self.db[name]["INC"]: | ||
| retVal += entry.getValue(self.owner) | ||
| return retVal | ||
| return retVal | ||
| def getMore(self, name: str): | ||
| retVal = 0 | ||
| if name in self.db.keys() and "MORE" in self.db[name].keys(): | ||
| for entry in self.db[name]["MORE"]: | ||
| retVal += entry.getValue(self.owner) | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For "more" multipliers (hence the name) you need to calculate the product, not the sum. | ||
| return retVal | ||
| return retVal | ||
| def clear(self): | ||
| self.db = dict() | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Always initialise instance attributes in def__init__(self, owner: Any=None) ->None:
self.owner=ownerself.db= {}
defclear(self) ->None:
self.db.clear() | ||
| def test(): | ||
| db = ModifierDatabase() | ||
| mod1 = Modifier("Health", "BASE", 12, "", tags = { "type": "Multiplier", "var": "Level" }) | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There should be no whitespace around kwargs bindings. | ||
| mod2 = Modifier("Health", "BASE", 13, "") | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From an usability perspective, it would be nice to provide the source as a default parameter, making it optional. | ||
| db.addEntry(mod1) | ||
| db.addEntry(mod2) | ||
| import pprint | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Import statements should be at the top of the respective scope, so here this should go directly after the function signature. | ||
| pprint.pprint(db.db) | ||
| print("BASE: " + str(db.getBase("Health"))) | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use f-strings for formatting here. This also allows to get rid of the explicit string conversion. | ||
| if __name__ == "__main__": | ||
| test() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # | ||
| from Dependency import addDependency, _dependencies | ||
| from math import floor | ||
| class Modifier: | ||
| def __init__(self, name: str, type: str, value, source: str, conditionals: set = set(), tags: dict = {}): | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function signature doesn't do what you think it does. In Python, function signatures are run once at the time of creation. This means that after that, if you have mutable default arguments, all invocations of the functions share their mutable arguments, leading to hard to find bugs. Canonically, you would solve this by providing a sentinel default value, e.g.: fromtypingimportOptionaldef__init__(self, conditionals: Optional[set[...]] =None, tags: Optional[dict[..., ...]] =None) ->None:
ifconditionalsisNone:
conditionals=set()
iftagsisNone:
tags= {} | ||
| self.name = name | ||
| self.type = type | ||
| self.value = value | ||
| self.source = source | ||
| self.conditionals = conditionals | ||
| self.tags = tags | ||
| self.process() | ||
Comment on lines
+6
to
+14
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since all this initialiser does is set some attributes, this is a perfect use case for importdataclassesfromtypingimportUnion@dataclasses.dataclassclassModifier:
name: strtype_: strvalue: Union[float, int]
source: str=""conditionals: set=dataclasses.field(default_factory=set)
tags: dict=dataclasses.field(default_factory=dict)
def__post_init__(self) ->None:
self.process()Note the | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since modifiers in our mod DB are always unique by ID and do not need to be ordered, we could simplify the code in Since we have decided that modifiers should always be inequal to every other modifier than themselves, this is possible to implement correctly even though classModifier:
...
def__eq__(self, other: Modifier) ->bool:
returnselfisotherdef__hash__(self) ->int:
returnid(self)This allows us to turn mod_db.ModifierDatabase into: classModifierDatabase:
def__init__(self) ->None:
self.db=collections.defaultdict(lambda: collections.defaultdict(set))
defadd_entry(self, entry: modifiers.Modifier) ->None:
name=entry.nametype_=entry.type_self.db[name][type_].add(entry)and subsequently: defadd_entry(self, entry: modifiers.Modifier) ->None:
self.db[entry.name][entry.type_].add(entry) | ||
| def process(self): | ||
| if 'type' in self.tags.keys() and self.tags['type'] == "Multiplier": | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Python, prefer EAFP (easier to ask for forgiveness than permission) over LBYL (look before you leap). So instead of checking whether a key exists, just try to access it and handle the exception if it doesnt exist. | ||
| addDependency(self.tags['var'].lower(), f"{self.type.lower()}_{self.name.lower()}") | ||
| addDependency(f"{self.type.lower()}_{self.name.lower()}", f"max_{self.name.lower()}") | ||
| #print(self) | ||
| def getValue(self, caller = None): | ||
| if caller: | ||
| #print(f"getting var: {self.name}:{self.type}") | ||
| if 'type' in self.tags.keys() and self.tags['type'] == "Multiplier": | ||
| var = f"{self.tags['var'].lower()}" | ||
| l = caller.__getattribute__(var) | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid variable names like lowercase "L" that could be mistaken for other characters, depending on the font used. | ||
| #print(f"{var}: {l}") | ||
| return floor(self.value * l) | ||
| return self.value | ||
| return self.value | ||
| def __repr__(self): | ||
| ret = f"{self.name}:{self.type}\n" | ||
| ret += f"Value: {self.value}\n" | ||
| ret += f"{self.tags} -- {self.source}" | ||
| return ret | ||
Comment on lines
+34
to
+37
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can take advantage of implicit string concatenation here. | ||
| def test(): | ||
| m = Modifier("Health", "BASE", 12, "", tags = { "type": "Multiplier", "var": "Level" }) | ||
| print(m) | ||
| if __name__ == "__main__": | ||
| test() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| # | ||
| from functools import cached_property | ||
| from math import floor | ||
| from Dependency import Dependency, getDependencies | ||
| from ModDB import ModifierDatabase | ||
| from Modifiers import Modifier | ||
| class Player(Dependency): | ||
| def __init__(self, level, strength): | ||
| self.modDB = ModifierDatabase(self) | ||
| self._level = level | ||
| self.addMod(Modifier("Health", "BASE", 12, "Base Per Level", tags = { "type": "Multiplier", "var": "Level" })) | ||
| self.addMod(Modifier("Health", "BASE", 0.5, "Base Per Strength", tags = { "type": "Multiplier", "var": "Max_Strength"})) | ||
| self.addMod(Modifier("Strength", "BASE", strength, "Starting")) | ||
| def addMod(self, mod: Modifier): | ||
| self.modDB.addEntry(mod) | ||
| attr = f"{mod.type.lower()}_{mod.name.lower()}" | ||
| try: | ||
| self.__delattr__(f"{attr}") | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, | ||
| except: | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, avoid bare | ||
| #print("FAIL") | ||
| pass | ||
| if attr in getDependencies(): | ||
| self._reset_dependent_vars(attr) | ||
| @cached_property | ||
| def base_health(self): | ||
| #ret = 38 + self.level * 12 + floor(self.max_strength / 2) + self.flat_health | ||
| ret = 38 + self.modDB.getBase("Health") + self.flat_health | ||
| print(f"BASE Health calculated: {ret}") | ||
| return ret | ||
| @cached_property | ||
| def max_health(self): | ||
| ret = floor(self.base_health * (1 + self.inc_health / 100) * (1 + self.more_health / 100)) | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Summing up all like modifiers looks more like a responsibility of the mod DB than the | ||
| print(f"Total Health calculated: {ret}") | ||
| return ret | ||
| @cached_property | ||
| def base_strength(self): | ||
| ret = self.modDB.getBase("Strength") | ||
| print(f"BASE Strength calculated: {ret}") | ||
| return ret | ||
| @cached_property | ||
| def max_strength(self): | ||
| ret = floor((self.base_strength + self.flat_strength) * (1 + self.inc_strength / 100) * (1 + self.more_strength / 100)) | ||
| print(f"Total Strength calculated: {ret}") | ||
| return ret | ||
| @property | ||
| def level(self): | ||
| return self._level | ||
| @cached_property | ||
| def flat_health(self): | ||
| ret = self.modDB.getFlat("Health") | ||
| print(f"FLAT Health calculated: {ret}") | ||
| return ret | ||
| @cached_property | ||
| def more_health(self): | ||
| ret = self.modDB.getMore("Health") | ||
| print(f"MORE Health calculated: {ret}") | ||
| return ret | ||
| @cached_property | ||
| def inc_health(self): | ||
| ret = self.modDB.getInc("Health") | ||
| print(f"INC Health calculated: {ret}") | ||
| return ret | ||
| @cached_property | ||
| def flat_strength(self): | ||
| ret = self.modDB.getFlat("Strength") | ||
| print(f"FLAT Strength calculated: {ret}") | ||
| return ret | ||
| @cached_property | ||
| def more_strength(self): | ||
| ret = self.modDB.getMore("Strength") | ||
| print(f"MORE Strength calculated: {ret}") | ||
| return ret | ||
| @cached_property | ||
| def inc_strength(self): | ||
| ret = self.modDB.getInc("Strength") | ||
| print(f"INC Strength calculated: {ret}") | ||
| return ret | ||
| def test(): | ||
| player = Player(1, 20) | ||
| #print(f"{player.base_health}\n") | ||
| print(f"{player.max_health}\n") | ||
| player.level = 5 | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will shadow the @level.setterdeflevel(self, value: int) ->None:
self._level=value | ||
| print(f"{player.max_health}\n") | ||
| player.addMod(Modifier("Health", "MORE", 100, "")) | ||
| print(f"{player.max_health}\n") | ||
| player.addMod(Modifier("Strength", "FLAT", 100, "")) | ||
| player.addMod(Modifier("Strength", "INC", 30, "")) | ||
| player.addMod(Modifier("Strength", "MORE", 15, "")) | ||
| player.addMod(Modifier("Health", "FLAT", 500, "")) | ||
| #print(f"{player.max_strength}\n") | ||
| print(f"{player.max_health}\n") | ||
| if __name__ == "__main__": | ||
| test() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does every file begin with "#" here?