重啟撲克機器人之路 -12:當簡化反而帶來更多可能

更新於 發佈於 閱讀時間約 26 分鐘
raw-image

昨天在改進撲克資料的提取過程時,有了一些有趣的領悟。原本想著要把所有細節都考慮進去,結果反而讓自己陷入了一個數據過於複雜的困境。特別是在處理raise action的部分,之前總覺得每個不同的下注大小都應該被當作獨特的數據點來訓練,現在回想起來,這種過度精確的想法反而限制了模型的學習空間。

這讓我想起以前用Hand2Note分析玩家池的經驗 ,在數學以及理論上,30%跟50%的下注差異滿多的,但在實戰中,玩家的range其實並沒有太大的區別。於是我決定將各種下注大小重新分類,簡化成幾個主要類別:小注、中注、大注等。這個決定不僅讓數據更容易處理,更重要的是讓每個類別都有足夠的樣本來進行有意義的分析。

在整理數據的過程中也發現了一些之前忽略的問題。比如pot size的計算方式有誤 - 之前用的是牌局開始時的金額,而忽略已經投入底池的錢。還有玩家ID的部分,原先直接使用玩家名稱導致數據不夠一致,現在改用簡單的ID編號後,數據的一致性提高了不少。

原本打算使用Random Forest Classification來處理這project,感覺這樣可以讓整個流程變得更直接簡單。然而在檢視訓練數據時,意識到poker中previous action的重要性可能遠超過我的預期。這讓我開始思考是否需要一個能更好地處理序列數據的模型。

在與語言模型討論後,決定嘗試將特徵分成兩個分支:一個處理全局遊戲狀態,另一個專門處理動作序列。這個新方案使用了LSTM網絡來處理序列資料,雖然我還不太理解它的運作原理,但至少模型可以開始訓練了,這總是個好的起點。

回顧昨天的進展,發現有時候「簡化」反而能帶來更多可能性。就像之前用OpenHoldem時,總是想要處理所有可能的情況,結果反而讓程式變得難以維護。現在學會了如何在保持核心功能的同時,適度簡化實作方式,這或許才是真正的進步。

接下來還需要深入研究LSTM網絡的原理,雖然現在還是一頭霧水,但至少方向是對的。就像之前寫過的,有時候不需要完全理解所有細節,先讓東西能跑起來,再慢慢改進可能是更好的策略。

更新後的手牌資料提取,適用於iPoker xml hand history:

#!/usr/bin/env python3

import os

import xml.etree.ElementTree as ET

import json

import re



# ------------------------------

# Helper functions

# ------------------------------



def safe_float(text):

try:

return float(re.sub(r"[^\d\.]", "", text))

except Exception:

return 0.0



def street_from_round(round_no):

return {1: "preflop", 2: "flop", 3: "turn", 4: "river"}.get(round_no, "unknown")



def get_hole_cards(game, player):

for r in game.findall('round'):

for elem in r.findall('cards'):

if elem.attrib.get("type") == "Pocket" and elem.attrib.get("player") == player:

if elem.text:

cards = elem.text.split()

if any(c.upper().startswith("X") for c in cards):

return ["unknown", "unknown"]

else:

return cards

return ["unknown", "unknown"]



def simplify_action(action_details, round_no, blinds, pot_before_action):

"""

Simplify the action into a single string category.

Allowed original types:

- 0 → fold

- 3 → call

- 4 → check

- 5, 7, 23 → raise, which are further categorized as follows:

* Preflop (round 1), using the big blind as reference:

2.0×BB to 2.3×BB → "small raise preflop"

2.3×BB to 3.0×BB → "mid raise preflop"

3.0×BB to 4.0×BB → "big raise preflop"

≥ 4.0×BB → "all in preflop"

* Postflop (round > 1), using the pot (before the action) as reference:

< 35% of pot → "small raise postflop"

35% to 70% of pot → "mid raise postflop"

70% to 110% of pot → "big raise postflop"

≥ 110% of pot → "all in postflop"

"""

