blackjack-cli 0.3.1__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.
- blackjack_cli-0.3.1/PKG-INFO +20 -0
- blackjack_cli-0.3.1/README.md +2 -0
- blackjack_cli-0.3.1/pyproject.toml +43 -0
- blackjack_cli-0.3.1/src/blackjack_cli/__init__.py +2 -0
- blackjack_cli-0.3.1/src/blackjack_cli/blackjack.py +137 -0
- blackjack_cli-0.3.1/src/blackjack_cli/config.py +54 -0
- blackjack_cli-0.3.1/src/blackjack_cli/console.py +213 -0
- blackjack_cli-0.3.1/src/blackjack_cli/models/__init__.py +3 -0
- blackjack_cli-0.3.1/src/blackjack_cli/models/stats_model.py +60 -0
- blackjack_cli-0.3.1/src/blackjack_cli/stats.py +64 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: blackjack-cli
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: This is a cli-based blackjack game! Woo!
|
|
5
|
+
License: GPLv3
|
|
6
|
+
Author: Vlek
|
|
7
|
+
Author-email: derek@mccammond.org
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: License :: Other/Proprietary License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
|
|
15
|
+
Requires-Dist: rich-click (>=1.6.1,<2.0.0)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# Blackjack
|
|
19
|
+
A node.js Blackjack library
|
|
20
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "blackjack-cli"
|
|
3
|
+
version = "0.3.1"
|
|
4
|
+
description = "This is a cli-based blackjack game! Woo!"
|
|
5
|
+
authors = ["Vlek <derek@mccammond.org>"]
|
|
6
|
+
license = "GPLv3"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.10"
|
|
11
|
+
rich-click = "^1.6.1"
|
|
12
|
+
pyyaml = "^6.0.1"
|
|
13
|
+
|
|
14
|
+
[tool.poetry.group.dev.dependencies]
|
|
15
|
+
black = "^24.4.2"
|
|
16
|
+
pytest = "^8.2.2"
|
|
17
|
+
coverage = "^7.3.1"
|
|
18
|
+
pre-commit-hooks = "^4.4.0"
|
|
19
|
+
|
|
20
|
+
[tool.poetry.scripts]
|
|
21
|
+
blackjack = "blackjack_cli.console:bj"
|
|
22
|
+
bj = "blackjack_cli.console:bj"
|
|
23
|
+
hit = "blackjack_cli.console:hit"
|
|
24
|
+
stand = "blackjack_cli.console:stand"
|
|
25
|
+
stay = "blackjack_cli.console:stand"
|
|
26
|
+
|
|
27
|
+
[tool.coverage.paths]
|
|
28
|
+
source = ["src", "*/site-packages"]
|
|
29
|
+
tests = ["tests", "*/tests"]
|
|
30
|
+
|
|
31
|
+
[tool.coverage.run]
|
|
32
|
+
parallel = true
|
|
33
|
+
branch = true
|
|
34
|
+
source = ["blackjack-cli"]
|
|
35
|
+
omit = ["tests"]
|
|
36
|
+
|
|
37
|
+
[tool.coverage.report]
|
|
38
|
+
show_missing = false
|
|
39
|
+
fail_under = 90
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["poetry-core"]
|
|
43
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This is the base logic file for all of the Blackjack game code.
|
|
3
|
+
|
|
4
|
+
This is going to house things like:
|
|
5
|
+
- What is a deck?
|
|
6
|
+
- What is a player/dealer?
|
|
7
|
+
- Turn order
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
from deck import Deck, Card, CardHand
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def blackjackScoringStrategy(cards: list[Card]) -> int:
|
|
17
|
+
"""Returns the score of a given list of cards."""
|
|
18
|
+
|
|
19
|
+
# We do a shallow copy so that we do not mess up the order.
|
|
20
|
+
cards = cards.copy()
|
|
21
|
+
|
|
22
|
+
def aceLastSorting(key: Card) -> int:
|
|
23
|
+
"""Puts aces last in a list of cards when sorting."""
|
|
24
|
+
if key.value == Card.Value.ACE:
|
|
25
|
+
return 1
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
cards.sort(key=aceLastSorting)
|
|
29
|
+
|
|
30
|
+
result: int = 0
|
|
31
|
+
|
|
32
|
+
for card in cards:
|
|
33
|
+
if isinstance(card.value.value, int):
|
|
34
|
+
value: int = card.value.value
|
|
35
|
+
elif card.value == Card.Value.ACE:
|
|
36
|
+
if result + 11 > 21:
|
|
37
|
+
value = 1
|
|
38
|
+
else:
|
|
39
|
+
value = 11
|
|
40
|
+
else:
|
|
41
|
+
value = 10
|
|
42
|
+
|
|
43
|
+
result += value
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GameState(Enum):
|
|
48
|
+
Playing = "playing"
|
|
49
|
+
Win = "wins"
|
|
50
|
+
Lose = "losses"
|
|
51
|
+
Blackjack = "blackjacks"
|
|
52
|
+
Push = "pushes"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Blackjack:
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
self.gameState: GameState = GameState.Playing
|
|
58
|
+
|
|
59
|
+
# Standard blackjack is played with 4 decks
|
|
60
|
+
# instead of just one.
|
|
61
|
+
self.deck: Deck = Deck() * 4
|
|
62
|
+
self.deck.shuffle()
|
|
63
|
+
|
|
64
|
+
self.playersCards: CardHand = CardHand([self.deck.draw(), self.deck.draw()])
|
|
65
|
+
self.dealersCards: CardHand = CardHand([self.deck.draw(), self.deck.draw()])
|
|
66
|
+
|
|
67
|
+
for hand in [self.playersCards, self.dealersCards]:
|
|
68
|
+
hand.setScoringStrategy(blackjackScoringStrategy)
|
|
69
|
+
|
|
70
|
+
playersScore: int = self.playersCards.getScore()
|
|
71
|
+
dealersScore: int = self.dealersCards.getScore()
|
|
72
|
+
|
|
73
|
+
# If one or both blackjack:
|
|
74
|
+
if playersScore == 21:
|
|
75
|
+
self.gameState = GameState.Blackjack
|
|
76
|
+
|
|
77
|
+
if dealersScore == 21:
|
|
78
|
+
self.gameState = GameState.Push
|
|
79
|
+
|
|
80
|
+
def hit(self) -> GameState:
|
|
81
|
+
if self.isGameOver():
|
|
82
|
+
return self.gameState
|
|
83
|
+
|
|
84
|
+
self._drawCard(self.playersCards)
|
|
85
|
+
|
|
86
|
+
newScore: int = self.playersCards.getScore()
|
|
87
|
+
|
|
88
|
+
if newScore > 21:
|
|
89
|
+
self.gameState = GameState.Lose
|
|
90
|
+
elif newScore == 21:
|
|
91
|
+
self.gameState = GameState.Win
|
|
92
|
+
|
|
93
|
+
return self.gameState
|
|
94
|
+
|
|
95
|
+
def stand(self) -> GameState:
|
|
96
|
+
if not self.isGameOver():
|
|
97
|
+
self.performDealersTurn()
|
|
98
|
+
|
|
99
|
+
return self.gameState
|
|
100
|
+
|
|
101
|
+
def performDealersTurn(self) -> Self:
|
|
102
|
+
"""Performs the dealer's moves and finishes the game."""
|
|
103
|
+
playersScore: int = self.playersCards.getScore()
|
|
104
|
+
dealersScore: int = self.dealersCards.getScore()
|
|
105
|
+
|
|
106
|
+
# If the player bust or the dealer got blackjack
|
|
107
|
+
if playersScore > 21 or dealersScore == 21:
|
|
108
|
+
self.gameState = GameState.Lose
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
while (dealersScore := self.dealersCards.getScore()) <= 16:
|
|
112
|
+
self._drawCard(self.dealersCards)
|
|
113
|
+
|
|
114
|
+
dealersScore = self.dealersCards.getScore()
|
|
115
|
+
|
|
116
|
+
if playersScore > dealersScore or dealersScore > 21:
|
|
117
|
+
self.gameState = GameState.Win
|
|
118
|
+
else:
|
|
119
|
+
if playersScore == dealersScore:
|
|
120
|
+
self.gameState = GameState.Push
|
|
121
|
+
else:
|
|
122
|
+
self.gameState = GameState.Lose
|
|
123
|
+
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
def _drawCard(self, hand: CardHand) -> Card | None:
|
|
127
|
+
"""Draws a card and places it into the given hand."""
|
|
128
|
+
newCard: Card | None = self.deck.draw()
|
|
129
|
+
|
|
130
|
+
if newCard is not None:
|
|
131
|
+
hand.add(newCard)
|
|
132
|
+
|
|
133
|
+
return newCard
|
|
134
|
+
|
|
135
|
+
def isGameOver(self) -> bool:
|
|
136
|
+
"""Returns whether the game is already over."""
|
|
137
|
+
return self.gameState != GameState.Playing
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration manager
|
|
3
|
+
|
|
4
|
+
We need to be able to save configurations to a file in a user-friendly
|
|
5
|
+
way that will be retrievable later.
|
|
6
|
+
|
|
7
|
+
If people wish to then save these settings and use them across different
|
|
8
|
+
PCs, they should have that option.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from click import get_app_dir
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DEFAULT_CONFIG_FOLDER: str = get_app_dir("blackjack")
|
|
18
|
+
DEFAULT_CONFIG_FILE_NAME: str = "blackjack.yaml"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Config:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
config_file_path: str = f"{DEFAULT_CONFIG_FOLDER}/{DEFAULT_CONFIG_FILE_NAME}",
|
|
25
|
+
) -> None:
|
|
26
|
+
self._load(config_file_path)
|
|
27
|
+
|
|
28
|
+
def _load(self, config_file_path: str) -> None:
|
|
29
|
+
self.config_file_path: Path = Path(config_file_path).expanduser()
|
|
30
|
+
|
|
31
|
+
self.config_folder_path: Path = self.config_file_path.parent
|
|
32
|
+
|
|
33
|
+
if not self.config_folder_path.exists():
|
|
34
|
+
self.config_folder_path.mkdir()
|
|
35
|
+
|
|
36
|
+
if self.config_file_path.exists():
|
|
37
|
+
with self.config_file_path.open() as config_file:
|
|
38
|
+
self.config: dict[str, object] = yaml.safe_load(config_file)
|
|
39
|
+
else:
|
|
40
|
+
self.config: dict[str, object] = {}
|
|
41
|
+
|
|
42
|
+
def write(self) -> None:
|
|
43
|
+
"""Writes the configuration to the config file."""
|
|
44
|
+
|
|
45
|
+
with open(self.config_file_path, "w") as config_file:
|
|
46
|
+
config_file.write(yaml.dump(self.config))
|
|
47
|
+
|
|
48
|
+
def fileExists(self, filename: str) -> Path | None:
|
|
49
|
+
"""Returns whether the given file name exists in the config folder."""
|
|
50
|
+
potentialFile: Path = self.config_file_path / filename
|
|
51
|
+
|
|
52
|
+
if potentialFile.exists():
|
|
53
|
+
return potentialFile
|
|
54
|
+
return
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entryway into the project for console-based usage.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import rich_click as click
|
|
6
|
+
import pickle
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from random import choice
|
|
9
|
+
|
|
10
|
+
from blackjack_cli.blackjack import Blackjack, GameState
|
|
11
|
+
from blackjack_cli.config import Config
|
|
12
|
+
from blackjack_cli.stats import Stats
|
|
13
|
+
|
|
14
|
+
from deck import CardHand
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
config = Config()
|
|
18
|
+
|
|
19
|
+
STATS_FILE_PATH: Path = config.config_folder_path / "stats.json"
|
|
20
|
+
STATE_FILE_PATH: Path = config.config_folder_path / "gamestate.pickle"
|
|
21
|
+
|
|
22
|
+
playerStats = Stats(STATS_FILE_PATH)
|
|
23
|
+
|
|
24
|
+
isNewGame: bool = not STATE_FILE_PATH.exists()
|
|
25
|
+
|
|
26
|
+
if isNewGame:
|
|
27
|
+
game_instance = Blackjack()
|
|
28
|
+
else:
|
|
29
|
+
with open(STATE_FILE_PATH, "rb") as sf:
|
|
30
|
+
game_instance: Blackjack = pickle.load(sf)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.group()
|
|
34
|
+
def bj() -> None:
|
|
35
|
+
"""Blackjack CLI"""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@bj.command()
|
|
40
|
+
def hit() -> None:
|
|
41
|
+
"""Starts new game or asks dealer for another card."""
|
|
42
|
+
gameState: GameState = game_instance.gameState
|
|
43
|
+
|
|
44
|
+
if not isNewGame:
|
|
45
|
+
gameState = game_instance.hit()
|
|
46
|
+
|
|
47
|
+
if gameState != GameState.Playing:
|
|
48
|
+
__print_end_of_game(game_instance)
|
|
49
|
+
playerStats.save(STATS_FILE_PATH, game_instance.gameState)
|
|
50
|
+
STATE_FILE_PATH.unlink(missing_ok=True)
|
|
51
|
+
else:
|
|
52
|
+
click.echo(
|
|
53
|
+
f"{__get_hands_string(game_instance.playersCards, game_instance.dealersCards, True)} Hit or Stand?"
|
|
54
|
+
)
|
|
55
|
+
__save_state(STATE_FILE_PATH, game_instance)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@bj.command()
|
|
59
|
+
def stand() -> None:
|
|
60
|
+
"""Performs the dealer's turn and ends the game."""
|
|
61
|
+
if isNewGame:
|
|
62
|
+
click.echo("Type 'hit' to start a new game.")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
game_instance.stand()
|
|
66
|
+
|
|
67
|
+
__print_end_of_game(game_instance)
|
|
68
|
+
playerStats.save(STATS_FILE_PATH, game_instance.gameState)
|
|
69
|
+
|
|
70
|
+
STATE_FILE_PATH.unlink(missing_ok=True)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@bj.command()
|
|
74
|
+
def stats() -> None:
|
|
75
|
+
"""Lists the player's game statistics."""
|
|
76
|
+
click.echo("Player stats:")
|
|
77
|
+
|
|
78
|
+
click.echo(playerStats.data)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def __save_state(filepath: Path, gameData: Blackjack) -> None:
|
|
82
|
+
with open(filepath, "wb") as stateFile:
|
|
83
|
+
stateFile.write(pickle.dumps(gameData))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def __print_end_of_game(game: Blackjack) -> None:
|
|
87
|
+
result_text: str = "You lose"
|
|
88
|
+
|
|
89
|
+
match game.gameState:
|
|
90
|
+
case GameState.Win:
|
|
91
|
+
result_text = "You win"
|
|
92
|
+
case GameState.Blackjack:
|
|
93
|
+
result_text = "Blackjack"
|
|
94
|
+
case GameState.Push:
|
|
95
|
+
result_text = "Push"
|
|
96
|
+
|
|
97
|
+
previousGameState: GameState = playerStats.data._last_outcome
|
|
98
|
+
currentStreak: int = playerStats.data._current_streak
|
|
99
|
+
|
|
100
|
+
sass: str = __get_sass(
|
|
101
|
+
game.gameState,
|
|
102
|
+
previousGameState,
|
|
103
|
+
currentStreak,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
click.echo(
|
|
107
|
+
f"{__get_hands_string(game.playersCards, game.dealersCards, False)} {result_text}! {sass}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def __get_hands_string(
|
|
112
|
+
playerHand: CardHand, dealerHand: CardHand, hideDealersSecondCard: bool
|
|
113
|
+
) -> str:
|
|
114
|
+
"""Returns the formatted string for player and dealer."""
|
|
115
|
+
if hideDealersSecondCard:
|
|
116
|
+
dealersCards: str = " ".join([str(dealerHand.cards[0]), "?"])
|
|
117
|
+
dealersScore: str = "?"
|
|
118
|
+
else:
|
|
119
|
+
dealersCards = str(dealerHand)
|
|
120
|
+
dealersScore = __get_score_string(dealerHand)
|
|
121
|
+
|
|
122
|
+
return f"Player: {playerHand} ({playerHand.getScore()}). House: {dealersCards} ({dealersScore})."
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def __get_score_string(hand: CardHand) -> str:
|
|
126
|
+
"""Returns either Blackjack or the score of a hand."""
|
|
127
|
+
handScore = hand.getScore()
|
|
128
|
+
|
|
129
|
+
if handScore == 21 and len(hand.cards) == 2:
|
|
130
|
+
scoreOutput: str = "Blackjack"
|
|
131
|
+
else:
|
|
132
|
+
scoreOutput = str(handScore)
|
|
133
|
+
|
|
134
|
+
return scoreOutput
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def __get_sass(
|
|
138
|
+
gameState: GameState, lastGameOutcome: GameState, currentStreak: int
|
|
139
|
+
) -> str:
|
|
140
|
+
"""Given game state, returns some flavor text to add to the end of game."""
|
|
141
|
+
responseBank: list[str]
|
|
142
|
+
|
|
143
|
+
loseResponseBank: list[str] = [
|
|
144
|
+
"Play again?",
|
|
145
|
+
"The house always wins!",
|
|
146
|
+
"Maybe next time.",
|
|
147
|
+
"💩",
|
|
148
|
+
"Tough luck.",
|
|
149
|
+
"Womp womp",
|
|
150
|
+
"You can turn it around next time.",
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
winResponseBank: list[str] = [
|
|
154
|
+
"Another win?",
|
|
155
|
+
"Next time you'll lose! >:C",
|
|
156
|
+
"I will shuffle better next time. :..<.",
|
|
157
|
+
"🎆",
|
|
158
|
+
"🎉",
|
|
159
|
+
"🔥",
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
pushResponseBank: list[str] = [
|
|
163
|
+
"Draw!",
|
|
164
|
+
"Alright, we will call it a draw.",
|
|
165
|
+
"Maybe you'll win next time?",
|
|
166
|
+
"Give it another shot.",
|
|
167
|
+
"Maybe next time.",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
# This is before the streak is incremented in the stats, so what is saved
|
|
171
|
+
# is our previous streak. Need to increment to include current win/lose
|
|
172
|
+
currentStreak += 1
|
|
173
|
+
|
|
174
|
+
# If a streak continues
|
|
175
|
+
if gameState == lastGameOutcome:
|
|
176
|
+
# Winning streak continues
|
|
177
|
+
if gameState in [GameState.Win, GameState.Blackjack]:
|
|
178
|
+
responseBank = [
|
|
179
|
+
f"Continuing the winning streak to {currentStreak}!",
|
|
180
|
+
f"{currentStreak} times a winner!",
|
|
181
|
+
f"Will you lose after winning {currentStreak} times?",
|
|
182
|
+
] + winResponseBank
|
|
183
|
+
elif gameState == GameState.Push:
|
|
184
|
+
responseBank = [
|
|
185
|
+
f"{currentStreak} pushes? That's rare.",
|
|
186
|
+
] + pushResponseBank
|
|
187
|
+
# Losing streak continues.
|
|
188
|
+
# Here's where the real sass is!
|
|
189
|
+
else:
|
|
190
|
+
responseBank = [
|
|
191
|
+
f"You increase your losing streak to {currentStreak}!",
|
|
192
|
+
f"{currentStreak} losses. Maybe you'll win next time?",
|
|
193
|
+
] + loseResponseBank
|
|
194
|
+
|
|
195
|
+
# A losing streak was broken
|
|
196
|
+
elif gameState in [GameState.Win, GameState.Blackjack]:
|
|
197
|
+
responseBank = [
|
|
198
|
+
"You broke your losing streak!",
|
|
199
|
+
f"Losing streak of {currentStreak} broken!",
|
|
200
|
+
"Now to turn this into a winning streak!",
|
|
201
|
+
] + winResponseBank
|
|
202
|
+
|
|
203
|
+
elif gameState == GameState.Push:
|
|
204
|
+
responseBank = pushResponseBank
|
|
205
|
+
|
|
206
|
+
# A winning streak was broken
|
|
207
|
+
else:
|
|
208
|
+
responseBank = [
|
|
209
|
+
"Wining streak broken!",
|
|
210
|
+
"Will this lead to a losing streak?",
|
|
211
|
+
] + loseResponseBank
|
|
212
|
+
|
|
213
|
+
return choice(responseBank)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from blackjack_cli.blackjack import GameState
|
|
3
|
+
from typing_extensions import Self
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BlackjackStats:
|
|
7
|
+
_wins: int = 0
|
|
8
|
+
_losses: int = 0
|
|
9
|
+
_pushes: int = 0
|
|
10
|
+
_blackjacks: int = 0
|
|
11
|
+
_longest_win_streak: int = 0
|
|
12
|
+
_longested_losing_streak: int = 0
|
|
13
|
+
_longest_push_streak: int = 0
|
|
14
|
+
_current_streak: int = 0
|
|
15
|
+
_last_outcome: GameState = GameState.Playing
|
|
16
|
+
_last_played: str = ""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
"""Initializes a BlackjackStats object."""
|
|
20
|
+
|
|
21
|
+
def deserialize(self, data: dict[str, Any]) -> Self:
|
|
22
|
+
"""Imports a json string, updates class variables."""
|
|
23
|
+
|
|
24
|
+
if "_last_outcome" in data:
|
|
25
|
+
data["_last_outcome"] = GameState(data["_last_outcome"])
|
|
26
|
+
|
|
27
|
+
self.__dict__ = data
|
|
28
|
+
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def serialize(self) -> dict[str, Any]:
|
|
32
|
+
"""Exports class data as a saveable dict"""
|
|
33
|
+
|
|
34
|
+
result: dict[str, Any] = self.__dict__
|
|
35
|
+
result["_last_outcome"] = self._last_outcome.value
|
|
36
|
+
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
def __str__(self) -> str:
|
|
40
|
+
"""Returns a string representation of the stats values"""
|
|
41
|
+
result: list[str] = []
|
|
42
|
+
values: dict[str, Any] = self.__dict__
|
|
43
|
+
|
|
44
|
+
keysToOutput: dict[str, str] = {
|
|
45
|
+
"_wins": "Wins",
|
|
46
|
+
"_losses": "Losses",
|
|
47
|
+
"_pushes": "Pushes",
|
|
48
|
+
"_blackjacks": "Blackjacks",
|
|
49
|
+
"_longest_win_streak": "Longest winning streak",
|
|
50
|
+
"_longested_losing_streak": "Longest losing streak",
|
|
51
|
+
"_longest_push_streak": "Longest push streak",
|
|
52
|
+
"_current_streak": "Current streak",
|
|
53
|
+
"_last_played": "Last played",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for key, value in keysToOutput.items():
|
|
57
|
+
if key in values:
|
|
58
|
+
result.append(f"{value}: {values[key]}")
|
|
59
|
+
|
|
60
|
+
return " " + "\n ".join(result)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import json
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
5
|
+
from blackjack_cli.blackjack import GameState
|
|
6
|
+
from blackjack_cli.models import BlackjackStats
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Stats:
|
|
10
|
+
def __init__(self, statsFilePath: Path) -> None:
|
|
11
|
+
"""Initializes a stats collection object."""
|
|
12
|
+
|
|
13
|
+
self.data: BlackjackStats = BlackjackStats()
|
|
14
|
+
|
|
15
|
+
if statsFilePath.exists():
|
|
16
|
+
with open(statsFilePath, "r") as statsFile:
|
|
17
|
+
self.data.deserialize(json.load(statsFile))
|
|
18
|
+
|
|
19
|
+
def save(self, statsFilePath: Path, gameState: GameState) -> None:
|
|
20
|
+
"""Saves the last game's state to the stats file."""
|
|
21
|
+
self.data._last_played = str(date.today())
|
|
22
|
+
|
|
23
|
+
match gameState:
|
|
24
|
+
case winType if winType in [GameState.Win, GameState.Blackjack]:
|
|
25
|
+
if winType == GameState.Win:
|
|
26
|
+
self.data._wins += 1
|
|
27
|
+
else:
|
|
28
|
+
self.data._blackjacks += 1
|
|
29
|
+
|
|
30
|
+
if self.data._last_outcome in [GameState.Win, GameState.Blackjack]:
|
|
31
|
+
self.data._current_streak += 1
|
|
32
|
+
|
|
33
|
+
if self.data._longest_win_streak < self.data._current_streak:
|
|
34
|
+
self.data._longest_win_streak = self.data._current_streak
|
|
35
|
+
else:
|
|
36
|
+
self.data._current_streak = 1
|
|
37
|
+
|
|
38
|
+
case GameState.Lose:
|
|
39
|
+
self.data._losses += 1
|
|
40
|
+
|
|
41
|
+
if self.data._last_outcome == GameState.Lose:
|
|
42
|
+
self.data._current_streak += 1
|
|
43
|
+
|
|
44
|
+
if self.data._longested_losing_streak < self.data._current_streak:
|
|
45
|
+
self.data._longested_losing_streak = self.data._current_streak
|
|
46
|
+
else:
|
|
47
|
+
self.data._current_streak = 1
|
|
48
|
+
|
|
49
|
+
case GameState.Push:
|
|
50
|
+
self.data._pushes += 1
|
|
51
|
+
self.data._current_streak = 0
|
|
52
|
+
|
|
53
|
+
if self.data._last_outcome == GameState.Push:
|
|
54
|
+
self.data._current_streak += 1
|
|
55
|
+
|
|
56
|
+
if self.data._longest_push_streak < self.data._current_streak:
|
|
57
|
+
self.data._longest_push_streak = self.data._current_streak
|
|
58
|
+
else:
|
|
59
|
+
self.data._current_streak = 1
|
|
60
|
+
|
|
61
|
+
self.data._last_outcome = gameState
|
|
62
|
+
|
|
63
|
+
with open(statsFilePath, "w") as statsFile:
|
|
64
|
+
json.dump(self.data.serialize(), statsFile)
|