今天開始著手處理對手模型訓練所需的數據。說實話,數據處理一直是我比較抗拒的部分 - 總覺得又繁瑣又無趣。但經過這幾年的經驗,我越來越清楚意識到:在機器學習中,數據的品質往往決定了成敗。就算你用了最先進的模型架構,餵進去的數據品質不佳,最終結果還是不會理想。
有趣的是,當我開始處理iPoker的XML格式牌局記錄時,發現事情並沒有想像中困難。回想幾年前第一次看到這些複雜的牌局記錄時,光是那些看不懂的程式碼就讓我望而卻步。但現在有了這幾年寫程式的經驗,再加上語言模型的協助,理解這些XML結構反而變得輕鬆許多。這種進步讓我感到欣慰 - 至少證明這幾年的摸索沒有白費。
目前的重點是要從這些牌局記錄中提取出有意義的數據,建立一個能預測玩家行為傾向的回歸模型。具體來說,就是分析在特定場景下(比如特定的牌面、底池大小、玩家位置等),玩家選擇raise、call或fold的機率分布。這讓我想到需要仔細梳理每個決策點的場景資訊,確保模型能"看到"玩家當下可見的所有資訊。
snapshot = { "round_no": round_no, "current_street": current_street, "blinds": blinds_info, "player_positions": positions, "player_stacks": stacks, "pot_size": pot_size, "board_cards": board_cards, "previous_actions": actions_history, "players_remaining": active_players, "is_button": is_button }
處理showdown資訊可能需要另外考慮 - 我還在思考是否該將其整合進同一個模型,還是分開處理會比較合適。這部分可能需要再多做一些研究和實驗。
經過今天的工作,我發現自己對數據處理的態度似乎有了微妙的改變。雖然還是不能說特別喜歡這個過程,但至少不再覺得那麼困難和可怕了。或許這就是所謂的成長吧 - 當你逐漸掌握了工具和方法,曾經令人生畏的事物也會變得平常。
這是我用來處理iPoker Hand History的程式碼:
import xml.etree.ElementTree as ET
import json
import re
def safe_float(text):
"""Convert a string to float after removing commas."""
if text is None:
return 0.0
return float(re.sub(r",", "", text.strip()))
def parse_decision_logs(root, hero):
"""
Parse the XML tree and, for every opponent decision (action) in rounds 1 and later
(preflop and beyond), produce a snapshot log capturing the game state at the moment
BEFORE the action is processed.
In this snapshot:
- The snapshot does NOT include any actions from round 0 (blinds/antes).
- The re-indexed action counter is incremented for every action in rounds ≥ 1,
so that no action gets a number of 0.
- Only opponent actions are logged (the hero's actions are skipped).
- The snapshot shows the acting opponent's hole cards (if available); the hero's
or other players' hole cards are not revealed in the snapshot.
"""
logs = []
for game in root.findall('game'):
general = game.find('general')
if general is None:
continue
# --- Extract blinds and ante ---
small_blind_elem = general.find('smallblind')
big_blind_elem = general.find('bigblind')
ante_elem = general.find('ante')
small_blind = safe_float(small_blind_elem.text) if small_blind_elem is not None else 0.0
big_blind = safe_float(big_blind_elem.text) if big_blind_elem is not None else 0.0
ante = safe_float(ante_elem.text) if ante_elem is not None else 0.0
# --- Process players ---
players = {} # key: player name, value: player info
active_players = {} # tracks whether a player is still in the hand
players_elem = general.find('players')
if players_elem is None:
continue
for player_elem in players_elem.findall('player'):
name = player_elem.attrib.get('name')
seat = int(player_elem.attrib.get('seat'))
is_dealer = (player_elem.attrib.get('dealer') == "1")
chips = safe_float(player_elem.attrib.get('chips'))
bet = safe_float(player_elem.attrib.get('bet'))
win = safe_float(player_elem.attrib.get('win'))
players[name] = {
'seat': seat,
'is_dealer': is_dealer,
'chips': chips,
'bet': bet,
'win': win
}
active_players[name] = True
# --- Determine relative positions ---
# Sort players by seat number.
sorted_players = sorted(players.items(), key=lambda item: item[1]['seat'])
# Find the dealer (button). If none is marked, assume the first player is the dealer.
dealer_index = None
for i, (name, info) in enumerate(sorted_players):
if info['is_dealer']:
dealer_index = i
break
if dealer_index is None:
dealer_index = 0
button_player = sorted_players[dealer_index][0]
small_blind_player = sorted_players[(dealer_index + 1) % len(sorted_players)][0]
big_blind_player = sorted_players[(dealer_index + 2) % len(sorted_players)][0]
# Assign relative position codes: small blind = 0, big blind = 1, button = 2, others = 3.
player_positions = {}
for name in players:
if name == small_blind_player:
players[name]['position_relative'] = 0
elif name == big_blind_player:
players[name]['position_relative'] = 1
elif name == button_player:
players[name]['position_relative'] = 2
else:
players[name]['position_relative'] = 3
player_positions[name] = players[name]['position_relative']
# Skip this game if the hero is not present.
if hero not in players:
continue
# --- Initialize state variables ---
pot_size = 0.0
cumulative_actions = [] # will contain actions from rounds >= 1 only
board_cards = [] # community cards seen so far
pocket_cards = {} # updated when encountering <cards type="Pocket">
snapshot_action_counter = 0 # counter for actions in rounds >= 1
# --- Process rounds in order (sorted by round number) ---
rounds = game.findall('round')
rounds = sorted(rounds, key=lambda r: int(r.attrib.get('no')))
for r in rounds:
round_no = int(r.attrib.get('no'))
for elem in r:
if elem.tag == 'cards':
card_type = elem.attrib.get('type')
if card_type == 'Pocket':
# Record pocket cards for the given player.
player = elem.attrib.get('player')
text = elem.text.strip() if elem.text else ""
cards = text.split()
# If cards are hidden (e.g., "X X"), record as unknown.
if any(card.upper().startswith("X") for card in cards):
pocket_cards[player] = ["unknown", "unknown"]
else:
pocket_cards[player] = cards
else:
# Community cards (flop, turn, river)
cards = elem.text.split() if elem.text else []
board_cards.extend(cards)
elif elem.tag == 'action':
# Build an action dictionary.
action_details = {
'round': round_no,
'player': elem.attrib.get('player'),
'action_type': int(elem.attrib.get('type')),
'action_sum': safe_float(elem.attrib.get('sum'))
}
# For actions in round 0 (blinds/antes), update state only (do not record them).
if round_no < 1:
pot_size += action_details['action_sum']
# (If desired, you could update pocket_cards from round 0 as well.)
continue
# For actions in rounds >= 1, first assign a new re-indexed action number.
snapshot_action_counter += 1
# Copy the action_details and add the new action number.
action_details['action_no'] = snapshot_action_counter
# If the acting player is not the hero, produce a snapshot BEFORE processing the action.
# (The snapshot’s "previous_actions" will be the cumulative_actions so far.)
if action_details['player'] != hero:
is_button = (action_details['player'] == button_player)
actor_cards = pocket_cards.get(action_details['player'], ["unknown", "unknown"])
# Build the snapshot.
snapshot = {
"gamecode": game.attrib.get("gamecode"),
"round_no": round_no,
"current_street": (
"preflop" if round_no == 1 else
"flop" if round_no == 2 else
"turn" if round_no == 3 else
"river" if round_no == 4 else "unknown"
),
"blinds": {
"small_blind": small_blind,
"big_blind": big_blind,
"ante": ante
},
"player_positions": player_positions,
"player_stacks": {name: players[name]['chips'] for name in players},
"pot_size": pot_size, # state BEFORE processing this action
"board_cards": board_cards.copy(),
"previous_actions": cumulative_actions.copy(), # only round>=1 actions so far
"action": action_details.copy(),
"players_remaining": sum(1 for active in active_players.values() if active),
"is_button": is_button,
"actor_hole_cards": pocket_cards.get(action_details['player'], ["unknown", "unknown"])
}
logs.append(snapshot)
# Now update the state: add this action to the cumulative history.
cumulative_actions.append(action_details)
pot_size += action_details['action_sum']
# Mark the actor as inactive if the action is a fold (we assume type==0 means fold).
if action_details['action_type'] == 0:
active_players[action_details['player']] = False
return logs
if __name__ == '__main__':
try:
# Parse the XML file (adjust 'test.xml' to your filename)
tree = ET.parse('test.xml')
root = tree.getroot()
# Determine the hero's nickname from the session-level <nickname> element.
session_general = root.find('general')
if session_general is not None and session_general.find('nickname') is not None:
hero = session_general.find('nickname').text.strip()
else:
hero = "" # fallback if not found
# Parse opponent decision snapshots (skipping hero actions and round 0 actions)
logs = parse_decision_logs(root, hero)
print(json.dumps(logs, indent=4))
except ET.ParseError as e:
print("Error parsing XML file:", e)