Skip to content

Commit 6b52794

Browse files
committed
Simplify all the things
1 parent ae9ffbf commit 6b52794

3 files changed

Lines changed: 180 additions & 91 deletions

File tree

src/shellcraft/core.py

Lines changed: 128 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import os
55
from builtins import str
66
from copy import copy
7+
from typing import Any, Dict, List, Optional, Union
78

89
import toml
10+
from pydantic import BaseModel, Field, root_validator
911

1012
from shellcraft.utils import to_float, to_list
1113

@@ -119,86 +121,136 @@ def add(self, item):
119121
return new_item
120122

121123

122-
class BaseItem(object):
123-
"""Abstract class for new items."""
124+
class ResourceAmounts(BaseModel):
125+
clay: int = 0
126+
ore: int = 0
127+
energy: int = 0
124128

125-
_pb = None
126-
"""This links the item to a serializable Protobuf message that is part of the GameState."""
129+
def get(self, resource: str, default: int = 0) -> int:
130+
return getattr(self, resource, default)
127131

128-
def __init__(self, name):
129-
"""Initialise an empty item."""
130-
self.name = name
131-
self.difficulty = 0
132-
self.description = ""
133-
self.prerequisites = {}
134-
self.cost = {}
135-
self.effects = {}
136-
self.strings = {}
137-
138-
@classmethod
139-
def from_dict(cls, name, data):
140-
"""Load an item from dict representation."""
141-
item = cls(name)
142-
143-
item.description = data.get("description", "")
144-
item.difficulty = data.get("difficulty", 0)
145-
146-
item.prerequisites = data.get("prerequisites", {})
147-
item.prerequisites["items"] = to_list(item.prerequisites.get("items"))
148-
item.prerequisites["research"] = to_list(item.prerequisites.get("research"))
149-
item.prerequisites["triggers"] = to_list(item.prerequisites.get("triggers"))
150-
item.cost = data.get("cost", {})
151-
item.strings = data.get("strings", {})
152-
item.effects = data.get("effects", {})
153-
for effect in (
132+
def __iter__(self):
133+
yield ("clay", self.clay)
134+
yield ("ore", self.ore)
135+
yield ("energy", self.energy)
136+
137+
138+
class Prerequisites(ResourceAmounts):
139+
items: List[str] = Field(default_factory=list)
140+
research: List[str] = Field(default_factory=list)
141+
triggers: List[str] = Field(default_factory=list)
142+
143+
@root_validator(pre=True)
144+
def wrap_in_list(cls, values):
145+
for field in ["items", "research", "triggers"]:
146+
if field in values and not isinstance(values[field], (list, tuple)):
147+
values[field] = [values[field]]
148+
return values
149+
150+
151+
class Effects(ResourceAmounts):
152+
enable_commands: List[str] = Field(default_factory=list)
153+
enable_items: List[str] = Field(default_factory=list)
154+
enable_resources: List[str] = Field(default_factory=list)
155+
enable_research: List[str] = Field(default_factory=list)
156+
events: List[str] = Field(default_factory=list)
157+
triggers: List[str] = Field(default_factory=list)
158+
159+
# Resources to be granted
160+
clay: int = 0
161+
ore: int = 0
162+
energy: int = 0
163+
164+
@root_validator(pre=True)
165+
def wrap_in_list(cls, values):
166+
for field in [
154167
"enable_commands",
155168
"enable_items",
156169
"enable_resources",
170+
"enable_research",
157171
"events",
158172
"triggers",
159-
):
160-
item.effects[effect] = to_list(item.effects.get(effect))
161-
return item
173+
]:
174+
if field in values and not isinstance(values[field], (list, tuple)):
175+
values[field] = [values[field]]
176+
return values
177+
178+
179+
class BaseItem(BaseModel):
180+
"""Pydantic base class for game items."""
181+
182+
name: str
183+
difficulty: float = 0
184+
description: str = ""
185+
prerequisites: Prerequisites = Field(default_factory=Prerequisites)
186+
effects: Effects = Field(default_factory=Effects)
187+
cost: ResourceAmounts = Field(default_factory=ResourceAmounts)
188+
strings: Dict[str, str] = Field(default_factory=dict)
189+
190+
# Use model_config to allow arbitrary types
191+
model_config = {"arbitrary_types_allowed": True}
192+
193+
def __init__(self, name: str = "", **data):
194+
"""Initialise an item."""
195+
if name:
196+
data["name"] = name
197+
super().__init__(**data)
198+
# Set _pb as a private attribute after initialization
199+
self._pb = None
200+
201+
@classmethod
202+
def from_dict(cls, name, data):
203+
"""Load an item from dict representation."""
204+
# Process prerequisites
205+
data["name"] = name
206+
return cls.model_validate(data)
162207

