Browse Source

Merge branch 'feature/select_troops'

Bastien Sevajol 5 years ago
parent
commit
36f94e3246
42 changed files with 1192 additions and 89 deletions
  1. 4 2
      README.md
  2. 6 0
      config.yaml
  3. 4 0
      opencombat/exception.py
  4. 0 0
      opencombat/game/__init__.py
  5. 8 8
      opencombat/game/actor.py
  6. 0 0
      opencombat/game/animation.py
  7. 11 11
      opencombat/game/base.py
  8. 0 0
      opencombat/game/const.py
  9. 1 1
      opencombat/game/fire.py
  10. 2 2
      opencombat/game/image.py
  11. 0 0
      opencombat/game/move.py
  12. 0 0
      opencombat/game/placement.py
  13. 0 0
      opencombat/game/state.py
  14. 3 3
      opencombat/game/weapon.py
  15. 17 0
      opencombat/gui.py
  16. 11 60
      opencombat/state.py
  17. 1 0
      opencombat/strategy/__init__.py
  18. 45 0
      opencombat/strategy/manager.py
  19. 1 0
      opencombat/strategy/selection/__init__.py
  20. 207 0
      opencombat/strategy/selection/gui.py
  21. 1 0
      opencombat/strategy/team/__init__.py
  22. 34 0
      opencombat/strategy/team/model.py
  23. 114 0
      opencombat/strategy/team/stash.py
  24. 37 0
      opencombat/strategy/teams.xml
  25. 33 0
      opencombat/strategy/teams.xsd
  26. 111 0
      opencombat/strategy/troops.py
  27. 3 0
      opencombat/strategy/troops_template.xml
  28. 1 0
      opencombat/strategy/unit/__init__.py
  29. 34 0
      opencombat/strategy/unit/model.py
  30. 83 0
      opencombat/strategy/unit/stash.py
  31. 11 0
      opencombat/strategy/units.xml
  32. 21 0
      opencombat/strategy/units.xsd
  33. 1 1
      opencombat/terminal/base.py
  34. 75 0
      opencombat/xml.py
  35. 3 1
      run.py
  36. 70 0
      select_troops.py
  37. 10 0
      test_config.yaml
  38. 37 0
      tests/fixtures/teams.xml
  39. 11 0
      tests/fixtures/units.xml
  40. 0 0
      tests/strategy/__init__.py
  41. 107 0
      tests/strategy/test_teams.py
  42. 74 0
      tests/strategy/test_units.py

+ 4 - 2
README.md View File

@@ -10,7 +10,7 @@ Open source close combat inspired game. Presentation here: http://www.closecomba
10 10
 
11 11
 Tested only under linux, debian/Ubuntu. Before install project, install OS packages:
12 12
 
13
-    build-essential libsdl1.2debian libsdl-image1.2 libsdl-image1.2-dev libsdl-ttf2.0-0 libsdl-ttf2.0-dev libsdl-mixer1.2 libsdl-mixer1.2-dev redis-server
13
+    build-essential python3-tk libsdl1.2debian libsdl-image1.2 libsdl-image1.2-dev libsdl-ttf2.0-0 libsdl-ttf2.0-dev libsdl-mixer1.2 libsdl-mixer1.2-dev redis-server
14 14
 
15 15
 Python version: 3.5+
16 16
 
@@ -31,7 +31,9 @@ You also need a running redis server (used db number is `0`, soon configurable).
31 31
 
32 32
 ## Troops selection
33 33
 
34
-Not developed yet
34
+Start troops selection GUI with:
35
+
36
+    python select_troops.py --country URSS --country DE
35 37
 
36 38
 ## Troops Placement phase
37 39
 

+ 6 - 0
config.yaml View File

@@ -25,6 +25,12 @@ global:
25 25
     state_dumper: "opencombat.state.StateDumper"
26 26
     state_schema: "opencombat/state.xsd"
27 27
     state_template: "opencombat/state_template.xml"
28
+    unit_stash: "opencombat.strategy.unit.stash.UnitStash"
29
+    team_stash: "opencombat.strategy.team.stash.TeamStash"
30
+    teams_schema: "opencombat/strategy/teams.xsd"
31
+    units_schema: "opencombat/strategy/units.xsd"
32
+    troop_dumper: "opencombat.strategy.troops.TroopDumper"
33
+    troop_schema: "opencombat/strategy/troops.xsd"
28 34
     cache_dir_path: 'cache'
29 35
     include_path:
30 36
       maps:

+ 4 - 0
opencombat/exception.py View File

@@ -23,3 +23,7 @@ class StateLoadError(OpenCombatException):
23 23
 
24 24
 class NotFoundError(OpenCombatException):
25 25
     pass
26
+
27
+
28
+class NotFoundException(OpenCombatException):
29
+    pass

opencombat/gui/__init__.py → opencombat/game/__init__.py View File


opencombat/gui/actor.py → opencombat/game/actor.py View File

@@ -14,17 +14,17 @@ from synergine2_xyz.exception import UnknownAnimationIndex
14 14
 from opencombat.exception import UnknownWeapon
15 15
 from opencombat.exception import WrongMode
16 16
 from opencombat.exception import UnknownFiringAnimation
17
-from opencombat.gui.animation import ANIMATION_CRAWL
18
-from opencombat.gui.animation import ANIMATION_WALK
19
-from opencombat.gui.const import MODE_MAN_STAND_UP
20
-from opencombat.gui.const import MODE_MAN_CRAWLING
21
-from opencombat.gui.image import TileImageCacheManager
22
-from opencombat.gui.weapon import RIFFLE
23
-from opencombat.gui.weapon import WeaponImageApplier
17
+from opencombat.game.animation import ANIMATION_CRAWL
18
+from opencombat.game.animation import ANIMATION_WALK
19
+from opencombat.game.const import MODE_MAN_STAND_UP
20
+from opencombat.game.const import MODE_MAN_CRAWLING
21
+from opencombat.game.image import TileImageCacheManager
22
+from opencombat.game.weapon import RIFFLE
23
+from opencombat.game.weapon import WeaponImageApplier
24 24
 from opencombat.user_action import UserAction
25 25
 
26 26
 if typing.TYPE_CHECKING:
27
-    from opencombat.gui.fire import GuiFiringEvent
27
+    from opencombat.game.fire import GuiFiringEvent
28 28
 
29 29
 
30 30
 MODE_DEFAULT = 'MODE_DEFAULT'

opencombat/gui/animation.py → opencombat/game/animation.py View File


opencombat/gui/base.py → opencombat/game/base.py View File

@@ -18,11 +18,11 @@ from synergine2_cocos2d.interaction import InteractionManager
18 18
 from synergine2_cocos2d.middleware import MapMiddleware
19 19
 from synergine2_cocos2d.util import PathManager
20 20
 
