-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathengine.py
More file actions
180 lines (145 loc) · 6.37 KB
/
Copy pathengine.py
File metadata and controls
180 lines (145 loc) · 6.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import gymnasium as gym
from gymnasium import spaces
import numpy as np
from shoe import BlackjackShoe
class CasinoBlackjackEnv(gym.Env):
def __init__(self, num_decks=6):
super().__init__()
self.shoe = BlackjackShoe(num_decks)
# Actions: 0: Stick, 1: Hit, 2: Double, 3: Split
self.action_space = spaces.Discrete(4)
# Updated Observation: [Scaled Sum, Scaled Dealer, Usable Ace, Can Split, Can Double, Scaled Count]
self.observation_space = spaces.Box(
low=np.array([0.0, 0.0, 0.0, 0.0, 0.0, -1.0]),
high=np.array([1.5, 1.0, 1.0, 1.0, 1.0, 1.0]),
dtype=np.float32
)
def _get_obs(self):
if self.current_hand_idx >= len(self.player_hands):
return np.zeros(6, dtype=np.float32)
hand = self.player_hands[self.current_hand_idx]
p_sum, usable_ace = self._calc_hand_total(hand)
d_card = self.dealer_hand[0]
# Logic for valid actions
can_split = 1.0 if (len(hand) == 2 and hand[0] == hand[1]) else 0.0
can_double = 1.0 if (len(hand) == 2 and p_sum in [9, 10, 11]) else 0.0
# Scale values for NN stability
scaled_p_sum = p_sum / 21.0
scaled_d_card = d_card / 11.0
scaled_count = np.clip(self.shoe.get_count() / 10.0, -1, 1)
return np.array([
scaled_p_sum, scaled_d_card, float(usable_ace),
can_split, can_double, scaled_count
], dtype=np.float32)
def reset(self, seed=None, options=None):
super().reset(seed=seed)
# Dealer gets TWO cards (one is hidden from observation)
self.dealer_hand = [self.shoe.draw(), self.shoe.draw()]
self.player_hands = [[self.shoe.draw(), self.shoe.draw()]]
self.hand_bets = [1.0]
self.current_hand_idx = 0
return self._get_obs(), {}
def step(self, action):
hand = self.player_hands[self.current_hand_idx]
reward = 0.0
terminated = False
# HIT (Action 1)
if action == 1:
hand.append(self.shoe.draw())
if self._calc_hand_total(hand)[0] > 21:
self._move_to_next_hand() # Move to next hand if this one busts
# STICK (Action 0)
elif action == 0:
self._move_to_next_hand()
# DOUBLE DOWN
elif action == 2:
p_sum, _ = self._calc_hand_total(hand)
if len(hand) == 2 and p_sum in [9, 10, 11]:
self.hand_bets[self.current_hand_idx] *= 2
hand.append(self.shoe.draw())
# Check for bust on the one card drawn
if self._calc_hand_total(hand)[0] > 21:
reward = -1.0 * self.hand_bets[self.current_hand_idx]
self._move_to_next_hand()
if self.current_hand_idx >= len(self.player_hands) and reward == 0:
reward = self._get_reward(len(self.player_hands) - 1)
else:
reward = -0.1 # Illegal action penalty
"""
# SPLIT
elif action == 3:
if len(hand) == 2 and hand[0] == hand[1]:
card = hand.pop()
new_hand = [card, self.shoe.draw()]
hand.append(self.shoe.draw())
self.player_hands.append(new_hand)
self.hand_bets.append(1.0)
"""
# CHECK IF ROUND IS OVER
terminated = self.current_hand_idx >= len(self.player_hands)
if terminated:
# Sum rewards for ALL hands once the dealer has finished playing
reward = sum(self._get_reward(i) for i in range(len(self.player_hands)))
return self._get_obs(), reward, terminated, False, {}
def _move_to_next_hand(self):
self.current_hand_idx += 1
if self.current_hand_idx >= len(self.player_hands):
# Check if all hands busted. If so, dealer doesn't even need to play!
all_busted = all(self._calc_hand_total(h)[0] > 21 for h in self.player_hands)
if not all_busted:
self._dealer_play()
return True
return False
def _dealer_play(self):
"""Dealer hits until 17 or bust."""
while True:
d_sum, _ = self._calc_hand_total(self.dealer_hand)
if d_sum >= 17:
break
self.dealer_hand.append(self.shoe.draw())
def _get_reward(self, hand_idx):
"""Calculates reward for a specific hand after dealer has played."""
hand = self.player_hands[hand_idx]
p_sum, _ = self._calc_hand_total(hand)
d_sum, _ = self._calc_hand_total(self.dealer_hand)
bet = self.hand_bets[hand_idx]
# Check for Natural Blackjack (only on 2 cards and total is 21)
is_player_bj = (len(hand) == 2 and p_sum == 21)
is_dealer_bj = (len(self.dealer_hand) == 2 and d_sum == 21)
if is_player_bj:
if is_dealer_bj:
return 0.0 # Blackjack Push
else:
return 1.5 * bet # Natural Blackjack payoff
# Check if dealer has BJ and player just has a regular 21
if is_dealer_bj and p_sum == 21:
return -1.0 * bet
# Standard outcome logic
if p_sum > 21:
return -1.0 * bet
if d_sum > 21:
return 1.0 * bet
if p_sum > d_sum:
return 1.0 * bet
if p_sum < d_sum:
return -1.0 * bet
return 0.0 # Standard Push
def _calc_hand_total(self, hand):
"""
Calculates the best possible sum for a hand.
Returns: (sum, usable_ace)
"""
p_sum = sum(hand)
# Check for 'usable' ace (counting an Ace as 11 instead of 1)
# Note: In our shoe, we draw Aces as 11. If we bust, we convert them to 1.
# If the shoe draws Aces as 1 or 11, adjust here:
# Let's assume Aces are represented as 11 in our shoe for this logic:
usable_ace = False
# If we have an 11 and we're over 21, convert 11 -> 1
# We use a while loop in case there are multiple Aces
temp_hand = list(hand)
while sum(temp_hand) > 21 and 11 in temp_hand:
temp_hand[temp_hand.index(11)] = 1
p_sum = sum(temp_hand)
usable_ace = 11 in temp_hand
return p_sum, usable_ace