allowed_types = {0: "fold", 3: "call", 4: "check", 5: "raise", 7: "raise", 23: "raise"}

orig_type = action_details['action_type']

if orig_type not in allowed_types:

return None

base_action = allowed_types[orig_type]

new_action = action_details.copy()

if base_action != "raise":

new_action["simple_action_type"] = base_action

else:

if round_no == 1:

bb = blinds.get("big_blind", 1)

ratio = action_details['action_sum'] / bb if bb != 0 else 0

if ratio < 2.3:

new_action["simple_action_type"] = "small raise preflop"

elif 2.3 <= ratio < 3.0:

new_action["simple_action_type"] = "mid raise preflop"

elif 3.0 <= ratio < 4.0:

new_action["simple_action_type"] = "big raise preflop"

elif ratio >= 4.0:

new_action["simple_action_type"] = "all in preflop"

else:

new_action["simple_action_type"] = "raise"

else:

ratio = action_details['action_sum'] / pot_before_action if pot_before_action > 0 else 0

if ratio < 0.35:

new_action["simple_action_type"] = "small raise postflop"

elif ratio < 0.70:

new_action["simple_action_type"] = "mid raise postflop"

elif ratio < 1.10:

new_action["simple_action_type"] = "big raise postflop"

else:

new_action["simple_action_type"] = "all in postflop"

return new_action



def parse_decision_logs(root, hero):

logs = []

for game in root.findall('game'):

gamecode = game.attrib.get("gamecode", "")

general = game.find('general')

blinds = {

'small_blind': safe_float(general.findtext('smallblind', default="0")),

'big_blind': safe_float(general.findtext('bigblind', default="0")),

'ante': safe_float(general.findtext('ante', default="0"))

}

# Get players element and determine initial stacks.

players_elem = general.find('players')

# Identify small blind and big blind using round 0 actions.

small_blind_player = None

big_blind_player = None

for r in game.findall('round'):

if int(r.attrib.get('no', 0)) == 0:

for child in r:

if child.tag == "action":

if child.attrib.get('type') == "1":

small_blind_player = child.attrib.get('player')

elif child.attrib.get('type') == "2":

big_blind_player = child.attrib.get('player')

break

# Identify the button player from the dealer flag.

button_player = None

for p in players_elem.findall('player'):

if p.attrib.get('dealer','0') == '1':

button_player = p.attrib.get('name')

break

# Create mapping: small blind → 0, big blind → 1, button → 2.

player_positions = {}

if small_blind_player is not None:

player_positions[small_blind_player] = 0

if big_blind_player is not None:

player_positions[big_blind_player] = 1

if button_player is not None:

player_positions[button_player] = 2

# Get initial stacks for the mapped players.

player_stacks = {}

for p in players_elem.findall('player'):

name = p.attrib['name']

if name in player_positions:

player_stacks[name] = safe_float(p.attrib.get('chips', "0"))

active_players = {name: True for name in player_positions}

pot_size = 0.0

board_cards = []

cumulative_actions = [] # simplified actions (each includes "action_round")

snapshot_action_counter = 0

# Track cumulative contributions from each player.

player_contributions = {name: 0.0 for name in player_positions}

for r in game.findall('round'):

round_no = int(r.attrib.get('no', 0))

for child in r:

if child.tag == "cards":

if child.attrib.get("type") != "Pocket":

if child.text:

board_cards.extend(child.text.split())

elif child.tag == "action":

action_details = {

'player': child.attrib.get('player'),

'action_type': int(child.attrib.get('type')),

'action_sum': safe_float(child.attrib.get('sum')),

'action_round': round_no

}

player = action_details['player']

contribution = action_details['action_sum']

# For round 0 (blinds/antes): update contributions and pot; no snapshot.

if round_no < 1:

if player in player_contributions:

