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 +28 -0
- dopynion-0.1.0/PKG-INFO +10 -0
- dopynion-0.1.0/README.md +2 -0
- dopynion-0.1.0/dopynion/__init__.py +0 -0
- dopynion-0.1.0/dopynion/__main__.py +41 -0
- dopynion-0.1.0/dopynion/cards.py +298 -0
- dopynion-0.1.0/dopynion/constants.py +1 -0
- dopynion-0.1.0/dopynion/exceptions.py +26 -0
- dopynion-0.1.0/dopynion/game.py +65 -0
- dopynion-0.1.0/dopynion/player.py +121 -0
- dopynion-0.1.0/dopynion.egg-info/PKG-INFO +10 -0
- dopynion-0.1.0/dopynion.egg-info/SOURCES.txt +16 -0
- dopynion-0.1.0/dopynion.egg-info/dependency_links.txt +1 -0
- dopynion-0.1.0/dopynion.egg-info/top_level.txt +1 -0
- dopynion-0.1.0/pyproject.toml +54 -0
- dopynion-0.1.0/setup.cfg +4 -0
- dopynion-0.1.0/tests/test_game.py +92 -0
- dopynion-0.1.0/tests/test_player.py +48 -0
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.
|
dopynion-0.1.0/PKG-INFO
ADDED
dopynion-0.1.0/README.md
ADDED
|
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,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
|
+
|
|
@@ -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"
|
dopynion-0.1.0/setup.cfg
ADDED
|
@@ -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)
|