163208
def __repr__(self):
164209
"""String representation of the item."""
165-
return self.name
210+
return str(self.name)
211+
212+
def __str__(self):
213+
"""String representation of the item."""
214+
return str(self.name)
166215

167216
def __setattr__(self, key, value):
168-
"""Override attribute setter for items with attached message (Protobuf or Pydantic)."""
169-
if key != "_pb" and self._pb is not None:
217+
"""Override attribute setter to sync with attached storage model (_pb)."""
218+
# Let Pydantic handle its own fields first
219+
super().__setattr__(key, value)
220+
221+
# Also sync to _pb if it exists and the key is a serializable field
222+
if hasattr(self, "_pb") and self._pb is not None and key != "_pb":
170223
# Check if it's a protobuf object
171224
if hasattr(self._pb.__class__, "DESCRIPTOR"):
172225
if key in self._pb.__class__.DESCRIPTOR.fields_by_name.keys():
173226
setattr(self._pb, key, value)
174-
else:
175-
self.__dict__[key] = value
176227
# Check if it's a Pydantic model
177228
elif hasattr(self._pb, "model_fields"):
178229
if key in self._pb.model_fields:
179230
setattr(self._pb, key, value)
180-
else:
181-
self.__dict__[key] = value
182-
else:
183-
self.__dict__[key] = value
184-
else:
185-
self.__dict__[key] = value
186-
187-
def __getattr__(self, key):
188-
"""Override attribute getter for items with attached message (Protobuf or Pydantic)."""
189-
if key != "_pb" and self._pb:
190-
# Check if it's a protobuf object
191-
if hasattr(self._pb.__class__, "DESCRIPTOR"):
192-
if key in self._pb.__class__.DESCRIPTOR.fields_by_name.keys():
193-
return getattr(self._pb, key)
194-
# Check if it's a Pydantic model
195-
elif hasattr(self._pb, "model_fields"):
196-
if key in self._pb.model_fields:
197-
return getattr(self._pb, key)
198-
raise AttributeError(key)
199231

200-
201-
class BaseFactory(object):
232+
def __getattribute__(self, key):
233+
"""Override attribute getter with fallback to storage model (_pb) if needed."""
234+
try:
235+
# Try to get from the Pydantic model first
236+
return super().__getattribute__(key)
237+
except AttributeError:
238+
# If attribute doesn't exist on the Pydantic model, try _pb
239+
if hasattr(self, "_pb") and self._pb and key != "_pb":
240+
# Check if it's a protobuf object
241+
if hasattr(self._pb.__class__, "DESCRIPTOR"):
242+
if key in self._pb.__class__.DESCRIPTOR.fields_by_name.keys():
243+
return getattr(self._pb, key)
244+
# Check if it's a Pydantic model
245+
elif hasattr(self._pb, "model_fields"):
246+
if key in self._pb.model_fields:
247+
return getattr(self._pb, key)
248+
raise AttributeError(
249+
f"'{self.__class__.__name__}' object has no attribute '{key}'"
250+
)
251+
252+
253+
class BaseFactory:
202254
"""Factory pattern to instantiate items."""
203255

204256
FIXTURES = "collection.toml"
@@ -220,7 +272,7 @@ def __init__(self, game):
220272
}
221273
self.game = game
222274

223-
def get(self, item_name):
275+
def get(self, item_name) -> Optional[BaseItem]:
224276
"""Get an item instance by name."""
225277
if isinstance(item_name, BaseItem):
226278
return item_name
@@ -251,15 +303,19 @@ def available_items(self):
251303

252304
def _resources_missing_to_craft(self, item_name):
253305
item = self.get(item_name)
306+
assert item
307+
254308
return {
255309
res: int(res_cost - self.game.resources.get(res))
256-
for res, res_cost in item.cost.items()
310+
for res, res_cost in item.cost
257311
if res_cost - self.game.resources.get(res) > 0
258312
}
259313

260314
def can_afford(self, item_name):
261315
"""Return true if we have enough resources to create an item."""
262316
item = self.get(item_name)
317+
assert item
318+
263319
for resource in RESOURCES:
264320
if item.cost.get(resource, 0) > self.game.resources.get(resource):
265321
return False
@@ -269,33 +325,34 @@ def can_afford(self, item_name):
269325
def apply_effects(self, item_name):
270326
"""Apply all effects of an item."""
271327
item = self.get(item_name)
328+
assert item
272329

273330
# Enable commands
274-
for command in item.effects.get("enable_commands", []):
331+
for command in item.effects.enable_commands:
275332
if command not in self.game.state.commands_enabled:
276333
self.game.alert("You unlocked the `{}` command", command)
277334
self.game.state.commands_enabled.append(command)
278335

279336
# Enable resouces
280-
for resources in item.effects.get("enable_resources", []):
337+
for resources in item.effects.enable_resources:
281338
if resources not in self.game.state.resources_enabled:
282339
self.game.alert("You can now mine *{}*.", resources)
283340
self.game.state.resources_enabled.append(resources)
284341