21
-from opencombat.gui.animation import ANIMATION_WALK
22
-from opencombat.gui.animation import ANIMATION_CRAWL
23
-from opencombat.gui.fire import GuiFiringEvent
24
-from opencombat.gui.placement import SetSubjectPositionsInteraction
25
-from opencombat.gui.state import SaveStateInteraction
21
+from opencombat.game.animation import ANIMATION_WALK
22
+from opencombat.game.animation import ANIMATION_CRAWL
23
+from opencombat.game.fire import GuiFiringEvent
24
+from opencombat.game.placement import SetSubjectPositionsInteraction
25
+from opencombat.game.state import SaveStateInteraction
26 26
 from opencombat.simulation.interior import InteriorManager
27 27
 from opencombat.simulation.tmx import TileMap
28 28
 from opencombat.user_action import UserAction
@@ -45,8 +45,8 @@ from opencombat.simulation.event import FireEvent
45 45
 from opencombat.simulation.event import DieEvent
46 46
 from opencombat.simulation.subject import ManSubject
47 47
 from opencombat.simulation.subject import TankSubject
48
-from opencombat.gui.actor import Man as ManActor
49
-from opencombat.gui.actor import HeavyVehicle as HeavyVehicleActor
48
+from opencombat.game.actor import Man as ManActor
49
+from opencombat.game.actor import HeavyVehicle as HeavyVehicleActor
50 50
 
51 51
 
52 52
 class EditLayer(BaseEditLayer):
@@ -290,10 +290,10 @@ class Game(TMXGui):
290 290
         )
291 291
 
292 292
     def before_run(self) -> None:
293
-        from opencombat.gui.move import MoveActorInteraction
294
-        from opencombat.gui.move import MoveFastActorInteraction
295
-        from opencombat.gui.move import MoveCrawlActorInteraction
296
-        from opencombat.gui.fire import FireActorInteraction
293
+        from opencombat.game.move import MoveActorInteraction
294
+        from opencombat.game.move import MoveFastActorInteraction
295
+        from opencombat.game.move import MoveCrawlActorInteraction
296
+        from opencombat.game.fire import FireActorInteraction
297 297
 
298 298
         self.layer_manager.interaction_manager.register(
299 299
             MoveActorInteraction,

opencombat/gui/const.py → opencombat/game/const.py View File


opencombat/gui/fire.py → opencombat/game/fire.py View File

@@ -1,7 +1,7 @@
1 1
 # coding: utf-8
2 2
 import typing
3 3
 
4
-from opencombat.gui.actor import BaseActor
4
+from opencombat.game.actor import BaseActor
5 5
 from opencombat.simulation.event import DEFAULT_WEAPON_TYPE
6 6
 from opencombat.simulation.fire import RequestFireBehaviour
7 7
 from synergine2_cocos2d.interaction import BaseActorInteraction

opencombat/gui/image.py → opencombat/game/image.py View File

@@ -12,7 +12,7 @@ from opencombat.exception import UnknownWeapon
12 12
 from opencombat.exception import UnknownFiringAnimation
13 13
 
14 14
 if typing.TYPE_CHECKING:
15
-    from opencombat.gui.actor import BaseActor
15
+    from opencombat.game.actor import BaseActor
16 16
 
17 17
 
18 18
 class FiringImageCache(ImageCache):
@@ -72,7 +72,7 @@ class TileImageCacheManager(ImageCacheManager):
72 72
     ) -> None:
73 73
         super().__init__(actor, config)
74 74
         self.firing_cache = FiringImageCache()
75
-        from opencombat.gui.actor import BaseActor
75
+        from opencombat.game.actor import BaseActor
76 76
         self.actor = typing.cast(BaseActor, self.actor)