player_contributions[player] += contribution

pot_size += contribution

continue

# For rounds ≥ 1, capture current pot (before adding current action).

current_pot = pot_size

# Actor's current stack (as seen when taking the action).

actor_current_stack = player_stacks.get(player, 0.0) - player_contributions.get(player, 0.0)

# Simplify the action.

simple_action = simplify_action(action_details, round_no, blinds, current_pot)

if simple_action is not None:

snapshot_action_counter += 1

simple_action['action_no'] = snapshot_action_counter

# Replace the "player" field with its numeric value.

numeric_player = player_positions.get(player)

simple_action["player"] = numeric_player

# Build current stacks keyed by numeric positions.

current_player_stacks = { player_positions[name] : (player_stacks[name] - player_contributions.get(name, 0.0))

for name in player_positions }

# Create snapshot only for non-hero actions.

if player != hero:

snapshot = {

"gamecode": gamecode,

"round_no": round_no,

"current_street": street_from_round(round_no),

"blinds": blinds,

"player_positions": player_positions, # mapping from name to number

"player_stacks": current_player_stacks, # mapping from number to current stack

"pot_size": current_pot,

"board_cards": board_cards.copy(),

"previous_actions": [],

"action": simple_action.copy(),

"players_remaining": sum(1 for v in active_players.values() if v),

"is_button": (player_positions.get(player) == 2),

"actor_hole_cards": get_hole_cards(game, player),

"actor_stack_size": actor_current_stack,

"actor_position": player_positions.get(player)

}

# For previous actions, use the numeric "player" already set.

for act in cumulative_actions:

act2 = act.copy()

# Since act["player"] is now numeric, we simply assign it.

act2["player_position"] = act["player"]

snapshot["previous_actions"].append(act2)

logs.append(snapshot)

# Update contributions and pot AFTER snapshot creation.

if player in player_contributions:

player_contributions[player] += contribution

pot_size += contribution

if simple_action is not None:

cumulative_actions.append(simple_action)

# Mark a player as inactive if they folded.

if action_details.get('action_type') == 0:

active_players[player] = False

return logs



# ------------------------------

# Process All XML Files in ipoker_hh Folder

# ------------------------------



def process_all_hand_history(root_folder, hero=None):

all_logs = []

for dirpath, dirnames, filenames in os.walk(root_folder):

for filename in filenames:

if filename.endswith(".xml"):

file_path = os.path.join(dirpath, filename)

try:

tree = ET.parse(file_path)

root_xml = tree.getroot()

if hero is None:

session_general = root_xml.find('general')

if session_general is not None and session_general.find('nickname') is not None:

hero = session_general.find('nickname').text.strip()

logs = parse_decision_logs(root_xml, hero)

all_logs.extend(logs)

print(f"Processed file: {file_path} -> {len(logs)} snapshots.")

except ET.ParseError as e:

print(f"Error parsing XML file: {file_path}", e)

return all_logs



# ------------------------------

# Main

# ------------------------------



if __name__ == '__main__':

root_folder = "ipoker_hh_test" # Adjust folder path as needed.

hero_name = None # Or manually set your hero's name.

print("Processing all hand history XML files in folder:", root_folder)

all_logs = process_all_hand_history(root_folder, hero=hero_name)

print("Total snapshots extracted:", len(all_logs))

with open("logs.json", "w") as outfile:

json.dump(all_logs, outfile, indent=4)

print("Data extraction complete. Saved to logs.json")


