dopynion 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dopynion-0.1.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Vincent Poulailleau
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.1
2
+ Name: dopynion
3
+ Version: 0.1.0
4
+ Summary: Dominion
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+
9
+ # dopynion
10
+ Dominion in Python
@@ -0,0 +1,2 @@
1
+ # dopynion
2
+ Dominion in Python
File without changes
@@ -0,0 +1,41 @@
1
+ # example of execution
2
+
3
+ import random
4
+
5
+ from dopynion.cards import Card, CardName
6
+ from dopynion.game import Game
7
+ from dopynion.player import Player
8
+
9
+ game = Game()
10
+ player1 = Player("Foo")
11
+ player2 = Player("Bar")
12
+ player3 = Player("Baz")
13
+ game.add_player(player1)
14
+ game.add_player(player2)
15
+ game.add_player(player3)
16
+ game.start()
17
+
18
+ for turn in range(50):
19
+ print("#" * 80)
20
+ print(f"# turn {turn}")
21
+ print("#" * 80)
22
+ print(dict(game.stock._quantities)) # noqa: SLF001
23
+ player1.start_turn()
24
+ print(player1)
25
+
26
+ while player1.actions_left and player1.hand.contains_action():
27
+ actions = player1.hand.action_cards
28
+ player1.action(random.choice(actions)) # noqa: S311
29
+ print(player1)
30
+
31
+ while player1.purchases_left and player1.hand.contains_money():
32
+ money = player1.hand.money
33
+ buyables: list[CardName] = [
34
+ card_name
35
+ for card_name, class_ in Card.types.items()
36
+ if class_.cost <= money and card_name in game.stock
37
+ ]
38
+ player1.buy(random.choice(buyables)) # noqa: S311
39
+ print(player1)
40
+
41
+ player1.end_turn()
@@ -0,0 +1,298 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import random
5
+ import sys
6
+ from collections import defaultdict
7
+ from enum import StrEnum
8
+ from typing import TYPE_CHECKING, ClassVar
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable
12
+
13
+ from dopynion.player import Player
14
+
15
+
16
+ class CardName(StrEnum): # Create with a metaclass
17
+ COPPER = "copper"
18
+ COUNCILROOM = "councilroom"
19
+ CURSE = "curse"
20
+ DUCHY = "duchy"
21
+ ESTATE = "estate"
22
+ FESTIVAL = "festival"
23
+ GOLD = "gold"
24
+ LABORATORY = "laboratory"
25
+ MARKET = "market"
26
+ PROVINCE = "province"
27
+ SILVER = "silver"
28
+ SMITHY = "smithy"
29
+ VILLAGE = "village"
30
+ WOODCUTTER = "woodcutter"
31
+
32
+ def __repr__(self) -> str:
33
+ return self.value.title()
34
+
35
+
36
+ class ClassNameRepr(type):
37
+ def __repr__(cls) -> str:
38
+ return cls.__name__
39
+
40
+
41
+ class Card(metaclass=ClassNameRepr):
42
+ types: ClassVar[dict[CardName, type[Card]]] = {}
43
+ name = "Unknown"
44
+ cost = 10_000
45
+ money = 0
46
+ is_action = False
47
+ is_kingdom = True
48
+ is_money = False
49
+
50
+ def __init_subclass__(cls) -> None:
51
+ Card.types[CardName[cls.__name__.upper()]] = cls
52
+
53
+ def __eq__(self, other: object) -> bool:
54
+ return isinstance(self, other) or self.__class__ is other
55
+
56
+ def __hash__(self) -> int:
57
+ return hash(self.name)
58
+
59
+ def __repr__(self) -> str:
60
+ return self.name
61
+
62
+ @classmethod
63
+ def action(cls, player: Player) -> None:
64
+ raise NotImplementedError
65
+
66
+
67
+ class Copper(Card):
68
+ name = "Cuivre"
69
+ cost = 0
70
+ money = 1
71
+ is_kingdom = False
72
+ is_money = True
73
+
74
+
75
+ class CouncilRoom(Card):
76
+ name = "Chambre du conseil"
77
+ cost = 5
78
+ is_action = True
79
+
80
+ @classmethod
81
+ def action(cls, player: Player) -> None:
82
+ for _ in range(4):
83
+ player.hand.append(player.take_one_card_from_deck())
84
+ player.purchases_left += 1
85
+
86
+
87
+ class Curse(Card):
88
+ name = "Malédiction"
89
+ cost = 0
90
+ is_kingdom = False
91
+
92
+
93
+ class Duchy(Card):
94
+ name = "Duché"
95
+ cost = 5
96
+ is_kingdom = False
97
+
98
+
99
+ class Estate(Card):
100
+ name = "Domaine"
101
+ cost = 2
102
+ is_kingdom = False
103
+
104
+
105
+ class Festival(Card):
106
+ name = "Festival"
107
+ cost = 5
108
+ is_action = True
109
+
110
+ @classmethod
111
+ def action(cls, player: Player) -> None:
112
+ player.actions_left += 2
113
+ player.purchases_left += 1
114
+ player.money += 2
115
+
116
+
117
+ class Gold(Card):
118
+ name = "Or"
119
+ cost = 6
120
+ money = 3
121
+ is_kingdom = False
122
+ is_money = True
123
+
124
+
125
+ class Laboratory(Card):
126
+ name = "Laboratoire"
127
+ cost = 5
128
+ is_action = True
129
+
130
+ @classmethod
131
+ def action(cls, player: Player) -> None:
132
+ for _ in range(2):
133
+ player.hand.append(player.take_one_card_from_deck())
134
+ player.actions_left += 1
135
+
136
+
137
+ class Market(Card):
138
+ name = "Marché"
139
+ cost = 5
140
+ is_action = True
141
+
142
+ @classmethod
143
+ def action(cls, player: Player) -> None:
144
+ player.hand.append(player.take_one_card_from_deck())
145
+ player.actions_left += 1
146
+ player.purchases_left += 1
147
+ player.money += 1
148
+
149
+
150
+ class Province(Card):
151
+ name = "Province"
152
+ cost = 8
153
+ is_kingdom = False
154
+
155
+
156
+ class Silver(Card):
157
+ name = "Argent"
158
+ cost = 4
159
+ money = 2
160
+ is_kingdom = False
161
+ is_money = True
162
+
163
+
164
+ class Smithy(Card):
165
+ name = "Forgeron"
166
+ cost = 4
167
+ is_action = True
168
+
169
+ @classmethod
170
+ def action(cls, player: Player) -> None:
171
+ for _ in range(3):
172
+ player.hand.append(player.take_one_card_from_deck())
173
+
174
+
175
+ class Village(Card):
176
+ name = "Village"
177
+ cost = 3
178
+ is_action = True
179
+
180
+ @classmethod
181
+ def action(cls, player: Player) -> None:
182
+ player.hand.append(player.take_one_card_from_deck())
183
+ player.actions_left += 2
184
+
185
+
186
+ class Woodcutter(Card):
187
+ name = "Bucheron"
188
+ cost = 3
189
+ is_action = True
190
+
191
+ @classmethod
192
+ def action(cls, player: Player) -> None:
193
+ player.purchases_left += 1
194
+ player.money += 2
195
+
196
+
197
+ actions_card_name: set[CardName] = {
198
+ name.lower()
199
+ for name, class_ in inspect.getmembers(sys.modules[__name__], inspect.isclass)
200
+ if issubclass(class_, Card) and class_.name != "Unknown" and class_.is_action
201
+ }
202
+
203
+ money_card_name: set[CardName] = {CardName.COPPER, CardName.SILVER, CardName.GOLD}
204
+
205
+
206
+ class CardContainer:
207
+ """Storage for card."""
208
+
209
+ def __init__(self) -> None:
210
+ self._quantities: dict[CardName, int] = defaultdict(int)
211
+ self._cards: list[CardName] = []
212
+
213
+ def __getattr__(self, attribute: str) -> int:
214
+ if attribute.endswith("_qty"):
215
+ card_name = attribute[:-4]
216
+ return self._quantities[CardName[card_name.upper()]]
217
+ raise AttributeError
218
+
219
+ def __repr__(self) -> str:
220
+ return str(self._cards)
221
+
222
+ def append(self, card_name: CardName) -> None:
223
+ self._cards.append(card_name)
224
+ self._quantities[card_name] += 1
225
+
226
+ def append_several(self, qty: int, card_name: CardName) -> None:
227
+ for _ in range(qty):
228
+ self.append(card_name)
229
+
230
+ def remove(self, card_name: CardName) -> None:
231
+ self._quantities[card_name] -= 1
232
+ self._cards.remove(card_name)
233
+
234
+ def shuffle(self) -> None:
235
+ random.shuffle(self._cards)
236
+
237
+ def contains_action(self) -> bool:
238
+ return any(
239
+ card_name in actions_card_name
240
+ for card_name, qty in self._quantities.items()
241
+ if qty > 0
242
+ )
243
+
244
+ @property
245
+ def action_cards(self) -> CardContainer:
246
+ ret = CardContainer()
247
+ for card_name, qty in self._quantities.items():
248
+ if card_name in actions_card_name:
249
+ ret.append_several(qty, card_name)
250
+ return ret
251
+
252
+ def contains_money(self) -> bool:
253
+ return any(
254
+ card_name in money_card_name
255
+ for card_name, qty in self._quantities.items()
256
+ if qty > 0
257
+ )
258
+
259
+ @property
260
+ def money_cards(self) -> CardContainer:
261
+ ret = CardContainer()
262
+ for card_name, qty in self._quantities.items():
263
+ if card_name in money_card_name:
264
+ ret.append_several(qty, card_name)
265
+ return ret
266
+
267
+ @property
268
+ def money(self) -> int:
269
+ return (
270
+ 1 * self._quantities.get(CardName.COPPER, 0)
271
+ + 2 * self._quantities.get(CardName.SILVER, 0)
272
+ + 3 * self._quantities.get(CardName.GOLD, 0)
273
+ )
274
+
275
+ def pop(self, index: int = -1) -> CardName:
276
+ card_name = self._cards.pop(index)
277
+ self._quantities[card_name] -= 1
278
+ return card_name
279
+
280
+ def __contains__(self, card_name: CardName) -> bool:
281
+ return self._quantities.get(card_name, 0) > 0
282
+
283
+ def __len__(self) -> int:
284
+ return len(self._cards)
285
+
286
+ def __getitem__(self, index: int) -> CardName:
287
+ return self._cards[index]
288
+
289
+ def quantity(self, card_name: CardName) -> int:
290
+ return self._quantities.get(card_name, 0)
291
+
292
+ def sort(self, key: Callable, *, reverse: bool = False) -> None:
293
+ self._cards.sort(key=key, reverse=reverse)
294
+
295
+ def empty_to(self, other: CardContainer) -> None:
296
+ while self:
297
+ card_name = self.pop(0)
298
+ other.append(card_name)
@@ -0,0 +1 @@
1
+ MAX_NB_PLAYERS = 4
@@ -0,0 +1,26 @@
1
+ class InvalidCommandError(Exception):
2
+ pass
3
+
4
+
5
+ class AddPlayerDuringGameError(InvalidCommandError):
6
+ pass
7
+
8
+
9
+ class InvalidActionError(InvalidCommandError):
10
+ pass
11
+
12
+
13
+ class ActionDuringBuyError(InvalidCommandError):
14
+ pass
15
+
16
+
17
+ class MissingCardError(InvalidCommandError):
18
+ pass
19
+
20
+
21
+ class InvalidBuyError(InvalidCommandError):
22
+ pass
23
+
24
+
25
+ class NotEnoughMoneyError(InvalidCommandError):
26
+ pass
@@ -0,0 +1,65 @@
1
+ import inspect
2
+ import random
3
+
4
+ import dopynion.cards
5
+ from dopynion.cards import Card, CardContainer, CardName
6
+ from dopynion.constants import MAX_NB_PLAYERS
7
+ from dopynion.exceptions import (
8
+ AddPlayerDuringGameError,
9
+ InvalidCommandError,
10
+ )
11
+ from dopynion.player import Player
12
+
13
+
14
+ class Game:
15
+ def __init__(self) -> None:
16
+ self.players: list[Player] = []
17
+ self.started = False
18
+ self.stock = CardContainer()
19
+ self.stock.append_several(30, CardName.GOLD)
20
+ self.stock.append_several(40, CardName.SILVER)
21
+ self.stock.append_several(60, CardName.COPPER)
22
+ self.stock.append_several(12, CardName.ESTATE)
23
+ self.stock.append_several(12, CardName.DUCHY)
24
+ self.stock.append_several(12, CardName.PROVINCE)
25
+ self.stock.append_several(30, CardName.CURSE)
26
+
27
+ def __getattr__(self, name: str) -> int:
28
+ if name.endswith("_qty"):
29
+ return getattr(self.stock, name)
30
+ raise AttributeError
31
+
32
+ def add_player(self, player: Player) -> None:
33
+ if self.started:
34
+ raise AddPlayerDuringGameError
35
+ if len(self.players) < MAX_NB_PLAYERS:
36
+ self.players.append(player)
37
+ player.game = self
38
+ self.copper_qty -= 7
39
+ else:
40
+ msg = f"At most {MAX_NB_PLAYERS} players"
41
+ raise InvalidCommandError(msg)
42
+
43
+ def start(self) -> None:
44
+ self.started = True
45
+ if len(self.players) <= 2: # noqa: PLR2004
46
+ self.estate_qty = 8
47
+ self.duchy_qty = 8
48
+ self.province_qty = 8
49
+ self.curse_qty = 10
50
+ elif len(self.players) == 3: # noqa: PLR2004
51
+ self.curse_qty = 20
52
+ possible_kingdoms: list[CardName] = [
53
+ name.lower()
54
+ for name, class_ in inspect.getmembers(dopynion.cards, inspect.isclass)
55
+ if issubclass(class_, Card)
56
+ and class_.name != "Unknown"
57
+ and class_.is_kingdom
58
+ ]
59
+ for _ in range(10):
60
+ if not possible_kingdoms:
61
+ break # TODO à virer quand on aura plus de 10 possibles
62
+ card_name = random.choice(possible_kingdoms) # noqa: S311
63
+ self.stock.append_several(10, card_name)
64
+ # TODO pour jardin c'est particulier, cf bas de la page 2
65
+ possible_kingdoms.remove(card_name)
@@ -0,0 +1,121 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from dopynion.cards import Card, CardContainer, CardName
4
+ from dopynion.exceptions import (
5
+ ActionDuringBuyError,
6
+ InvalidActionError,
7
+ InvalidBuyError,
8
+ NotEnoughMoneyError,
9
+ )
10
+
11
+ if TYPE_CHECKING:
12
+ import dopynion.game
13
+
14
+
15
+ class State:
16
+ ACTION = 1
17
+ BUY = 2
18
+ ADJUST = 3
19
+
20
+
21
+ class Player:
22
+ def __init__(self, name: str) -> None:
23
+ self.game: dopynion.game.Game = None
24
+ self.name = name
25
+ self.deck = CardContainer()
26
+ self.deck.append_several(7, CardName.COPPER)
27
+ self.deck.append_several(3, CardName.ESTATE)
28
+ self.deck.shuffle()
29
+ self.hand = CardContainer()
30
+ self.discard = CardContainer()
31
+ self.played_cards = CardContainer()
32
+ self.actions_left: int = 0
33
+ self.purchases_left: int = 0
34
+ self.money: int = 0
35
+ self.state = State.ACTION
36
+ self._adjust()
37
+
38
+ def __repr__(self) -> str:
39
+ ret = f"Player: {self.name}\n"
40
+ ret += f" - actions left: {self.actions_left}, "
41
+ ret += f"purchases left: {self.purchases_left}, "
42
+ ret += f"money left: {self.money + self.hand.money}\n"
43
+ ret += f" - hand: {self.hand}\n"
44
+ ret += f" - played cards: {self.played_cards}\n"
45
+ ret += f" - deck: {self.deck}\n"
46
+ ret += f" - discard: {self.discard}\n"
47
+ return ret
48
+
49
+ def _check_for_action_to_buy_transition(self) -> None:
50
+ if not self.hand.contains_action() or not self.actions_left:
51
+ self.actions_left = 0
52
+ self.state = State.BUY
53
+
54
+ def _check_for_buy_to_adjust_transition(self) -> None:
55
+ if not self.hand.contains_money() or not self.purchases_left:
56
+ self.purchases_left = 0
57
+ self.state = State.ADJUST
58
+
59
+ def start_turn(self) -> None:
60
+ self.state = State.ACTION
61
+ self.actions_left = 1
62
+ self.purchases_left = 1
63
+ self.money = 0
64
+ self._check_for_action_to_buy_transition()
65
+
66
+ def end_turn(self) -> None:
67
+ self._adjust()
68
+
69
+ def take_one_card_from_deck(self) -> CardName:
70
+ if not self.deck:
71
+ self.discard.empty_to(self.deck)
72
+ self.deck.shuffle()
73
+ return self.deck.pop(0)
74
+
75
+ def _adjust(self) -> None:
76
+ self.played_cards.empty_to(self.discard)
77
+ self.hand.empty_to(self.discard)
78
+ for _ in range(5):
79
+ self.hand.append(self.take_one_card_from_deck())
80
+ self.state = State.ADJUST
81
+
82
+ def _prepare_money(self, money: int) -> None:
83
+ money_cards = self.hand.money_cards
84
+ money_cards.sort(key=lambda card_name: Card.types[card_name].money)
85
+ while self.money < money:
86
+ if not money_cards:
87
+ break
88
+ money_card = money_cards.pop(0)
89
+ self.money += Card.types[money_card].money
90
+ self.hand.remove(money_card)
91
+ self.played_cards.append(money_card)
92
+
93
+ def buy(self, card_name: CardName) -> None:
94
+ print(f"> BUY {card_name} [{self.name}]")
95
+ quantity = getattr(self.game.stock, card_name + "_qty")
96
+ if not quantity:
97
+ raise InvalidBuyError(card_name)
98
+ card = Card.types[card_name]
99
+ if self.money + self.hand.money < card.cost:
100
+ raise NotEnoughMoneyError
101
+ self._prepare_money(card.cost)
102
+ self.money -= card.cost
103
+ self.game.stock.remove(card_name)
104
+ self.discard.append(card_name)
105
+ self.purchases_left -= 1
106
+ self._check_for_buy_to_adjust_transition()
107
+
108
+ def action(self, card_name: CardName) -> None:
109
+ print(f"> ACTION {card_name} [{self.name}]")
110
+ if self.state != State.ACTION:
111
+ raise ActionDuringBuyError(card_name)
112
+ if card_name not in self.hand:
113
+ raise InvalidActionError(card_name)
114
+ try:
115
+ Card.types[card_name].action(self)
116
+ except NotImplementedError as err:
117
+ raise InvalidActionError(card_name) from err
118
+ self.actions_left -= 1
119
+ self.hand.remove(card_name)
120
+ self.played_cards.append(card_name)
121
+ self._check_for_action_to_buy_transition()
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.1
2
+ Name: dopynion
3
+ Version: 0.1.0
4
+ Summary: Dominion
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+
9
+ # dopynion
10
+ Dominion in Python
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ dopynion/__init__.py
5
+ dopynion/__main__.py
6
+ dopynion/cards.py
7
+ dopynion/constants.py
8
+ dopynion/exceptions.py
9
+ dopynion/game.py
10
+ dopynion/player.py
11
+ dopynion.egg-info/PKG-INFO
12
+ dopynion.egg-info/SOURCES.txt
13
+ dopynion.egg-info/dependency_links.txt
14
+ dopynion.egg-info/top_level.txt
15
+ tests/test_game.py
16
+ tests/test_player.py
@@ -0,0 +1 @@
1
+ dopynion
@@ -0,0 +1,54 @@
1
+ [project]
2
+ name = "dopynion"
3
+ version = "0.1.0"
4
+ description = "Dominion"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = []
8
+
9
+ [tool.coverage.run]
10
+ omit = ["dopynion/__main__.py"]
11
+
12
+ [tool.ruff]
13
+ target-version = "py310"
14
+ output-format = "concise"
15
+ fix = true
16
+
17
+ [tool.ruff.lint]
18
+ preview = true
19
+ select = ["ALL"]
20
+ ignore = [
21
+ "CPY001",
22
+ "D1",
23
+ "D203",
24
+ "D212",
25
+ "FIX002",
26
+ "T201",
27
+ "TD002",
28
+ "TD003",
29
+ "TD004",
30
+ ]
31
+ fixable = ["ALL"]
32
+ unfixable = []
33
+
34
+ [tool.ruff.lint.extend-per-file-ignores]
35
+ "tests/**/*.py" = [
36
+ "S101", # asserts allowed in tests...
37
+ "ARG", # Unused function args -> fixtures nevertheless are functionally relevant...
38
+ "FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize()
39
+ "PLR2004", # Magic value used in comparison, ...
40
+ "INP001", # Add an `__init__.py`
41
+ ]
42
+
43
+ [tool.ruff.format]
44
+ preview = true
45
+ quote-style = "double"
46
+ indent-style = "space"
47
+ docstring-code-format = true
48
+
49
+ [tool.uv]
50
+ dev-dependencies = ["pytest-cov>=5.0.0", "pytest>=8.3.3", "ruff>=0.6.8"]
51
+
52
+ [tool.pytest.ini_options]
53
+ minversion = "8.0"
54
+ addopts = "-vvv --cov=dopynion --cov-report html --cov-branch"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,92 @@
1
+ import pytest
2
+
3
+ from dopynion.exceptions import (
4
+ AddPlayerDuringGameError,
5
+ InvalidCommandError,
6
+ )
7
+ from dopynion.game import Game
8
+ from dopynion.player import Player
9
+
10
+
11
+ def test_can_add_player() -> None:
12
+ game = Game()
13
+ game.add_player(Player("player 1"))
14
+ game.start()
15
+ assert len(game.players) == 1
16
+
17
+
18
+ def test_cannot_add_player_on_started_game() -> None:
19
+ game = Game()
20
+ game.add_player(Player("player 1"))
21
+ game.start()
22
+ with pytest.raises(AddPlayerDuringGameError):
23
+ game.add_player(Player("2"))
24
+
25
+
26
+ def test_max_nb_players() -> None:
27
+ game = Game()
28
+ game.add_player(Player("1"))
29
+ game.add_player(Player("2"))
30
+ game.add_player(Player("3"))
31
+ game.add_player(Player("4"))
32
+ with pytest.raises(InvalidCommandError):
33
+ game.add_player(Player("5"))
34
+ game.start()
35
+
36
+
37
+ def test_initial_money() -> None:
38
+ game = Game()
39
+ game.add_player(Player("1"))
40
+ game.add_player(Player("2"))
41
+ game.start()
42
+ assert game.gold_qty == 30
43
+ assert game.silver_qty == 40
44
+ assert game.copper_qty == 60 - 7 * 2
45
+
46
+
47
+ def test_initial_estates_2_players() -> None:
48
+ game = Game()
49
+ game.add_player(Player("1"))
50
+ game.add_player(Player("2"))
51
+ game.start()
52
+ assert game.estate_qty == 8
53
+ assert game.duchy_qty == 8
54
+ assert game.province_qty == 8
55
+
56
+
57
+ def test_initial_estates_3_players() -> None:
58
+ game = Game()
59
+ game.add_player(Player("1"))
60
+ game.add_player(Player("2"))
61
+ game.add_player(Player("3"))
62
+ game.start()
63
+ assert game.estate_qty == 12
64
+ assert game.duchy_qty == 12
65
+ assert game.province_qty == 12
66
+
67
+
68
+ def test_initial_malediction_2_players() -> None:
69
+ game = Game()
70
+ game.add_player(Player("1"))
71
+ game.add_player(Player("2"))
72
+ game.start()
73
+ assert game.curse_qty == 10
74
+
75
+
76
+ def test_initial_malediction_3_players() -> None:
77
+ game = Game()
78
+ game.add_player(Player("1"))
79
+ game.add_player(Player("2"))
80
+ game.add_player(Player("3"))
81
+ game.start()
82
+ assert game.curse_qty == 20
83
+
84
+
85
+ def test_initial_malediction_4_players() -> None:
86
+ game = Game()
87
+ game.add_player(Player("1"))
88
+ game.add_player(Player("2"))
89
+ game.add_player(Player("3"))
90
+ game.add_player(Player("4"))
91
+ game.start()
92
+ assert game.curse_qty == 30
@@ -0,0 +1,48 @@
1
+ import pytest
2
+
3
+ from dopynion.cards import CardName
4
+ from dopynion.exceptions import (
5
+ ActionDuringBuyError,
6
+ InvalidActionError,
7
+ )
8
+ from dopynion.player import Player, State
9
+
10
+
11
+ def test_initial_deck() -> None:
12
+ player = Player("Toto")
13
+ assert len(player.deck) + len(player.hand) == 10
14
+ assert player.deck.copper_qty + player.hand.copper_qty == 7
15
+ assert player.deck.estate_qty + player.hand.estate_qty == 3
16
+
17
+
18
+ def test_make_hand_enough_cards_in_deck() -> None:
19
+ player = Player("toto")
20
+ assert len(player.hand) == 5
21
+ assert len(player.deck) == 5
22
+
23
+
24
+ def test_make_hand_not_enough_cards_in_deck() -> None:
25
+ pass # TODO add the test
26
+
27
+
28
+ def test_unknown_action() -> None:
29
+ player = Player("toto")
30
+ player.start_turn()
31
+ player.state = State.ACTION
32
+ with pytest.raises(InvalidActionError):
33
+ player.action("FooBar")
34
+
35
+
36
+ def test_invalid_action() -> None:
37
+ player = Player("toto")
38
+ player.start_turn()
39
+ with pytest.raises(ActionDuringBuyError):
40
+ player.action(CardName.SMITHY)
41
+
42
+
43
+ def test_invalid_action_when_no_corresponding_card() -> None:
44
+ player = Player("toto")
45
+ player.start_turn()
46
+ player.state = State.ACTION
47
+ with pytest.raises(InvalidActionError):
48
+ player.action(CardName.VILLAGE)