77 77
         self.path_manager = PathManager(
78 78
             self.config.resolve('global.include_path.graphics'),

opencombat/gui/move.py → opencombat/game/move.py View File


opencombat/gui/placement.py → opencombat/game/placement.py View File


opencombat/gui/state.py → opencombat/game/state.py View File


opencombat/gui/weapon.py → opencombat/game/weapon.py View File

@@ -8,11 +8,11 @@ from synergine2.config import Config
8 8
 from synergine2_cocos2d.util import PathManager
9 9
 
10 10
 from opencombat.exception import UnknownWeapon
11
-from opencombat.gui.const import MODE_MAN_STAND_UP
12
-from opencombat.gui.const import MODE_MAN_CRAWLING
11
+from opencombat.game.const import MODE_MAN_STAND_UP
12
+from opencombat.game.const import MODE_MAN_CRAWLING
13 13
 
14 14
 if typing.TYPE_CHECKING:
15
-    from opencombat.gui.actor import BaseActor
15
+    from opencombat.game.actor import BaseActor
16 16
 
17 17
 RIFFLE = 'RIFFLE'
18 18
 

+ 17 - 0
opencombat/gui.py View File

@@ -0,0 +1,17 @@
1
+# coding: utf-8
2
+from logging import Logger
3
+from tkinter import Tk
4
+
5
+from synergine2.config import Config
6
+from synergine2.log import get_logger
7
+
8
+
9
+class Gui(object):
10
+    def __init__(
11
+        self,
12
+        config: Config,
13
+        master: Tk,
14
+    ) -> None:
15
+        self._config = config
16
+        self._logger = get_logger(self.__class__.__name__, config)
17
+        self._master = master

+ 11 - 60
opencombat/state.py View File

@@ -7,13 +7,13 @@ from lxml import etree
7 7
 from synergine2.config import Config
8 8
 from synergine2.log import get_logger
9 9
 
10
-from opencombat.exception import StateLoadError
11 10
 from opencombat.exception import NotFoundError
12 11
 from opencombat.simulation.base import TileStrategySimulation
13 12
 from opencombat.simulation.subject import TileSubject
14 13
 from opencombat.util import get_class_from_string_path
15 14
 from opencombat.util import pretty_xml
16 15
 from opencombat.util import get_text_xml_element
16
+from opencombat.xml import XmlValidator
17 17
 
18 18
 
19 19
 class State(object):
@@ -195,6 +195,15 @@ class StateLoader(object):
195 195
         self._config = config
196 196
         self._simulation = simulation
197 197
 
198
+        schema_file_path = self._config.get(
199
+            'global.state_schema',
200
+            'opencombat/state.xsd',
201
+        )
202
+        self._xml_validator = XmlValidator(
203
+            config,
204
+            schema_file_path,
205
+        )
206
+
198 207
     def get_state(
199 208
         self,
200 209
         state_file_path: str,
@@ -209,65 +218,7 @@ class StateLoader(object):
209 218
         self,
210 219
         state_file_path: str,
211 220
     ) -> Element:
212
-        # open and read schema file
213
-        schema_file_path = self._config.get(
214
-            'global.state_schema',
215
-            'opencombat/state.xsd',
216
-        )
217
-        with open(schema_file_path, 'r') as schema_file:
218
-            schema_to_check = schema_file.read()
219
-
220
-        # open and read xml file
221
-        with open(state_file_path, 'r') as xml_file:
222
-            xml_to_check = xml_file.read()
223
-
224
-        xmlschema_doc = etree.fromstring(schema_to_check.encode('utf-8'))
225
-        xmlschema = etree.XMLSchema(xmlschema_doc)
226
-
227
-        try:
228
-            doc = etree.fromstring(xml_to_check.encode('utf-8'))
229
-        # check for file IO error
230
-        except IOError as exc:
231
-            self._logger.error(exc)
232
-            raise StateLoadError('Invalid File "{}": {}'.format(
233
-                state_file_path,
234
-                str(exc),
235
-            ))
236
-        # check for XML syntax errors
237
-        except etree.XMLSyntaxError as exc:
238
-            self._logger.error(exc)
239
-            raise StateLoadError('XML Syntax Error in "{}": {}'.format(
240
-                state_file_path,
241
-                str(exc.error_log),
242
-            ))
243
-        except Exception as exc:
244
-            self._logger.error(exc)
245
-            raise StateLoadError('Unknown error with "{}": {}'.format(
246
-                state_file_path,
247
-                str(exc),
248
-            ))
249
-
250
-        # validate against schema
251
-        try:
252
-            xmlschema.assertValid(doc)
253
-        except etree.DocumentInvalid as exc:
254
-            self._logger.error(exc)
255
-            raise StateLoadError(
256
-                'Schema validation error with "{}": {}'.format(
257
-                    state_file_path,
258
-                    str(exc),
259
-                )
260
-            )
261
-        except Exception as exc:
262
-            self._logger.error(exc)
263
-            raise StateLoadError(
264
-                'Unknown validation error with "{}": {}'.format(
265
-                    state_file_path,
266
-                    str(exc),
267
-                )
268
-            )
269
-
270
-        return doc
221
+        return self._xml_validator.validate_and_return(state_file_path)
271 222
 
272 223
 
273 224
 class StateConstructorBuilder(object):

+ 1 - 0
opencombat/strategy/__init__.py View File

@@ -0,0 +1 @@
1
+# coding: utf-8

+ 45 - 0
opencombat/strategy/manager.py View File

@@ -0,0 +1,45 @@
1
+# coding: utf-8
2
+import typing
3
+
4
+from synergine2.config import Config
5
+from synergine2.log import get_logger
6
+
7
+from opencombat.strategy.team.model import TeamModel
8
+from opencombat.strategy.team.stash import TeamStash
9
+from opencombat.strategy.troops import TroopClassBuilder
10
+from opencombat.strategy.unit.stash import UnitStash
11
+
12
+
13
+class TroopManager(object):
14
+    def __init__(
15
+        self,
16
+        config: Config,
17
+        units_file_path: str,
18
+        teams_file_path: str,
19
+    ) -> None:
20
+        self._config = config
21
+        self._logger = get_logger('TroopManager', config)
22
+
23
+        self._builder = TroopClassBuilder(config)
24
+        self._unit_stash = self._builder.get_unit_stash(
25
+            units_file_path,
26
+        )
27
+        self._team_stash = self._builder.get_team_stash(
28
+            units_file_path,
29
+            teams_file_path,
30
+        )
31
+
32
+    @property
33
+    def team_stash(self) -> TeamStash:
34
+        return self._team_stash
35
+
36
+    @property
37
+    def unit_stash(self) -> UnitStash:
38
+        return self._unit_stash
39
+
40
+    def get_troop_dump(
41
+        self,
42
+        countries_troops: typing.Dict[str, typing.List[TeamModel]],
43
+    ):
44
+        dumper = self._builder.get_troop_dumper()
45
+        return dumper.get_troop_dump(countries_troops)

+ 1 - 0
opencombat/strategy/selection/__init__.py View File

@@ -0,0 +1 @@
1
+# coding: utf-8

+ 207 - 0
opencombat/strategy/selection/gui.py View File

@@ -0,0 +1,207 @@
1
+# coding: utf-8
2
+import os
3
+import typing
4
+from tkinter import Tk
5
+from tkinter import Button
6
+from tkinter import YES
7
+from tkinter import StringVar
8
+from tkinter import OptionMenu
9
+from tkinter import W
10
+from tkinter import E
11
+from tkinter import messagebox
12
+from tkinter.ttk import Combobox
13
+from tkinter.ttk import Treeview
14
+
15
+
16
+import time
17
+from synergine2.config import Config
18
+
19
+from opencombat.gui import Gui
20
+from opencombat.strategy.manager import TroopManager
21
+from opencombat.strategy.team.stash import TeamStash
22
+
23
+
24
+class SelectTroopsGui(Gui):
25
+    def __init__(
26
+        self,
27
+        config: Config,
28
+        master: Tk,
29
+        team_stash: TeamStash,
30
+        troop_manager: TroopManager,
31
+        countries: typing.List[str],
32
+        troops_dir_path: str = '.',
33
+    ) -> None:
34
+        super().__init__(config, master)
35
+        self._master.title('Troops selection')
36
+        self._countries = countries
37
+        self._team_stash = team_stash
38
+        self._troop_manager = troop_manager
39
+        self._troops_dir_path = troops_dir_path
40
+        self._countries_troops = {}  # type: typing.Dict[str, typing.List[TeamModel]]  # nopep8
41
+
42
+        # Widgets
43
+        self._selected_country_var = StringVar(self._master)
44
+        self._selected_country_var.set(countries[0])
45
+        self._selected_country_var.trace('w', self._country_changed)
46
+        self._select_country_menu = OptionMenu(
47
+            self._master,
48
+            self._selected_country_var,
49
+            *countries,
50
+        )
51
+
52
+        self._teams_var = StringVar(self._master)
53
+        self._teams_list = Combobox(
54
+            self._master,
55
+            height=10,
56
+            state='readonly',
57
+            textvariable=self._teams_var,
58
+        )
59
+
60
+        self._add_troop_var = StringVar(self._master)
61
+        self._add_troop_button = Button(
62
+            self._master,
63
+            textvariable=self._add_troop_var,
64
+            command=self._add_troop,
65
+        )
66
+        self._add_troop_var.set('Add troop')
67
+
68
+        self._remove_troop_var = StringVar(self._master)
69
+        self._remove_troop_button = Button(
70
+            self._master,
71
+            textvariable=self._remove_troop_var,
72
+            command=self._remove_troop,
73
+        )
74
+        self._remove_troop_var.set('Remove troop')
75
+
76
+        self._troops_view = Treeview(
77
+            self._master,
78
+            columns=('Soldiers',),
79
+            height=10,
80
+        )
81
+        self._troops_view.heading('#0', text='Team')
82
+        self._troops_view.heading('#1', text='Soldiers')
83
+        self._troops_view.column('#0', stretch=YES)
84
+        self._troops_view.column('#1', stretch=YES)
85
+
86
+        self._generate_troops_var = StringVar(self._master)
87
+        self._generate_troops_button = Button(
88
+            self._master,
89
+            textvariable=self._generate_troops_var,
90
+            command=self._generate_troops,
91
+        )
92
+        self._generate_troops_var.set('Generate troops')
93
+
94
+        # Layout
95
+        self._select_country_menu.grid(row=0, column=0, sticky=W)
96
+        self._teams_list.grid(row=1, column=0, sticky=W)
97
+        self._add_troop_button.grid(row=2, column=0, sticky=W)
98
+        self._troops_view.grid(row=3, column=0, sticky=W)
99
+        self._remove_troop_button.grid(row=4, column=0, sticky=W)
100
+        self._generate_troops_button.grid(row=4, column=0, sticky=E)
101
+
102
+        # Default behaviours
103
+        self._selected_country_var.set(countries[0])
104
+        self._country_changed()
105
+
106
+    def _country_changed(self, *args, **kwargs) -> None:
107
+        country = self._selected_country_var.get()
108
+
109
+        self._logger.info('Change country to "{}"'.format(
110
+            country,
111
+        ))
112
+        country_team_names = [
113
+            t.name for
114
+            t in self._team_stash.get_team_by_country(
115
+                self._selected_country_var.get(),
116
+            )
117
+        ]
118
+
119
+        self._logger.debug('Change teams for: "{}"'.format(country_team_names))
120
+        self._teams_list['values'] = country_team_names
121
+        self._teams_var.set(country_team_names[0])
122
+        self._update_troops_view(country)
123
+
124
+    def _add_troop(self, *args, **kwargs) -> None:
125
+        if self._teams_var.get():
126
+            country = self._selected_country_var.get()
127
+            team_name = self._teams_var.get()
128
+
129
+            self._logger.info('Add troop "{}" to country "{}" troops'.format(
130
+                team_name,
131
+                team_name,
132
+            ))
133
+
134
+            team_model = self._team_stash.get_team_by_name(
135
+                team_name=team_name,
136
+                team_country=country,
137
+            )
138
+            self._countries_troops.setdefault(country, []).append(
139
+                team_model,
140
+            )
141
+            self._update_troops_view(country)
142
+
143
+    def _remove_troop(self, *args, **kwargs) -> None:
144
+        selecteds = self._troops_view.selection()
145
+
146
+        for selected in selecteds:
147
+            team_name = self._troops_view.item(selected)['text']
148
+            country = self._selected_country_var.get()
149
+
150
+            self._logger.info('Remove team "{}" from country "{}"'.format(
151
+                team_name,
152
+                country,
153
+            ))
154
+
155
+            team_model = self._team_stash.get_team_by_name(
156
+                team_name=team_name,
157
+                team_country=country,
158
+            )
159
+
160
+            self._countries_troops[country].remove(team_model)
161
+
162
+        if selecteds:
163
+            self._update_troops_view(country)
164
+
165
+    def _update_troops_view(self, country: str) -> None:
166
+        teams = self._countries_troops.get(country, [])
167
+
168
+        self._troops_view.delete(*self._troops_view.get_children())
169
+        for team in teams:
170
+            self._troops_view.insert(
171
+                '',
172
+                'end',
173
+                text=team.name,
174
+                values=('o' * len(team.units,))
175
+            )
176
+
177
+    def _generate_troops(self, *args, **kwargs) -> None:
178
+        # Must have team(s) in all countries
179
+        if len(self._countries_troops.keys()) == len(self._countries) \
180
+                and all(self._countries_troops.values()):
181
+
182
+            troops_file_path = os.path.join(
183
+                self._troops_dir_path,
184
+                'troops_{}.xml'.format(str(time.time())),
185
+            )
186
+
187
+            self._logger.info('Generate troops into file "{}"'.format(
188
+                troops_file_path,
189
+            ))
190
+
191
+            troops_xml = self._troop_manager.get_troop_dump(
192
+                self._countries_troops,
193
+            )
194
+            with open(troops_file_path, 'w+') as file:
195
+                file.write(troops_xml)
196
+
197
+            messagebox.showinfo(
198
+                'Troops file generated',
199
+                'Troops file successfully generate at "{}"'.format(
200
+                    troops_file_path,
201
+                )
202
+            )
203
+        else:
204
+            messagebox.showinfo(
205
+                'Missing information',
206
+                'All countries must have teams',
207
+            )

+ 1 - 0
opencombat/strategy/team/__init__.py View File

@@ -0,0 +1 @@
1
+# coding: utf-8

+ 34 - 0
opencombat/strategy/team/model.py View File

@@ -0,0 +1,34 @@
1
+# coding: utf-8
2
+import typing
3
+
4
+from opencombat.strategy.unit.model import UnitModel
5
+
6
+
7
+class TeamModel(object):
8
+    def __init__(
9
+        self,
10
+        id_: str,
11
+        name: str,
12
+        country: str,
13
+        units: typing.List[UnitModel],
14
+    ) -> None:
15
+        self._id = id_
16
+        self._name = name
17
+        self._country = country
18
+        self._units = units
19
+
20
+    @property
21
+    def id(self) -> str:
22
+        return self._id
23
+
24
+    @property
25
+    def name(self) -> str:
26
+        return self._name
27
+
28
+    @property
29
+    def country(self) -> str:
30
+        return self._country
31
+
32
+    @property
33
+    def units(self) -> typing.List[UnitModel]:
34
+        return self._units

+ 114 - 0
opencombat/strategy/team/stash.py View File

@@ -0,0 +1,114 @@
1
+# coding: utf-8
2
+import typing
3
+from _elementtree import Element
4
+
5
+from synergine2.config import Config
6
+
7
+from opencombat.exception import NotFoundException
8
+from opencombat.strategy.team.model import TeamModel
9
+from opencombat.strategy.unit.stash import UnitStash
10
+from opencombat.util import get_text_xml_element
11
+from opencombat.xml import XmlValidator
12
+
13
+
14
+class TeamStash(object):
15
+    def __init__(
16
+        self,
17
+        config: Config,
18
+        teams_file_path: str,
19
+        unit_stash: UnitStash,
20
+    ) -> None:
21
+        self._config = config
22
+        self._teams = None  # type: typing.List[TeamModel]
23
+        self._unit_stash = unit_stash
24
+        self._teams_file_path = teams_file_path
25
+
26
+        self.schema_file_path = self._config.get(
27
+            'global.teams_schema',
28
+            'opencombat/strategy/teams.xsd',
29
+        )
30
+        self._xml_validator = XmlValidator(
31
+            config,
32
+            self.schema_file_path,
33
+        )
34
+        self._root_element = self._xml_validator.validate_and_return(
35
+            self._teams_file_path,
36
+        )
37
+
38
+    def _get_computed_teams(self) -> typing.List[TeamModel]:
39
+        teams = []
40
+
41
+        for team_element in self._root_element.findall('team'):
42
+            team_element = typing.cast(Element, team_element)
43
+
44
+            team_id = team_element.attrib['id']
45
+            team_country = team_element.attrib['country']
46
+            team_name = get_text_xml_element(team_element, 'name')
47
+            team_units = []
48
+
49
+            units_element = team_element.find('units')
50
+            for unit_element in units_element.findall('unit'):
51
+                unit_id = get_text_xml_element(unit_element, 'id')
52
+                unit = self._unit_stash.get_unit(unit_id, team_country)
53
+                team_units.append(unit)
54
+
55
+            teams.append(
56
+                TeamModel(
57
+                    id_=team_id,
58
+                    country=team_country,
59
+                    name=team_name,
60
+                    units=team_units
61
+                )
62
+            )
63
+
64
+        return teams
65
+
66
+    @property
67
+    def teams(self) -> typing.List[TeamModel]:
68
+        if self._teams is None:
69
+            self._teams = self._get_computed_teams()
70
+
71
+        return self._teams
72
+
73
+    def get_team(
74
+        self,
75
+        team_id: str,
76
+        team_country: str,
77
+    ) -> TeamModel:
78
+        for team in self.teams:
79
+            if team.id == team_id and team.country == team_country:
80
+                return team
81
+
82
+        raise NotFoundException(
83
+            'No team matching with id "{}" and country "{}" in "{}"'.format(
84
+                team_id,
85
+                team_country,
86
+                self.schema_file_path,
87
+            )
88
+        )
89
+
90
+    def get_team_by_name(
91
+        self,
92
+        team_name: str,
93
+        team_country: str,
94
+    ) -> TeamModel:
95
+        for team in self.teams:
96
+            if team.name == team_name and team.country == team_country:
97
+                return team
98
+
99
+        raise NotFoundException(
100
+            'No team matching with name "{}" and country "{}" in "{}"'.format(
101
+                team_name,
102
+                team_country,
103
+                self.schema_file_path,
104
+            )
105
+        )
106
+
107
+    def get_team_by_country(self, country: str) -> typing.List[TeamModel]:
108
+        teams = []  # type: typing.List[TeamModel]
109
+
110
+        for team in self.teams:
111
+            if team.country == country:
112
+                teams.append(team)
113
+
114
+        return teams

+ 37 - 0
opencombat/strategy/teams.xml View File

@@ -0,0 +1,37 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<teams>
3
+    <team id="std_team" country="URSS">
4
+        <name>Standard URSS team</name>
5
+        <units>
6
+            <unit>
7
+                <id>std_soldier</id>
8
+            </unit>
9
+            <unit>
10
+                <id>std_soldier</id>
11
+            </unit>
12
+            <unit>
13
+                <id>std_soldier</id>
14
+            </unit>
15
+            <unit>
16
+                <id>std_soldier</id>
17
+            </unit>
18
+        </units>
19
+    </team>
20
+    <team id="std_team" country="DE">
21
+        <name>Standard DE team</name>
22
+        <units>
23
+            <unit>
24
+                <id>std_soldier</id>
25
+            </unit>
26
+            <unit>
27
+                <id>std_soldier</id>
28
+            </unit>
29
+            <unit>
30
+                <id>std_soldier</id>
31
+            </unit>
32
+            <unit>
33
+                <id>std_soldier</id>
34
+            </unit>
35
+        </units>
36
+    </team>
37
+</teams>

+ 33 - 0
opencombat/strategy/teams.xsd View File

@@ -0,0 +1,33 @@
1
+<?xml version="1.0" encoding="UTF-8" ?>
2
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
3
+
4
+    <xs:element name="teams" type="teamstype"/>
5
+
6
+    <xs:complexType name="teamstype">
7
+        <xs:sequence>
8
+            <xs:element name="team" type="teamtype" maxOccurs="unbounded"/>
9
+        </xs:sequence>
10
+    </xs:complexType>
11
+
12
+    <xs:complexType name="teamtype">
13
+        <xs:sequence>
14
+            <xs:element name="name" type="xs:string" maxOccurs="1"/>
15
+            <xs:element name="units" type="unitstype" maxOccurs="1"/>
16
+        </xs:sequence>
17
+        <xs:attribute name="id" type="xs:string" use="required"/>
18
+        <xs:attribute name="country" type="xs:string" use="required"/>
19
+    </xs:complexType>
20
+
21
+    <xs:complexType name="unitstype">
22
+        <xs:sequence>
23
+            <xs:element name="unit" type="unittype" maxOccurs="unbounded" minOccurs="1"/>
24
+        </xs:sequence>
25
+    </xs:complexType>
26
+
27
+    <xs:complexType name="unittype">
28
+        <xs:sequence>
29
+            <xs:element name="id" type="xs:string" maxOccurs="1" minOccurs="1"/>
30
+        </xs:sequence>
31
+    </xs:complexType>
32
+
33
+</xs:schema>

+ 111 - 0
opencombat/strategy/troops.py View File

@@ -0,0 +1,111 @@
1
+# coding: utf-8
2
+import typing
3
+
4
+from lxml import etree
5
+
6
+from synergine2.config import Config
7
+from synergine2.log import get_logger
8
+
9
+from opencombat.strategy.team.model import TeamModel
10
+from opencombat.strategy.team.stash import TeamStash
11
+from opencombat.strategy.unit.stash import UnitStash
12
+from opencombat.util import get_class_from_string_path, pretty_xml
13
+
14
+
15
+class TroopDumper(object):
16
+    def __init__(
17
+        self,
18
+        config: Config,
19
+    ) -> None:
20
+        self._config = config
21
+        self._logger = get_logger('TroopDumper', config)
22
+
23
+    def get_troop_dump(
24
+        self,
25
+        countries_troops: typing.Dict[str, typing.List[TeamModel]],
26
+    ) -> str:
27
+        troops_template = self._config.resolve(
28
+            'global.troops_template',
29
+            'opencombat/strategy/troops_template.xml',
30
+        )
31
+        with open(troops_template, 'r') as xml_file:
32
+            template_str = xml_file.read()
33
+
34
+        parser = etree.XMLParser(remove_blank_text=True)
35
+        state_root = etree.fromstring(
36
+            template_str.encode('utf-8'),
37
+            parser,
38
+        )
39
+
40
+        for country, teams in countries_troops.items():
41
+            for team in teams:
42
+                troop_element = etree.SubElement(state_root, 'troop')
43
+                troop_element.attrib['country'] = country
44
+                troop_element.attrib['team_id'] = team.id
45
+
46
+        return pretty_xml(
47
+            etree.tostring(
48
+                state_root,
49
+            ).decode('utf-8'),
50
+        )
51
+
52
+
53
+class TroopClassBuilder(object):
54
+    def __init__(
55
+        self,
56
+        config: Config,
57
+    ) -> None:
58
+        self._logger = get_logger('TroopManagerBuilder', config)
59
+        self._config = config
60
+
61
+    def get_unit_stash(
62
+        self,
63
+        units_file_path: str,
64
+    ) -> UnitStash:
65
+        class_address = self._config.resolve(
66
+            'global.unit_stash',
67
+            'opencombat.strategy.unit.stash.UnitStash',
68
+        )
69
+        class_ = get_class_from_string_path(
70
+            self._config,
71
+            class_address,
72
+        )
73
+        return class_(
74
+            self._config,
75
+            units_file_path,
76
+        )
77
+
78
+    def get_team_stash(
79
+        self,
80
+        units_file_path: str,
81
+        teams_file_path: str,
82
+    ) -> TeamStash:
83
+        class_address = self._config.resolve(
84
+            'global.team_stash',
85
+            'opencombat.strategy.team.stash.TeamStash',
86
+        )
87
+        class_ = get_class_from_string_path(
88
+            self._config,
89
+            class_address,
90
+        )
91
+
92
+        unit_stash = self.get_unit_stash(units_file_path)
93
+        return class_(
94
+            self._config,
95
+            teams_file_path,
96
+            unit_stash=unit_stash,
97
+        )
98
+
99
+    def get_troop_dumper(self) -> TroopDumper:
100
+        class_address = self._config.resolve(
101
+            'global.troop_dumper',
102
+            'opencombat.strategy.troops.TroopDumper',
103
+        )
104
+        class_ = get_class_from_string_path(
105
+            self._config,
106
+            class_address,
107
+        )
108
+
109
+        return class_(
110
+            self._config,
111
+        )

+ 3 - 0
opencombat/strategy/troops_template.xml View File

@@ -0,0 +1,3 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<troops>
3
+</troops>

+ 1 - 0
opencombat/strategy/unit/__init__.py View File

@@ -0,0 +1 @@
1
+# coding: utf-8

+ 34 - 0
opencombat/strategy/unit/model.py View File

@@ -0,0 +1,34 @@
1
+# coding: utf-8
2
+import typing
3
+
4
+from opencombat.simulation.subject import TileSubject
5
+
6
+
7
+class UnitModel(object):
8
+    def __init__(
9
+        self,
10
+        id_: str,
11
+        name: str,
12
+        class_: typing.Type[TileSubject],
13
+        country: str,
14
+    ) -> None:
15
+        self._id = id_
16
+        self._name = name
17
+        self._class = class_
18
+        self._country = country
19
+
20
+    @property
21
+    def id(self) -> str:
22
+        return self._id
23
+
24
+    @property
25
+    def name(self) -> str:
26
+        return self._name
27
+
28
+    @property
29
+    def class_(self) -> typing.Type[TileSubject]:
30
+        return self._class
31
+
32
+    @property
33
+    def country(self) -> str:
34
+        return self._country

+ 83 - 0
opencombat/strategy/unit/stash.py View File

@@ -0,0 +1,83 @@
1
+# coding: utf-8
2
+import typing
3
+from _elementtree import Element
4
+
5
+from synergine2.config import Config
6
+
7
+from opencombat.exception import NotFoundException
8
+from opencombat.strategy.team.model import UnitModel
9
+from opencombat.util import get_text_xml_element
10
+from opencombat.util import get_class_from_string_path
11
+from opencombat.xml import XmlValidator
12
+
13
+
14
+class UnitStash(object):
15
+    def __init__(
16
+        self,
17
+        config: Config,
18
+        units_file_path: str,
19
+    ) -> None:
20
+        self._config = config
21
+        self._units = None  # type: typing.List[UnitModel]
22
+
23
+        self.schema_file_path = self._config.get(
24
+            'global.teams_schema',
25
+            'opencombat/strategy/units.xsd',
26
+        )
27
+        self._xml_validator = XmlValidator(
28
+            config,
29
+            self.schema_file_path,
30
+        )
31
+        self._root_element = self._xml_validator.validate_and_return(
32
+            units_file_path,
33
+        )
34
+
35
+    def _get_computed_units(self) -> typing.List[UnitModel]:
36
+        units = []
37
+
38
+        for unit_element in self._root_element.findall('unit'):
39
+            unit_element = typing.cast(Element, unit_element)
40
+
41
+            unit_id = unit_element.attrib['id']
42
+            unit_country = unit_element.attrib['country']
43
+            unit_name = get_text_xml_element(unit_element, 'name')
44
+            unit_class_path = get_text_xml_element(unit_element, 'type')
45
+            unit_class = get_class_from_string_path(
46
+                self._config,
47
+                unit_class_path,
48
+            )
49
+
50
+            units.append(
51
+                UnitModel(
52
+                    id_=unit_id,
53
+                    name=unit_name,
54
+                    class_=unit_class,
55
+                    country=unit_country,
56
+                )
57
+            )
58
+
59
+        return units
60
+
61
+    @property
62
+    def units(self) -> typing.List[UnitModel]:
63
+        if self._units is None:
64
+            self._units = self._get_computed_units()
65
+
66
+        return self._units
67
+
68
+    def get_unit(
69
+        self,
70
+        unit_id: str,
71
+        unit_country: str,
72
+    ) -> UnitModel:
73
+        for unit in self.units:
74
+            if unit.id == unit_id and unit.country == unit_country:
75
+                return unit
76
+
77
+        raise NotFoundException(
78
+            'No unit matching with id "{}" and country "{}" in "{}"'.format(
79
+                unit_id,
80
+                unit_country,
81
+                self.schema_file_path,
82
+            )
83
+        )

+ 11 - 0
opencombat/strategy/units.xml View File

@@ -0,0 +1,11 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<units>
3
+    <unit id="std_soldier" country="URSS">
4
+        <name>Standard soldier</name>
5
+        <type>opencombat.simulation.subject.ManSubject</type>
6
+    </unit>
7
+    <unit id="std_soldier" country="DE">
8
+        <name>Standard soldier</name>
9
+        <type>opencombat.simulation.subject.ManSubject</type>
10
+    </unit>
11
+</units>

+ 21 - 0
opencombat/strategy/units.xsd View File

@@ -0,0 +1,21 @@
1
+<?xml version="1.0" encoding="UTF-8" ?>
2
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
3
+
4
+    <xs:element name="units" type="unitstype"/>
5
+
6
+    <xs:complexType name="unitstype">
7
+        <xs:sequence>
8
+            <xs:element name="unit" type="unittype" maxOccurs="unbounded" minOccurs="1"/>
9
+        </xs:sequence>
10
+    </xs:complexType>
11
+
12
+    <xs:complexType name="unittype">
13
+        <xs:sequence>
14
+            <xs:element name="name" type="xs:string" maxOccurs="1" minOccurs="1"/>
15
+            <xs:element name="type" type="xs:string" maxOccurs="1" minOccurs="1"/>
16
+        </xs:sequence>
17
+        <xs:attribute name="id" type="xs:string" use="required"/>
18
+        <xs:attribute name="country" type="xs:string" use="required"/>
19
+    </xs:complexType>
20
+
21
+</xs:schema>

+ 1 - 1
opencombat/terminal/base.py View File

@@ -39,7 +39,7 @@ class CocosTerminal(GameTerminal):
39 39
         self.map_dir_path = map_dir_path
40 40
 
41 41
     def run(self):
42
-        from opencombat.gui.base import Game
42
+        from opencombat.game.base import Game
43 43
 
44 44
         self.gui = Game(
45 45
             self.config,

+ 75 - 0
opencombat/xml.py View File

@@ -0,0 +1,75 @@
1
+# coding: utf-8
2
+from _elementtree import Element
3
+
4
+from lxml import etree
5
+from synergine2.config import Config
6
+from synergine2.log import get_logger
7
+
8
+from opencombat.exception import StateLoadError
9
+
10
+
11
+class XmlValidator(object):
12
+    def __init__(
13
+        self,
14
+        config: Config,
15
+        schema_file_path: str,
16
+    ) -> None:
17
+        self._config = config
18
+        self._logger = get_logger('XmlValidator', config)
19
+        self._schema_file_path = schema_file_path
20
+
21
+    def validate_and_return(self, xml_file_path: str) -> Element:
22
+        with open(self._schema_file_path, 'r') as schema_file:
23
+            schema_to_check = schema_file.read()
24
+
25
+        # open and read xml file
26
+        with open(xml_file_path, 'r') as xml_file:
27
+            xml_to_check = xml_file.read()
28
+
29
+        xmlschema_doc = etree.fromstring(schema_to_check.encode('utf-8'))
30
+        xmlschema = etree.XMLSchema(xmlschema_doc)
31
+
32
+        try:
33
+            doc = etree.fromstring(xml_to_check.encode('utf-8'))
34
+        # check for file IO error
35
+        except IOError as exc:
36
+            self._logger.error(exc)
37
+            raise StateLoadError('Invalid File "{}": {}'.format(
38
+                xml_file_path,
39
+                str(exc),
40
+            ))
41
+        # check for XML syntax errors
42
+        except etree.XMLSyntaxError as exc:
43
+            self._logger.error(exc)
44
+            raise StateLoadError('XML Syntax Error in "{}": {}'.format(
45
+                xml_file_path,
46
+                str(exc.error_log),
47
+            ))
48
+        except Exception as exc:
49
+            self._logger.error(exc)
50
+            raise StateLoadError('Unknown error in "{}": {}'.format(
51
+                xml_file_path,
52
+                str(exc),
53
+            ))
54
+
55
+        # validate against schema
56
+        try:
57
+            xmlschema.assertValid(doc)
58
+        except etree.DocumentInvalid as exc:
59
+            self._logger.error(exc)
60
+            raise StateLoadError(
61
+                'Schema validation error in "{}": {}'.format(
62
+                    xml_file_path,
63
+                    str(exc),
64
+                )
65
+            )
66
+        except Exception as exc:
67
+            self._logger.error(exc)
68
+            raise StateLoadError(
69
+                'Unknown validation error in "{}": {}'.format(
70
+                    xml_file_path,
71
+                    str(exc),
72
+                )
73
+            )
74
+
75
+        return doc

+ 3 - 1
run.py View File

@@ -71,7 +71,9 @@ def main(
71 71
     core.run()
72 72
 
73 73
 if __name__ == '__main__':
74
-    parser = argparse.ArgumentParser(description='Run TileStrategy')
74
+    parser = argparse.ArgumentParser(
75
+        description='Run a map'
76
+    )
75 77
     parser.add_argument('map_dir_path', help='map directory path')
76 78
     parser.add_argument('--seed', dest='seed', default=None)
77 79
     parser.add_argument('--state', dest='state', default=None)

+ 70 - 0
select_troops.py View File

@@ -0,0 +1,70 @@
1
+# coding: utf-8
2
+import argparse
3
+import typing
4
+from tkinter import Tk
5
+
6
+from synergine2.config import Config
7
+
8
+from opencombat.strategy.manager import TroopManager
9
+from opencombat.strategy.selection.gui import SelectTroopsGui
10
+
11
+
12
+def main(
13
+    units_file_path: str,
14
+    teams_file_path: str,
15
+    countries: typing.List[str],
16
+    troops_dir_path: str = '.',
17
+) -> None:
18
+    config = Config()
19
+    config.load_yaml('config.yaml')
20
+
21
+    troop_manager = TroopManager(
22
+        config,
23
+        units_file_path=units_file_path,
24
+        teams_file_path=teams_file_path,
25
+    )
26
+
27
+    master = Tk()
28
+    gui = SelectTroopsGui(
29
+        config,
30
+        master=master,
31
+        team_stash=troop_manager.team_stash,
32
+        troop_manager=troop_manager,
33
+        countries=countries,
34
+        troops_dir_path=troops_dir_path,
35
+    )
36
+    master.mainloop()
37
+
38
+
39
+if __name__ == '__main__':
40
+    parser = argparse.ArgumentParser(
41
+        description='Display troops selection gui',
42
+    )
43
+    parser.add_argument(
44
+        '--units',
45
+        dest='units_file_path',
46
+        default='opencombat/strategy/units.xml',
47
+    )
48
+    parser.add_argument(
49
+        '--teams',
50
+        dest='teams_file_path',
51
+        default='opencombat/strategy/teams.xml',
52
+    )
53
+    parser.add_argument(
54
+        '--country',
55
+        action='append',
56
+        dest='countries',
57
+    )
58
+    parser.add_argument(
59
+        '--troops-dir-path',
60
+        dest='troops_dir_path',
61
+        default='.',
62
+    )
63
+    args = parser.parse_args()
64
+
65
+    main(
66
+        units_file_path=args.units_file_path,
67
+        teams_file_path=args.teams_file_path,
68
+        countries=args.countries,
69
+        troops_dir_path=args.troops_dir_path,
70
+    )

+ 10 - 0
test_config.yaml View File

@@ -21,6 +21,16 @@ game:
21 21
       draw_interior_gap: 2
22 22
 
23 23
 global:
24
+    state_loader: "opencombat.state.StateLoader"
25
+    state_dumper: "opencombat.state.StateDumper"
26
+    state_schema: "opencombat/state.xsd"
27
+    state_template: "opencombat/state_template.xml"
28
+    unit_stash: "opencombat.strategy.unit.stash.UnitStash"
29
+    team_stash: "opencombat.strategy.team.stash.TeamStash"
30
+    teams_schema: "opencombat/strategy/teams.xsd"
31
+    units_schema: "opencombat/strategy/units.xsd"
32
+    troop_dumper: "opencombat.strategy.troops.TroopDumper"
33
+    troop_schema: "opencombat/strategy/troops.xsd"
24 34
     cache_dir_path: 'cache'
25 35
     include_path:
26 36
       maps:

+ 37 - 0
tests/fixtures/teams.xml View File

@@ -0,0 +1,37 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<teams>
3
+    <team id="std_team" country="URSS">
4
+        <name>Standard URSS team</name>
5
+        <units>
6
+            <unit>
7
+                <id>std_soldier</id>
8
+            </unit>
9
+            <unit>
10
+                <id>std_soldier</id>
11
+            </unit>
12
+            <unit>
13
+                <id>std_soldier</id>
14
+            </unit>
15
+            <unit>
16
+                <id>std_soldier</id>
17
+            </unit>
18
+        </units>
19
+    </team>
20
+    <team id="std_team" country="DE">
21
+        <name>Standard DE team</name>
22
+        <units>
23
+            <unit>
24
+                <id>std_soldier</id>
25
+            </unit>
26
+            <unit>
27
+                <id>std_soldier</id>
28
+            </unit>
29
+            <unit>
30
+                <id>std_soldier</id>
31
+            </unit>
32
+            <unit>
33
+                <id>std_soldier</id>
34
+            </unit>
35
+        </units>
36
+    </team>
37
+</teams>

+ 11 - 0
tests/fixtures/units.xml View File

@@ -0,0 +1,11 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<units>
3
+    <unit id="std_soldier" country="URSS">
4
+        <name>Standard soldier</name>
5
+        <type>opencombat.simulation.subject.ManSubject</type>
6
+    </unit>
7
+    <unit id="std_soldier" country="DE">
8
+        <name>Standard soldier</name>
9
+        <type>opencombat.simulation.subject.ManSubject</type>
10
+    </unit>
11
+</units>

+ 0 - 0
tests/strategy/__init__.py View File


+ 107 - 0
tests/strategy/test_teams.py View File

@@ -0,0 +1,107 @@
1
+# coding: utf-8
2
+import pytest
3
+from synergine2.config import Config
4
+
5
+from opencombat.exception import NotFoundException
6
+from opencombat.strategy.team.model import TeamModel
7
+from opencombat.strategy.team.stash import TeamStash
8
+from opencombat.strategy.unit.model import UnitModel
9
+from opencombat.strategy.unit.stash import UnitStash
10
+
11
+
12
+@pytest.fixture
13
+def unit_stash(
14
+    config: Config,
15
+) -> UnitStash:
16
+    return UnitStash(
17
+        config,
18
+        'tests/fixtures/units.xml',
19
+    )
20
+
21
+
22
+def test_units_stash__ok__instantiate(
23
+    config: Config,
24
+    unit_stash: UnitStash,
25
+):
26
+    TeamStash(
27
+        config,
28
+        'opencombat/strategy/teams.xml',
29
+        unit_stash=unit_stash,
30
+    )
31
+
32
+
33
+def test_team_stash__ok__get_teams(
34
+    config: Config,
35
+    unit_stash: UnitStash,
36
+):
37
+    stash = TeamStash(
38
+        config,
39
+        'tests/fixtures/teams.xml',
40
+        unit_stash=unit_stash,
41
+    )
42
+    assert stash.teams
43
+    assert 2 == len(stash.teams)
44
+    assert isinstance(stash.teams[0], TeamModel)
45
+    assert isinstance(stash.teams[1], TeamModel)
46
+
47
+    assert 'std_team' == stash.teams[0].id
48
+    assert 'Standard team' == stash.teams[0].name
49
+    assert 'URSS' == stash.teams[0].country
50
+    assert stash.teams[0].units
51
+    assert 4 == len(stash.teams[0].units)
52
+    assert isinstance(stash.teams[0].units[0], UnitModel)
53
+    assert 'std_soldier' == stash.teams[0].units[0].id
54
+    assert 'std_soldier' == stash.teams[0].units[1].id
55
+    assert 'std_soldier' == stash.teams[0].units[2].id
56
+    assert 'std_soldier' == stash.teams[0].units[3].id
57
+
58
+    assert 'std_team' == stash.teams[1].id
59
+    assert 'Standard team' == stash.teams[1].name
60
+    assert 'DE' == stash.teams[1].country
61
+    assert stash.teams[0].units
62
+    assert 4 == len(stash.teams[1].units)
63
+    assert isinstance(stash.teams[1].units[0], UnitModel)
64
+    assert 'std_soldier' == stash.teams[1].units[0].id
65
+    assert 'std_soldier' == stash.teams[1].units[1].id
66
+    assert 'std_soldier' == stash.teams[1].units[2].id
67
+    assert 'std_soldier' == stash.teams[1].units[3].id
68
+
69
+
70
+def test_teams_stash__ok__get_unit(
71
+    config: Config,
72
+    unit_stash: UnitStash,
73
+):
74
+    stash = TeamStash(
75
+        config,
76
+        'tests/fixtures/teams.xml',
77
+        unit_stash=unit_stash,
78
+    )
79
+    assert stash.get_team('std_team', 'URSS')
80
+
81
+
82
+def test_teams_stash__error__get_team_wrong_country(
83
+    config: Config,
84
+    unit_stash: UnitStash,
85
+):
86
+    stash = TeamStash(
87
+        config,
88
+        'tests/fixtures/teams.xml',
89
+        unit_stash=unit_stash,
90
+    )
91
+
92
+    with pytest.raises(NotFoundException):
93
+        stash.get_team('std_team', 'UNKNOWN')
94
+
95
+
96
+def test_teams_stash__error__get_team_wrong_id(
97
+    config: Config,
98
+    unit_stash: UnitStash,
99
+):
100
+    stash = TeamStash(
101
+        config,
102
+        'tests/fixtures/teams.xml',
103
+        unit_stash=unit_stash,
104
+    )
105
+
106
+    with pytest.raises(NotFoundException):
107
+        stash.get_team('unknown', 'URSS')

+ 74 - 0
tests/strategy/test_units.py View File

@@ -0,0 +1,74 @@
1
+# coding: utf-8
2
+import pytest
3
+from synergine2.config import Config
4
+
5
+from opencombat.exception import NotFoundException
6
+from opencombat.simulation.subject import ManSubject
7
+from opencombat.strategy.team.model import UnitModel
8
+from opencombat.strategy.unit.stash import UnitStash
9
+
10
+
11
+def test_units_stash__ok__instantiate(
12
+    config: Config,
13
+):
14
+    UnitStash(
15
+        config,
16
+        'opencombat/strategy/units.xml',
17
+    )
18
+
19
+
20
+def test_units_stash__ok__get_units(
21
+    config: Config,
22
+):
23
+    stash = UnitStash(
24
+        config,
25
+        'tests/fixtures/units.xml',
26
+    )
27
+    assert stash.units
28
+    assert 2 == len(stash.units)
29
+    assert isinstance(stash.units[0], UnitModel)
30
+    assert isinstance(stash.units[1], UnitModel)
31
+
32
+    assert 'std_soldier' == stash.units[0].id
33
+    assert 'Standard soldier' == stash.units[0].name
34
+    assert 'URSS' == stash.units[0].country
35
+    assert ManSubject == stash.units[0].class_
36
+
37
+    assert 'std_soldier' == stash.units[1].id
38
+    assert 'Standard soldier' == stash.units[1].name
39
+    assert 'DE' == stash.units[1].country
40
+    assert ManSubject == stash.units[1].class_
41
+
42
+
43
+def test_units_stash__ok__get_unit(
44
+    config: Config,
45
+):
46
+    stash = UnitStash(
47
+        config,
48
+        'tests/fixtures/units.xml',
49
+    )
50
+    assert stash.get_unit('std_soldier', 'URSS')
51
+
52
+
53
+def test_units_stash__error__get_unit_wrong_country(
54
+    config: Config,
55
+):
56
+    stash = UnitStash(
57
+        config,
58
+        'tests/fixtures/units.xml',
59
+    )
60
+
61
+    with pytest.raises(NotFoundException):
62
+        stash.get_unit('std_soldier', 'UNKNOWN')
63
+
64
+
65
+def test_units_stash__error__get_unit_wrong_id(
66
+    config: Config,
67
+):
68
+    stash = UnitStash(
69
+        config,
70
+        'tests/fixtures/units.xml',
71
+    )
72
+
73
+    with pytest.raises(NotFoundException):
74
+        stash.get_unit('unknown', 'URSS')