avatar-img
3會員
14內容數
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
傑劉的沙龍 的其他內容
記錄了在學習Coursera機器學習課程時的心得,以及如何從寫程式的經驗中找到面對數學的新思維方式,體現了學習態度的轉變。
記錄了在開發撲克機器人對手模型時,如何與語言模型協作的心得,以及在這過程中對開發方法論的一些思考。特別強調了「先求有,再求好」的重要性,以及如何在保持開發效率和深入理解技術細節之間找到平衡。
記錄了在開發撲克機器人時處理數據的心得,從最初對XML格式感到困惑,到現在能夠從容面對數據處理的轉變過程。反思了數據品質在機器學習project中的重要性,以及自己在程式開發能力上的進步
記錄了在分析200萬筆撲克歷史記錄時的思考過程,從最初被 Q-Learning 吸引,到理解其在不完整資訊遊戲中的侷限,最終決定轉向建立對手模型系統的過程。反映了在技術選擇時,如何在炫酷與實用之間找到平衡。
記錄了在開發撲克機器人時,從對機器學習模型的成功驗證,到意識到自己又回到solver策略老路的過程。最終決定改變方向,轉向分析實戰數據的心路歷程。
記錄了放棄使用大型語言模型作為撲克機器人核心的決定過程,以及新的混合策略方案的構思。文章探討了技術選擇的考量因素,並回顧了過去開發經驗帶來的啟發。
記錄了在學習Coursera機器學習課程時的心得,以及如何從寫程式的經驗中找到面對數學的新思維方式,體現了學習態度的轉變。
記錄了在開發撲克機器人對手模型時,如何與語言模型協作的心得,以及在這過程中對開發方法論的一些思考。特別強調了「先求有,再求好」的重要性,以及如何在保持開發效率和深入理解技術細節之間找到平衡。
記錄了在開發撲克機器人時處理數據的心得,從最初對XML格式感到困惑,到現在能夠從容面對數據處理的轉變過程。反思了數據品質在機器學習project中的重要性,以及自己在程式開發能力上的進步
記錄了在分析200萬筆撲克歷史記錄時的思考過程,從最初被 Q-Learning 吸引,到理解其在不完整資訊遊戲中的侷限,最終決定轉向建立對手模型系統的過程。反映了在技術選擇時,如何在炫酷與實用之間找到平衡。
記錄了在開發撲克機器人時,從對機器學習模型的成功驗證,到意識到自己又回到solver策略老路的過程。最終決定改變方向,轉向分析實戰數據的心路歷程。
記錄了放棄使用大型語言模型作為撲克機器人核心的決定過程,以及新的混合策略方案的構思。文章探討了技術選擇的考量因素,並回顧了過去開發經驗帶來的啟發。
你可能也想看
Google News 追蹤
Thumbnail
體驗具有卓越性能的終極多功能性。讓您可以隨時隨地進行工作的先進精簡滑鼠。得益於 8K DPI 任意表面追踪和安靜的點按,現在精確度和響應能力都更上一層樓。
Thumbnail
時間快轉至2030年,或許城市還無懸浮飛車,但AI先會像電力存在每一個角落。百工百業運用智慧算力,就像打開水龍頭一樣容易。這描述並非Cyberpunk科幻場景,正逐漸在世界各地悄然成形。
Thumbnail
企業需關注數位優化和數位轉型以提高競爭力。數位優化強調提升效率和效果,數位轉型則重塑商業模式並超越優化,包含商業模式創新、數據驅動的決策和數位創新文化。該文章涵蓋了這些議題,並期待更多關於 RPA + AI 與數位轉型的最新趨勢與觀點。
Thumbnail
在科技發展的浪潮中,生成式AI無疑是引領未來的關鍵力量。透過深度學習技術,AI系統能夠從大量資料中發掘規律,並創造出全新的內容,無論是文字、圖像、音頻還是視頻,都可以在AI的加持下重新定義。
Thumbnail
【圖解IT大全:掌握數位科技趨勢,透視未來商業模式的148個關鍵】 數位轉型、AI、IoT...是不是讓你聽得霧沙沙? 當你經常覺得… ●最近的科技浪潮令人興奮,想多了解一點 ●被一堆科技名詞搞得心煩意亂,想快速全面了解 ●覺得市面的科技科普書不是太深入就是太淺白 這本書值得一讀!
Thumbnail
  靜靜地坐在桌前,腦海中如潮水般不斷回想著 Open AI 發佈會的點點滴滴。那一場發佈會,宛如一道淩厲的閃電,猛然間照亮了未來那原本被重重迷霧所籠罩的道路,卻也在我的心中肆意地湧起了無盡的不確定與惶恐。   看著那些關於 AI 發展的詳細介紹,我仿佛看到了一個正以驚人速度變化著的世界在
