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.
@@ -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,2 @@
1
+ # Blackjack
2
+ A node.js Blackjack library
@@ -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,2 @@
1
+ from deck.card import Card
2
+ from deck.deck import Deck
@@ -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,3 @@
1
+ from blackjack_cli.models.stats_model import BlackjackStats
2
+
3
+ __all__ = ["BlackjackStats"]
@@ -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)