285342
# Enable items
286-
for item_name in item.effects.get("enable_items", []):
343+
for item_name in item.effects.enable_items:
287344
if item_name not in self.game.state.tools_enabled:
288345
self.game.alert("You can now craft ${}$.", item_name)
289346
self.game.state.tools_enabled.append(item_name)
290347

291348
# Enable research
292-
for research in item.effects.get("enable_research", []):
349+
for research in item.effects.enable_research:
293350
if research not in self.game.state.research_enabled:
294351
self.game.alert("You can now research @{}@.", research)
295352
self.game.state.research_enabled.append(research)
296353

297354
# Trigger flags
298-
for trigger in item.effects.get("triggers", []):
355+
for trigger in item.effects.triggers:
299356
if trigger not in self.game.state.triggers:
300357
self.game.state.triggers.append(trigger)
301358

@@ -311,7 +368,7 @@ def apply_effects(self, item_name):
311368

312369
# Change mining difficulty
313370
for resource in RESOURCES:
314-
change = item.effects.get(f"{resource}_mining_difficulty", None)
371+
change = getattr(item.effects, f"{resource}_mining_difficulty", None)
315372
if change:
316373
change = to_float(change)
317374
self.game.mining_difficulty.multiply(resource, 1 - change)
@@ -320,7 +377,7 @@ def apply_effects(self, item_name):
320377
)
321378

322379
# Trigger events
323-
self.game.events.trigger(*item.effects.get("events", []))
380+
self.game.events.trigger(*item.effects.events)
324381

325382
def is_available(self, item_name):
326383
"""Return true if the prerequisites for an item are met."""
@@ -330,13 +387,13 @@ def is_available(self, item_name):
330387
for resource in RESOURCES:
331388
if self.game.resources.get(resource) < item.prerequisites.get(resource, 0):
332389
return False
333-
for required_item in item.prerequisites["items"]:
390+
for required_item in item.prerequisites.items:
334391
if not self.game.has_item(required_item):
335392
return False
336-
for research in item.prerequisites["research"]:
393+
for research in item.prerequisites.research:
337394
if research not in self.game.state.research_completed:
338395
return False
339-
for trigger in item.prerequisites["triggers"]:
396+
for trigger in item.prerequisites.triggers:
340397
if trigger not in self.game.state.triggers:
341398
return False
342399
return True

src/shellcraft/shellcraft.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
# -*- coding: utf-8 -*-
22
"""Game Classes."""
33

4-
from shellcraft.tools import ToolFactory
5-
from shellcraft.research import ResearchFactory
6-
from shellcraft.tutorial import TutorialFactory
4+
import datetime
5+
import os
6+
from random import random
7+
8+
from shellcraft.core import ItemProxy, ResourceProxy
79
from shellcraft.events import EventFactory
8-
from shellcraft.missions import MissionFactory
9-
from shellcraft.fractions import FractionProxy
1010
from shellcraft.exceptions import BusyException
11-
11+
from shellcraft.fractions import FractionProxy
1212
from shellcraft.game_state import GameState
13-
from shellcraft.core import ResourceProxy, ItemProxy
14-
15-
from random import random
16-
import os
17-
import datetime
13+
from shellcraft.missions import MissionFactory
14+
from shellcraft.research import ResearchFactory
15+
from shellcraft.tools import ToolFactory
16+
from shellcraft.tutorial import TutorialFactory
1817

1918

2019
class Game(object):
@@ -95,7 +94,9 @@ def complete_missions(self):
9594
def craft(self, tool_name):
9695
"""Craft a new tool by expending resources and time."""
9796
item = self.workshop.get(tool_name)
98-
for resource, res_cost in item.cost.items():
97+
assert item
98+
99+
for resource, res_cost in item.cost:
99100
self.resources.add(resource, -res_cost)
100101
self.tools.add(item)
101102
self._act("craft", tool_name, item.difficulty)
@@ -105,6 +106,8 @@ def craft(self, tool_name):
105106
def research(self, project_name):
106107
"""Research a new project by expending time."""
107108
project = self.lab.get(project_name)
109+
assert project
110+
108111
self.state.research_completed.append(project.name)
109112
self._act("research", project_name, project.difficulty)
110113
self.alert("Researched {}.", project)
@@ -196,10 +199,11 @@ def _act(self, task, target, duration):
196199
duration = 0
197200

198201
from shellcraft.game_state import Action
202+
199203
self.state.action = Action(
200204
task=task,
201205
target=str(target),
202-
completion=datetime.datetime.now() + datetime.timedelta(seconds=duration)
206+
completion=datetime.datetime.now() + datetime.timedelta(seconds=duration),
203207
)
204208

205209
@classmethod

0 commit comments

Comments
 (0)