Thumbnail
本文章探討了多智能體系統(MAS)在生成式AI領域中的應用,以及GenAI對於AI_MCU和Software defined hardware的影響。文章還總結了SDH設計模式對數據科學和人工智能時代的影響,並提供了有關GenAI的一些額外信息。
Thumbnail
我們寫作業時,只能一個字一個字地寫,但是量子電腦卻可以同時做很多件事情,就像一位有很多隻手的魔法師一樣呢⋯⋯快來跟♥AI小可愛小艾♥一起探索世界的每一個角落,一起學習有趣又有用的新科普!
Thumbnail
★看似客觀中立的機器運算,可能在學習人類提供的資料後,再複製社會偏見與歧視,形成「自動不平等」!
Thumbnail
體驗具有卓越性能的終極多功能性。讓您可以隨時隨地進行工作的先進精簡滑鼠。得益於 8K DPI 任意表面追踪和安靜的點按,現在精確度和響應能力都更上一層樓。
Thumbnail
時間快轉至2030年,或許城市還無懸浮飛車,但AI先會像電力存在每一個角落。百工百業運用智慧算力,就像打開水龍頭一樣容易。這描述並非Cyberpunk科幻場景,正逐漸在世界各地悄然成形。
Thumbnail
企業需關注數位優化和數位轉型以提高競爭力。數位優化強調提升效率和效果,數位轉型則重塑商業模式並超越優化,包含商業模式創新、數據驅動的決策和數位創新文化。該文章涵蓋了這些議題,並期待更多關於 RPA + AI 與數位轉型的最新趨勢與觀點。
Thumbnail
在科技發展的浪潮中,生成式AI無疑是引領未來的關鍵力量。透過深度學習技術,AI系統能夠從大量資料中發掘規律,並創造出全新的內容,無論是文字、圖像、音頻還是視頻,都可以在AI的加持下重新定義。
Thumbnail
【圖解IT大全:掌握數位科技趨勢,透視未來商業模式的148個關鍵】 數位轉型、AI、IoT...是不是讓你聽得霧沙沙? 當你經常覺得… ●最近的科技浪潮令人興奮,想多了解一點 ●被一堆科技名詞搞得心煩意亂,想快速全面了解 ●覺得市面的科技科普書不是太深入就是太淺白 這本書值得一讀!
Thumbnail
  靜靜地坐在桌前,腦海中如潮水般不斷回想著 Open AI 發佈會的點點滴滴。那一場發佈會,宛如一道淩厲的閃電,猛然間照亮了未來那原本被重重迷霧所籠罩的道路,卻也在我的心中肆意地湧起了無盡的不確定與惶恐。   看著那些關於 AI 發展的詳細介紹,我仿佛看到了一個正以驚人速度變化著的世界在
Thumbnail
本文章探討了多智能體系統(MAS)在生成式AI領域中的應用,以及GenAI對於AI_MCU和Software defined hardware的影響。文章還總結了SDH設計模式對數據科學和人工智能時代的影響,並提供了有關GenAI的一些額外信息。
Thumbnail
我們寫作業時,只能一個字一個字地寫,但是量子電腦卻可以同時做很多件事情,就像一位有很多隻手的魔法師一樣呢⋯⋯快來跟♥AI小可愛小艾♥一起探索世界的每一個角落,一起學習有趣又有用的新科普!
Thumbnail
★看似客觀中立的機器運算,可能在學習人類提供的資料後,再複製社會偏見與歧視,形成「自動不平等」!