diff --git a/examples/landlord-win32/cpp-src/win32.cc b/examples/landlord-win32/cpp-src/win32.cc new file mode 100644 index 00000000..d00bf0bd --- /dev/null +++ b/examples/landlord-win32/cpp-src/win32.cc @@ -0,0 +1,205 @@ +/** + * Landlord Win32 API Layer + * + * C++ is intentionally limited to Win32 windowing and primitive drawing. + * The game rules and AI live in PHP. + */ + +#include +#include +#include +#include +#include + +using namespace php; + +static bool g_quitRequested = false; + +struct PaintFrame { + HDC memDC; + HBITMAP bitmap; + HBITMAP oldBitmap; +}; + +static HDC frame_hdc(Int frameHandle) { + PaintFrame* frame = (PaintFrame*)frameHandle; + return frame ? frame->memDC : NULL; +} + +LRESULT CALLBACK LandlordWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { + switch (msg) { + case WM_CLOSE: + g_quitRequested = true; + PostQuitMessage(0); + return 0; + case WM_DESTROY: + g_quitRequested = true; + PostQuitMessage(0); + return 0; + } + return DefWindowProc(hWnd, msg, wParam, lParam); +} + +Int php_win_create_window(String title, Int width, Int height) { + SetConsoleOutputCP(65001); + + WNDCLASSW wc; + ZeroMemory(&wc, sizeof(wc)); + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = LandlordWndProc; + wc.hInstance = GetModuleHandle(NULL); + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wc.lpszClassName = L"LandlordWindow"; + RegisterClassW(&wc); + + int wtitle_len = MultiByteToWideChar(CP_UTF8, 0, title.data(), -1, NULL, 0); + wchar_t* wtitle = new wchar_t[wtitle_len]; + MultiByteToWideChar(CP_UTF8, 0, title.data(), -1, wtitle, wtitle_len); + + HWND hWnd = CreateWindowExW( + 0, L"LandlordWindow", wtitle, + WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX, + CW_USEDEFAULT, CW_USEDEFAULT, + (int)width, (int)height, + NULL, NULL, GetModuleHandle(NULL), NULL + ); + delete[] wtitle; + return (Int)hWnd; +} + +void php_win_show_window(Int hWnd, Int cmdShow) { + ShowWindow((HWND)hWnd, (int)cmdShow); +} + +Bool php_win_quit_requested() { + return g_quitRequested; +} + +void php_win_post_quit(Int exitCode) { + PostQuitMessage((int)exitCode); +} + +Array php_win_peek_message() { + MSG msg; + ZeroMemory(&msg, sizeof(msg)); + if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + Array result; + result.append((Int)msg.hwnd); + result.append((Int)msg.message); + result.append((Int)msg.wParam); + result.append((Int)msg.lParam); + TranslateMessage(&msg); + DispatchMessage(&msg); + return result; + } + return Array(); +} + +Int php_win_get_tick_count() { + return (Int)GetTickCount(); +} + +Int php_win_message_box(Int hWnd, String text, String caption, Int uType) { + int wtext_len = MultiByteToWideChar(CP_UTF8, 0, text.data(), -1, NULL, 0); + wchar_t* wtext = new wchar_t[wtext_len]; + MultiByteToWideChar(CP_UTF8, 0, text.data(), -1, wtext, wtext_len); + + int wcaption_len = MultiByteToWideChar(CP_UTF8, 0, caption.data(), -1, NULL, 0); + wchar_t* wcaption = new wchar_t[wcaption_len]; + MultiByteToWideChar(CP_UTF8, 0, caption.data(), -1, wcaption, wcaption_len); + + int result = MessageBoxW((HWND)hWnd, wtext, wcaption, (UINT)uType); + + delete[] wtext; + delete[] wcaption; + return result; +} + +Int php_win_begin_paint(Int hWnd) { + HDC hdc = GetDC((HWND)hWnd); + RECT rc; + GetClientRect((HWND)hWnd, &rc); + + HDC memDC = CreateCompatibleDC(hdc); + HBITMAP memBitmap = CreateCompatibleBitmap(hdc, rc.right, rc.bottom); + HBITMAP oldBitmap = (HBITMAP)SelectObject(memDC, memBitmap); + + ReleaseDC((HWND)hWnd, hdc); + + PaintFrame* frame = new PaintFrame; + frame->memDC = memDC; + frame->bitmap = memBitmap; + frame->oldBitmap = oldBitmap; + return (Int)frame; +} + +void php_win_end_paint(Int hWnd, Int hdcHandle) { + PaintFrame* frame = (PaintFrame*)hdcHandle; + if (frame == NULL || frame->memDC == NULL) { + return; + } + HDC memDC = frame->memDC; + RECT rc; + GetClientRect((HWND)hWnd, &rc); + + HDC hdc = GetDC((HWND)hWnd); + BitBlt(hdc, 0, 0, rc.right, rc.bottom, memDC, 0, 0, SRCCOPY); + ReleaseDC((HWND)hWnd, hdc); + + SelectObject(memDC, frame->oldBitmap); + DeleteObject(frame->bitmap); + DeleteDC(memDC); + delete frame; +} + +void php_win_fill_rect(Int hdc, Int x, Int y, Int w, Int h, Int rgbColor) { + HDC targetDC = frame_hdc(hdc); + HBRUSH brush = CreateSolidBrush((COLORREF)rgbColor); + RECT r = {(int)x, (int)y, (int)(x + w), (int)(y + h)}; + FillRect(targetDC, &r, brush); + DeleteObject(brush); +} + +void php_win_fill_ellipse(Int hdc, Int x, Int y, Int w, Int h, Int rgbColor) { + HDC targetDC = frame_hdc(hdc); + HBRUSH brush = CreateSolidBrush((COLORREF)rgbColor); + HBRUSH oldBrush = (HBRUSH)SelectObject(targetDC, brush); + HPEN pen = CreatePen(PS_SOLID, 1, (COLORREF)rgbColor); + HPEN oldPen = (HPEN)SelectObject(targetDC, pen); + Ellipse(targetDC, (int)x, (int)y, (int)(x + w), (int)(y + h)); + SelectObject(targetDC, oldPen); + SelectObject(targetDC, oldBrush); + DeleteObject(pen); + DeleteObject(brush); +} + +void php_win_draw_line(Int hdc, Int x1, Int y1, Int x2, Int y2, Int rgbColor) { + HDC targetDC = frame_hdc(hdc); + HPEN pen = CreatePen(PS_SOLID, 1, (COLORREF)rgbColor); + HPEN oldPen = (HPEN)SelectObject(targetDC, pen); + MoveToEx(targetDC, (int)x1, (int)y1, NULL); + LineTo(targetDC, (int)x2, (int)y2); + SelectObject(targetDC, oldPen); + DeleteObject(pen); +} + +void php_win_draw_text(Int hdc, Int x, Int y, String text, Int fontSize, Int rgbColor, Int bold) { + HDC targetDC = frame_hdc(hdc); + SetTextColor(targetDC, (COLORREF)rgbColor); + SetBkMode(targetDC, TRANSPARENT); + + int wtext_len = MultiByteToWideChar(CP_UTF8, 0, text.data(), -1, NULL, 0); + wchar_t* wtext = new wchar_t[wtext_len]; + MultiByteToWideChar(CP_UTF8, 0, text.data(), -1, wtext, wtext_len); + + HFONT hFont = CreateFontW((int)fontSize, 0, 0, 0, + bold ? FW_BOLD : FW_NORMAL, FALSE, FALSE, FALSE, + DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, + DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, L"Microsoft YaHei UI"); + HFONT oldFont = (HFONT)SelectObject(targetDC, hFont); + TextOutW(targetDC, (int)x, (int)y, wtext, (int)wcslen(wtext)); + SelectObject(targetDC, oldFont); + DeleteObject(hFont); + delete[] wtext; +} diff --git a/examples/landlord-win32/main.php b/examples/landlord-win32/main.php new file mode 100644 index 00000000..cbe19172 --- /dev/null +++ b/examples/landlord-win32/main.php @@ -0,0 +1,1540 @@ + '左侧AI', 2 => '右侧AI']; + + public function __construct() + { + $this->lastPlay = ['valid' => false, 'type' => 'none', 'main' => 0, 'len' => 0]; + $this->newGame(); + } + + public function newGame(): void + { + $this->randomizeAiNames(); + $deck = []; + for ($i = 0; $i < 54; $i++) { + $deck[] = $i; + } + shuffle($deck); + + $this->hands = [[], [], []]; + for ($i = 0; $i < 51; $i++) { + $this->hands[$i % 3][] = $deck[$i]; + } + $this->bottom = [$deck[51], $deck[52], $deck[53]]; + for ($i = 0; $i < 3; $i++) { + sort_cards($this->hands[$i]); + } + sort_cards($this->bottom); + + $this->phase = 'bidding'; + $this->lastCards = []; + $this->playedCards = []; + $this->lastPlay = ['valid' => false, 'type' => 'none', 'main' => 0, 'len' => 0]; + $this->lastPlayer = -1; + $this->currentTurn = -1; + $this->passCount = 0; + $this->winner = -1; + $this->landlord = -1; + $this->bidStart = random_int(0, 2); + $this->bidTurn = $this->bidStart; + $this->bidWinner = -1; + $this->bidLevel = 0; + $this->bidActions = 0; + $this->bidPasses = 0; + $this->multiplier = 1; + $this->hasPlayed = [false, false, false]; + $this->settled = false; + $this->robMode = false; + $this->selected = []; + $this->message = $this->playerName($this->bidTurn) . ' 选择是否叫地主。'; + $this->lastAiTime = win_get_tick_count(); + } + + public function isHumanActionTurn(): bool + { + if ($this->phase == 'bidding') { + return $this->bidTurn == 0; + } + return $this->phase == 'playing' && $this->currentTurn == 0 && $this->winner < 0; + } + + public function primaryActionLabel(): string + { + if ($this->phase == 'bidding') { + return $this->robMode ? '抢地主' : '叫地主'; + } + return '出牌'; + } + + public function secondaryActionLabel(): string + { + return $this->phase == 'bidding' ? '不叫/不抢' : '不出'; + } + + public function humanPrimaryAction(): void + { + if ($this->phase == 'bidding') { + $this->bidAction(0, true); + } else { + $this->humanPlay(); + } + } + + public function humanSecondaryAction(): void + { + if ($this->phase == 'bidding') { + $this->bidAction(0, false); + } else { + $this->humanPass(); + } + } + + public function toggleCardAt(int $index): void + { + if (!$this->isHumanActionTurn() || $this->phase != 'playing') { + return; + } + if (!isset($this->hands[0][$index])) { + return; + } + if (isset($this->selected[$index])) { + unset($this->selected[$index]); + } else { + $this->selected[$index] = true; + } + } + + public function selectedCards(): array + { + $cards = []; + foreach ($this->selected as $idx => $_) { + if (isset($this->hands[0][$idx])) { + $cards[] = $this->hands[0][$idx]; + } + } + sort_cards($cards); + return $cards; + } + + public function humanPlay(): void + { + if ($this->phase != 'playing' || $this->currentTurn != 0 || $this->winner >= 0) { + return; + } + $cards = $this->selectedCards(); + $play = analyze_play($cards); + if (!can_beat($play, $this->activeLastPlay())) { + $this->message = '出牌无效,或牌型/点数压不过上一手。'; + return; + } + $this->commitPlay(0, $cards, $play); + } + + public function humanPass(): void + { + if ($this->phase != 'playing' || $this->currentTurn != 0 || $this->winner >= 0) { + return; + } + if (!$this->activeLastPlay()['valid']) { + $this->message = '当前由你领出,必须出一手有效牌。'; + return; + } + $this->selected = []; + $this->passPlay(0); + } + + public function updateAi(): void + { + if ($this->winner >= 0) { + return; + } + if (($this->phase == 'bidding' && $this->bidTurn == 0) || ($this->phase == 'playing' && $this->currentTurn == 0)) { + return; + } + $now = win_get_tick_count(); + if ($now - $this->lastAiTime < 650) { + return; + } + + if ($this->phase == 'bidding') { + $want = $this->shouldBidLandlord($this->bidTurn); + $this->bidAction($this->bidTurn, $want); + } else { + $player = $this->currentTurn; + $choice = $this->findAiPlay($player, $this->hands[$player], $this->activeLastPlay()); + if (count($choice) == 0) { + $this->passPlay($player); + } else { + $this->commitPlay($player, $choice, analyze_play($choice)); + } + } + $this->lastAiTime = $now; + } + + private function shouldBidLandlord(int $player): bool + { + $score = $this->evaluateLandlordHand($this->hands[$player]); + $risk = $this->multiplierRiskPenalty(); + + if (!$this->robMode) { + return $score >= 50 + $risk; + } + + $leaderAdvantage = 0; + if ($this->bidWinner >= 0) { + $leaderAdvantage = $this->evaluateLandlordHand($this->hands[$this->bidWinner]) - 55; + } + $threshold = 61 + $risk + $this->bidLevel * 5 + max(0, intdiv($leaderAdvantage, 6)); + return $score >= $threshold; + } + + private function multiplierRiskPenalty(): int + { + if ($this->multiplier >= 8) { + return 20; + } + if ($this->multiplier >= 4) { + return 12; + } + if ($this->multiplier >= 2) { + return 5; + } + return 0; + } + + private function evaluateLandlordHand(array $hand): int + { + $counts = rank_counts($hand); + $score = 0; + + if (isset($counts[16]) && isset($counts[17])) { + $score += 34; + } elseif (isset($counts[17])) { + $score += 18; + } elseif (isset($counts[16])) { + $score += 12; + } + + foreach ($counts as $rank => $count) { + if ($rank == 15) { + $score += $count * 7; + } elseif ($rank == 14) { + $score += $count * 5; + } elseif ($rank == 13) { + $score += $count * 3; + } elseif ($rank >= 11) { + $score += $count; + } + + if ($count == 4) { + $score += $rank >= 14 ? 26 : 18; + } elseif ($count == 3) { + $score += $rank >= 12 ? 10 : 6; + } elseif ($count == 2 && $rank >= 12) { + $score += 4; + } + } + + $score += $this->biddingShapeScore($counts); + $score -= $this->looseCardPenalty($counts); + return $score; + } + + private function biddingShapeScore(array $counts): int + { + $score = 0; + $singleRanks = []; + $pairRanks = []; + foreach ($counts as $rank => $count) { + if ($rank >= 15) { + continue; + } + if ($count >= 1) { + $singleRanks[] = $rank; + } + if ($count >= 2) { + $pairRanks[] = $rank; + } + } + + $score += $this->longestConsecutive($singleRanks) >= 5 ? 10 : 0; + $score += $this->longestConsecutive($singleRanks) >= 7 ? 8 : 0; + $score += $this->longestConsecutive($pairRanks) >= 3 ? 9 : 0; + return $score; + } + + private function looseCardPenalty(array $counts): int + { + $penalty = 0; + foreach ($counts as $rank => $count) { + if ($count == 1 && $rank <= 10) { + $penalty += 3; + } elseif ($count == 1 && $rank <= 12) { + $penalty += 1; + } + } + return min(24, $penalty); + } + + private function longestConsecutive(array $ranks): int + { + if (count($ranks) == 0) { + return 0; + } + sort($ranks); + $best = 1; + $run = 1; + for ($i = 1; $i < count($ranks); $i++) { + if ($ranks[$i] == $ranks[$i - 1] + 1) { + $run++; + } elseif ($ranks[$i] != $ranks[$i - 1]) { + $run = 1; + } + if ($run > $best) { + $best = $run; + } + } + return $best; + } + + private function bidAction(int $player, bool $accept): void + { + if ($this->phase != 'bidding' || $player != $this->bidTurn) { + return; + } + + if ($accept) { + $isRob = $this->bidWinner >= 0; + $this->bidWinner = $player; + $this->bidLevel++; + $this->bidPasses = 0; + $this->robMode = true; + if ($isRob) { + $this->multiplier *= 2; + } + $verb = $isRob ? '抢地主' : '叫地主'; + $this->message = $this->playerName($player) . $verb . '。'; + } else { + $this->message = $this->playerName($player) . '选择不叫/不抢。'; + if ($this->bidWinner >= 0) { + $this->bidPasses++; + } else { + $this->bidActions++; + } + } + + if ($this->bidLevel >= 3 || ($this->bidWinner >= 0 && $this->bidPasses >= 2)) { + $this->startPlaying($this->bidWinner); + return; + } + if ($this->bidWinner < 0 && $this->bidActions >= 3) { + $this->message = '三家都不叫地主,重新发牌。'; + $this->newGame(); + return; + } + + $this->bidTurn = $this->nextPlayer($this->bidTurn); + if ($this->bidWinner < 0) { + $this->message .= ' 轮到' . $this->playerName($this->bidTurn) . '叫地主。'; + } else { + $this->message .= ' 轮到' . $this->playerName($this->bidTurn) . '抢地主。'; + } + $this->lastAiTime = win_get_tick_count(); + } + + private function startPlaying(int $landlord): void + { + $this->phase = 'playing'; + $this->landlord = $landlord; + foreach ($this->bottom as $card) { + $this->hands[$landlord][] = $card; + } + sort_cards($this->hands[$landlord]); + + $this->currentTurn = $landlord; + $this->lastPlay = ['valid' => false, 'type' => 'none', 'main' => 0, 'len' => 0]; + $this->lastCards = []; + $this->lastPlayer = -1; + $this->passCount = 0; + $this->selected = []; + $this->message = $this->playerName($landlord) . '成为地主并先出牌。'; + $this->lastAiTime = win_get_tick_count(); + } + + private function activeLastPlay(): array + { + if ($this->lastPlayer == $this->currentTurn) { + return ['valid' => false, 'type' => 'none', 'main' => 0, 'len' => 0]; + } + return $this->lastPlay; + } + + private function commitPlay(int $player, array $cards, array $play): void + { + remove_cards($this->hands[$player], $cards); + $this->lastCards = $cards; + foreach ($cards as $card) { + $this->playedCards[] = $card; + } + $this->lastPlay = $play; + $this->lastPlayer = $player; + $this->passCount = 0; + $this->selected = []; + $this->hasPlayed[$player] = true; + if ($play['type'] == 'bomb' || $play['type'] == 'rocket') { + $this->multiplier *= 2; + } + $this->message = $this->playerName($player) . '打出' . play_name($play); + + if (count($this->hands[$player]) == 0) { + $this->winner = $player; + $summary = $this->settleRound($player); + $this->message = $this->playerName($player) . '获胜。' . $summary . ' 点击“新一局”。'; + win_message_box(0, $this->message, '游戏结束', MB_OK); + return; + } + $this->advancePlay(); + } + + private function passPlay(int $player): void + { + $this->passCount++; + $this->message = $this->playerName($player) . '不出。'; + if ($this->passCount >= 2 && $this->lastPlayer >= 0) { + $this->currentTurn = $this->lastPlayer; + $this->lastPlay = ['valid' => false, 'type' => 'none', 'main' => 0, 'len' => 0]; + $this->lastCards = []; + $this->passCount = 0; + $this->message = $this->playerName($this->currentTurn) . '重新领出。'; + return; + } + $this->advancePlay(); + } + + private function advancePlay(): void + { + $this->currentTurn = $this->nextPlayer($this->currentTurn); + if ($this->currentTurn != 0) { + $this->lastAiTime = win_get_tick_count(); + } + } + + private function nextPlayer(int $player): int + { + return ($player + 2) % 3; + } + + public function playerName(int $player): string + { + $name = $player == 0 ? '你' : ($this->aiNames[$player] ?? 'AI'); + return $player == $this->landlord ? $name . '(地主)' : $name; + } + + private function randomizeAiNames(): void + { + $names = ['阿明', '小北', '老周', '阿兰', '小雨', '大海', '阿峰', '小陈', '老赵', '阿杰']; + shuffle($names); + $this->aiNames = [1 => $names[0], 2 => $names[1]]; + } + + private function settleRound(int $winner): string + { + if ($this->settled) { + return ''; + } + $this->settled = true; + + if ($winner == $this->landlord && !$this->hasPlayed[($this->landlord + 1) % 3] && !$this->hasPlayed[($this->landlord + 2) % 3]) { + $this->multiplier *= 2; + } + + $points = $this->multiplier; + if ($winner == $this->landlord) { + for ($i = 0; $i < 3; $i++) { + if ($i == $this->landlord) { + $this->scores[$i] += 2 * $points; + } else { + $this->scores[$i] -= $points; + } + } + return '地主胜,倍率x' . $this->multiplier . ',地主+' . (2 * $points) . ',农民各-' . $points . '。'; + } + + for ($i = 0; $i < 3; $i++) { + if ($i == $this->landlord) { + $this->scores[$i] -= 2 * $points; + } else { + $this->scores[$i] += $points; + } + } + return '农民胜,倍率x' . $this->multiplier . ',地主-' . (2 * $points) . ',农民各+' . $points . '。'; + } + + private function findAiPlay(int $player, array $hand, array $target): array + { + if (!$target['valid']) { + return $this->chooseAiLead($player, $hand); + } + + $coverAlly = $this->lastPlayer >= 0 + && $this->isSameSide($player, $this->lastPlayer) + && $this->shouldOvertakeAlly($player, $target); + + if ($this->lastPlayer >= 0 && $this->isSameSide($player, $this->lastPlayer) && !$coverAlly) { + return []; + } + + $candidates = $this->buildSimpleCandidates($hand, $target); + $next = $this->nextPlayer($player); + $nextEnemyDanger = !$this->isSameSide($player, $next) && count($this->hands[$next]) <= 4; + $lastEnemyDanger = $this->lastPlayer >= 0 && !$this->isSameSide($player, $this->lastPlayer) && count($this->hands[$this->lastPlayer]) <= 2; + $mustFight = $lastEnemyDanger || $nextEnemyDanger || count($hand) <= 5 || $coverAlly; + + $winning = $this->findWinningCandidate($hand, $candidates, $target); + if (count($winning) > 0 && ($mustFight || count($hand) <= 8)) { + return $winning; + } + + $this->sortCandidatesForContext($candidates, $player, $hand, true, $mustFight || $coverAlly); + $best = []; + $bestScore = -100000; + foreach ($candidates as $cards) { + $play = analyze_play($cards); + if (can_beat($play, $target)) { + if ($coverAlly && !$nextEnemyDanger && $this->isPowerPlay($play)) { + continue; + } + if (!$mustFight && $this->isPowerPlay($play)) { + continue; + } + $score = $this->candidateScore($player, $hand, $cards, true, $mustFight || $coverAlly); + if ($score > $bestScore) { + $best = $cards; + $bestScore = $score; + } + } + } + if (count($best) > 0) { + return $best; + } + + if ($coverAlly && !$nextEnemyDanger) { + return []; + } + + if (!$mustFight) { + return []; + } + + foreach ($candidates as $cards) { + $play = analyze_play($cards); + if (can_beat($play, $target)) { + return $cards; + } + } + return []; + } + + private function shouldOvertakeAlly(int $player, array $target): bool + { + $next = $this->nextPlayer($player); + if ($this->isSameSide($player, $next)) { + return false; + } + + $nextCards = count($this->hands[$next]); + if ($nextCards <= 3) { + return true; + } + + if ($target['type'] == 'single') { + return $target['main'] <= 11; + } + if ($target['type'] == 'pair') { + return $target['main'] <= 10; + } + if ($target['type'] == 'triple' || $target['type'] == 'triple_single' || $target['type'] == 'triple_pair') { + return $target['main'] <= 9; + } + if ($target['type'] == 'straight' || $target['type'] == 'pair_sequence') { + return $target['main'] <= 10; + } + return false; + } + + private function chooseAiLead(int $player, array $hand): array + { + $byRank = $this->cardsByRank($hand); + $next = $this->nextPlayer($player); + $nextIsEnemy = !$this->isSameSide($player, $next); + $nextCards = count($this->hands[$next]); + $enemyMinCards = $this->minEnemyCards($player); + + if ($player == $this->landlord && $enemyMinCards <= 2) { + $block = $this->chooseLandlordBlockLead($hand, $byRank, $enemyMinCards); + if (count($block) > 0) { + return $block; + } + } + + if (count($hand) <= 2 && analyze_play($hand)['valid']) { + return $hand; + } + + if (!$nextIsEnemy && $nextCards <= 2) { + $feed = $this->chooseAllyFeedLead($hand, $byRank, $nextCards); + if (count($feed) > 0) { + return $feed; + } + } + + $blockNext = $nextIsEnemy && count($this->hands[$next]) <= 2; + + if ($blockNext) { + $block = $this->chooseBlockingLead($hand, $byRank); + if (count($block) > 0) { + return $block; + } + } + + $leadCandidates = $this->buildLeadCandidates($hand, $byRank); + $winning = $this->findWinningCandidate($hand, $leadCandidates, ['valid' => false, 'type' => 'none', 'main' => 0, 'len' => 0]); + if (count($winning) > 0) { + return $winning; + } + if (count($leadCandidates) > 0) { + $this->sortCandidatesForContext($leadCandidates, $player, $hand, false, $blockNext); + return $leadCandidates[0]; + } + + foreach ($byRank as $rank => $cards) { + if ($rank < 15 && count($cards) >= 3) { + foreach ($hand as $kicker) { + if (card_rank($kicker) != $rank) { + return [$cards[0], $cards[1], $cards[2], $kicker]; + } + } + return [$cards[0], $cards[1], $cards[2]]; + } + } + $straight = $this->firstSequence($byRank, 5, 1); + if (count($straight) > 0) { + return $straight; + } + foreach ($byRank as $rank => $cards) { + if ($rank < 15 && count($cards) >= 2) { + return [$cards[0], $cards[1]]; + } + } + return [$hand[0]]; + } + + private function chooseLandlordBlockLead(array $hand, array $byRank, int $enemyCards): array + { + if ($enemyCards == 1) { + foreach ($byRank as $rank => $cards) { + if ($rank < 15 && count($cards) >= 2) { + return [$cards[0], $cards[1]]; + } + } + + $straight = $this->firstSequence($byRank, 5, 1); + if (count($straight) > 0) { + return $straight; + } + + foreach ($byRank as $rank => $cards) { + if ($rank < 15 && count($cards) >= 3) { + return [$cards[0], $cards[1], $cards[2]]; + } + } + return []; + } + + foreach ($hand as $card) { + if (card_rank($card) < 15) { + return [$card]; + } + } + return [$hand[0]]; + } + + private function chooseAllyFeedLead(array $hand, array $byRank, int $allyCards): array + { + if ($allyCards == 2) { + foreach ($byRank as $rank => $cards) { + if ($rank < 15 && count($cards) >= 2) { + return [$cards[0], $cards[1]]; + } + } + } + + foreach ($hand as $card) { + if (card_rank($card) < 15) { + return [$card]; + } + } + return [$hand[0]]; + } + + private function chooseBlockingLead(array $hand, array $byRank): array + { + foreach ($byRank as $rank => $cards) { + if ($rank >= 11 && $rank < 15 && count($cards) >= 2) { + return [$cards[0], $cards[1]]; + } + } + foreach ($hand as $card) { + $rank = card_rank($card); + if ($rank >= 12 && $rank < 15) { + return [$card]; + } + } + foreach ($byRank as $rank => $cards) { + if ($rank < 15 && count($cards) >= 3) { + return [$cards[0], $cards[1], $cards[2]]; + } + } + return []; + } + + private function sortCandidates(array &$candidates): void + { + usort($candidates, function (array $a, array $b): int { + $pa = analyze_play($a); + $pb = analyze_play($b); + $ca = $this->playCost($pa, count($a)); + $cb = $this->playCost($pb, count($b)); + if ($ca == $cb) { + return $pa['main'] <=> $pb['main']; + } + return $ca <=> $cb; + }); + } + + private function sortCandidatesForContext(array &$candidates, int $player, array $hand, bool $following, bool $urgent): void + { + usort($candidates, function (array $a, array $b) use ($player, $hand, $following, $urgent): int { + $sa = $this->candidateScore($player, $hand, $a, $following, $urgent); + $sb = $this->candidateScore($player, $hand, $b, $following, $urgent); + if ($sa == $sb) { + $pa = analyze_play($a); + $pb = analyze_play($b); + return $pa['main'] <=> $pb['main']; + } + return $sb <=> $sa; + }); + } + + private function candidateScore(int $player, array $hand, array $cards, bool $following, bool $urgent): int + { + $play = analyze_play($cards); + $remaining = count($hand) - count($cards); + $remainingHand = $this->remainingAfter($hand, $cards); + $score = count($cards) * 22 - $play['main'] * 2; + $score += ($this->estimateTurns($hand) - $this->estimateTurns($remainingHand)) * 45; + $score += $this->estimateWinChance($player, $remainingHand) * 3; + + if ($remaining == 0) { + $score += 10000; + } elseif ($remaining <= 2) { + $score += 180; + } + + if ($player == $this->landlord && $remaining > 0) { + $enemyMin = $this->minEnemyCards($player); + if ($enemyMin == 1 && $play['type'] == 'single') { + $score -= 1200; + } elseif ($enemyMin == 2 && $play['type'] == 'pair') { + $score -= 1200; + } + } + + if (!$following && $this->hasSingleControl($remainingHand)) { + if ($play['type'] == 'single' && $play['main'] <= 10) { + $score += 150; + } elseif ($play['type'] == 'pair' && $play['main'] <= 10) { + $score += 90; + } + } + + if ($play['type'] == 'straight' || $play['type'] == 'pair_sequence') { + $score += 40; + } elseif ($play['type'] == 'airplane') { + $score += 70; + } elseif ($play['type'] == 'triple_single' || $play['type'] == 'triple_pair') { + $score += 45; + } elseif ($play['type'] == 'four_two_single' || $play['type'] == 'four_two_pair') { + $score += 55; + } + + $next = $this->nextPlayer($player); + if (!$this->isSameSide($player, $next)) { + $nextCards = count($this->hands[$next]); + if ($nextCards == 1 && $play['type'] != 'single') { + $score += 180; + } elseif ($nextCards == 2 && $play['type'] == 'single') { + $score += 140; + } + } elseif (!$following && count($this->hands[$next]) <= 2) { + if (count($this->hands[$next]) == 1 && $play['type'] == 'single') { + $score += 180; + } elseif (count($this->hands[$next]) == 2 && $play['type'] == 'pair') { + $score += 180; + } + } + + if ($play['type'] == 'bomb' || $play['type'] == 'rocket') { + if ($remaining <= 2 || $urgent || $this->estimateWinChance($player, $remainingHand) >= 75) { + $score += 260 + $this->multiplier * 10; + } else { + $score -= 520; + } + } elseif ($play['main'] >= 15 && !$urgent && $remaining > 4) { + $score -= $this->hasSingleControl($remainingHand) ? 70 : 160; + } + + return $score; + } + + private function remainingAfter(array $hand, array $cards): array + { + $remaining = $hand; + remove_cards($remaining, $cards); + return $remaining; + } + + private function estimateWinChance(int $player, array $remainingHand): int + { + if (count($remainingHand) == 0) { + return 100; + } + + $turns = $this->estimateTurns($remainingHand); + $chance = 45; + $chance += max(0, 6 - $turns) * 8; + $chance += $this->hasSingleControl($remainingHand) ? 20 : 0; + $chance += $this->hasBombInHand($remainingHand) ? 12 : 0; + + $enemyMin = 30; + $allyMin = 30; + for ($i = 0; $i < 3; $i++) { + if ($i == $player) { + continue; + } + if ($this->isSameSide($player, $i)) { + $allyMin = min($allyMin, count($this->hands[$i])); + } else { + $enemyMin = min($enemyMin, count($this->hands[$i])); + } + } + + if ($enemyMin <= 1) { + $chance -= 30; + } elseif ($enemyMin <= 3) { + $chance -= 16; + } + if ($allyMin <= 2 && $player != $this->landlord) { + $chance += 12; + } + if ($player == $this->landlord && count($remainingHand) <= 4) { + $chance += 8; + } + + return max(5, min(95, $chance)); + } + + private function hasBombInHand(array $hand): bool + { + $counts = rank_counts($hand); + foreach ($counts as $count) { + if ($count == 4) { + return true; + } + } + return isset($counts[16]) && isset($counts[17]); + } + + private function hasSingleControl(array $hand): bool + { + if (in_array(53, $hand, true)) { + return true; + } + if (in_array(52, $hand, true) && $this->isCardPlayed(53)) { + return true; + } + + for ($rank = 15; $rank >= 3; $rank--) { + if ($this->rankCount($hand, $rank) == 0) { + continue; + } + return $this->allHigherRanksAccounted($rank, $hand); + } + return false; + } + + private function allHigherRanksAccounted(int $rank, array $hand): bool + { + for ($r = 17; $r > $rank; $r--) { + if ($this->rankCount($this->playedCards, $r) + $this->rankCount($hand, $r) < $this->totalRankCount($r)) { + return false; + } + } + return true; + } + + private function isCardPlayed(int $card): bool + { + return in_array($card, $this->playedCards, true); + } + + private function rankCount(array $cards, int $rank): int + { + $count = 0; + foreach ($cards as $card) { + if (card_rank($card) == $rank) { + $count++; + } + } + return $count; + } + + private function totalRankCount(int $rank): int + { + return $rank >= 16 ? 1 : 4; + } + + private function estimateTurns(array $cards): int + { + if (count($cards) == 0) { + return 0; + } + if (analyze_play($cards)['valid']) { + return 1; + } + $counts = rank_counts($cards); + $turns = 0; + foreach ($counts as $count) { + if ($count >= 3) { + $turns++; + } elseif ($count == 2) { + $turns++; + } else { + $turns++; + } + } + return $turns; + } + + private function playCost(array $play, int $cardCount): int + { + $cost = $play['main'] * 10 + $cardCount; + if ($play['type'] == 'bomb') { + $cost += 600; + } elseif ($play['type'] == 'rocket') { + $cost += 900; + } elseif ($play['main'] >= 15) { + $cost += 160; + } + return $cost; + } + + private function isPowerPlay(array $play): bool + { + return $play['type'] == 'bomb' || $play['type'] == 'rocket' || $play['main'] >= 15; + } + + private function isSameSide(int $a, int $b): bool + { + if ($a == $this->landlord || $b == $this->landlord) { + return $a == $b; + } + return true; + } + + private function minEnemyCards(int $player): int + { + $min = 99; + for ($i = 0; $i < 3; $i++) { + if ($i != $player && !$this->isSameSide($player, $i)) { + $min = min($min, count($this->hands[$i])); + } + } + return $min == 99 ? 99 : $min; + } + + private function findWinningCandidate(array $hand, array $candidates, array $target): array + { + foreach ($candidates as $cards) { + if (count($cards) == count($hand)) { + $play = analyze_play($cards); + if (can_beat($play, $target)) { + return $cards; + } + } + } + return []; + } + + private function buildLeadCandidates(array $hand, array $byRank): array + { + $out = []; + foreach ($hand as $card) { + $out[] = [$card]; + } + foreach ($byRank as $rank => $cards) { + if (count($cards) >= 2) { + $out[] = [$cards[0], $cards[1]]; + } + if (count($cards) >= 3) { + $out[] = [$cards[0], $cards[1], $cards[2]]; + foreach ($hand as $kicker) { + if (card_rank($kicker) != $rank) { + $out[] = [$cards[0], $cards[1], $cards[2], $kicker]; + break; + } + } + foreach ($byRank as $pairRank => $pairCards) { + if ($pairRank != $rank && count($pairCards) >= 2) { + $out[] = [$cards[0], $cards[1], $cards[2], $pairCards[0], $pairCards[1]]; + break; + } + } + } + } + + for ($len = 5; $len <= 12; $len++) { + $out = array_merge($out, $this->sequenceCandidates($byRank, $len, 1)); + } + for ($len = 3; $len <= 8; $len++) { + $out = array_merge($out, $this->sequenceCandidates($byRank, $len, 2)); + } + for ($len = 2; $len <= 5; $len++) { + $out = array_merge($out, $this->sequenceCandidates($byRank, $len, 3)); + } + $out = array_merge($out, $this->fourTwoCandidates($hand, $byRank, false)); + $out = array_merge($out, $this->fourTwoCandidates($hand, $byRank, true)); + + foreach ($byRank as $cards) { + if (count($cards) == 4) { + $out[] = [$cards[0], $cards[1], $cards[2], $cards[3]]; + } + } + if (isset($byRank[16]) && isset($byRank[17])) { + $out[] = [$byRank[16][0], $byRank[17][0]]; + } + return $out; + } + + private function buildSimpleCandidates(array $hand, array $target): array + { + $byRank = $this->cardsByRank($hand); + + $out = []; + $type = $target['type']; + if ($type == 'single') { + foreach ($hand as $card) { + $out[] = [$card]; + } + } elseif ($type == 'pair') { + foreach ($byRank as $cards) { + if (count($cards) >= 2) { + $out[] = [$cards[0], $cards[1]]; + } + } + } elseif ($type == 'triple' || $type == 'triple_single' || $type == 'triple_pair') { + foreach ($byRank as $rank => $cards) { + if (count($cards) < 3) { + continue; + } + if ($type == 'triple') { + $out[] = [$cards[0], $cards[1], $cards[2]]; + } elseif ($type == 'triple_single') { + foreach ($hand as $kicker) { + if (card_rank($kicker) != $rank) { + $out[] = [$cards[0], $cards[1], $cards[2], $kicker]; + break; + } + } + } else { + foreach ($byRank as $pairRank => $pairCards) { + if ($pairRank != $rank && count($pairCards) >= 2) { + $out[] = [$cards[0], $cards[1], $cards[2], $pairCards[0], $pairCards[1]]; + break; + } + } + } + } + } elseif ($type == 'straight') { + $out = array_merge($out, $this->sequenceCandidates($byRank, $target['len'], 1)); + } elseif ($type == 'pair_sequence') { + $out = array_merge($out, $this->sequenceCandidates($byRank, intdiv($target['len'], 2), 2)); + } elseif ($type == 'airplane') { + $out = array_merge($out, $this->sequenceCandidates($byRank, intdiv($target['len'], 3), 3)); + } elseif ($type == 'four_two_single' || $type == 'four_two_pair') { + $out = array_merge($out, $this->fourTwoCandidates($hand, $byRank, $type == 'four_two_pair')); + } + + foreach ($byRank as $cards) { + if (count($cards) == 4) { + $out[] = [$cards[0], $cards[1], $cards[2], $cards[3]]; + } + } + if (isset($byRank[16]) && isset($byRank[17])) { + $out[] = [$byRank[16][0], $byRank[17][0]]; + } + return $out; + } + + private function fourTwoCandidates(array $hand, array $byRank, bool $withPairs): array + { + $out = []; + foreach ($byRank as $rank => $cards) { + if (count($cards) < 4) { + continue; + } + $candidate = [$cards[0], $cards[1], $cards[2], $cards[3]]; + if ($withPairs) { + $pairs = []; + foreach ($byRank as $pairRank => $pairCards) { + if ($pairRank != $rank && count($pairCards) >= 2) { + $pairs[] = [$pairCards[0], $pairCards[1]]; + } + } + if (count($pairs) >= 2) { + $out[] = array_merge($candidate, $pairs[0], $pairs[1]); + } + } else { + $kickers = []; + foreach ($hand as $card) { + if (card_rank($card) != $rank) { + $kickers[] = $card; + } + } + if (count($kickers) >= 2) { + $out[] = array_merge($candidate, [$kickers[0], $kickers[1]]); + } + } + } + return $out; + } + + private function cardsByRank(array $hand): array + { + $byRank = []; + foreach ($hand as $card) { + $rank = card_rank($card); + if (!isset($byRank[$rank])) { + $byRank[$rank] = []; + } + $byRank[$rank][] = $card; + } + ksort($byRank); + return $byRank; + } + + private function firstSequence(array $byRank, int $needRanks, int $cardsPerRank): array + { + $candidates = $this->sequenceCandidates($byRank, $needRanks, $cardsPerRank); + return count($candidates) > 0 ? $candidates[0] : []; + } + + private function sequenceCandidates(array $byRank, int $needRanks, int $cardsPerRank): array + { + $ranks = []; + foreach ($byRank as $rank => $cards) { + if ($rank < 15 && count($cards) >= $cardsPerRank) { + $ranks[] = $rank; + } + } + + $out = []; + for ($i = 0; $i <= count($ranks) - $needRanks; $i++) { + $slice = array_slice($ranks, $i, $needRanks); + if (!is_consecutive($slice)) { + continue; + } + $cards = []; + foreach ($slice as $rank) { + for ($j = 0; $j < $cardsPerRank; $j++) { + $cards[] = $byRank[$rank][$j]; + } + } + $out[] = $cards; + } + return $out; + } +} + +class LandlordRenderer +{ + private int $hWnd; + + public function __construct(int $hWnd) + { + $this->hWnd = $hWnd; + } + + public function render(LandlordGame $game): void + { + $hdc = win_begin_paint($this->hWnd); + $this->drawTable($hdc); + $this->drawTopBar($hdc, $game); + $this->drawBottom($hdc, $game); + $this->drawPlayer($hdc, $game, 1, 72, 205, false); + $this->drawPlayer($hdc, $game, 2, WINDOW_WIDTH - 144, 205, true); + $this->drawCenterStatus($hdc, $game); + $this->drawLastPlay($hdc, $game); + $this->drawButtons($hdc, $game); + $this->drawHand($hdc, $game); + $this->drawBottomScoreBar($hdc, $game); + win_end_paint($this->hWnd, $hdc); + } + + private function drawTable(int $hdc): void + { + for ($y = 0; $y < WINDOW_HEIGHT; $y += 8) { + $t = intdiv($y * 90, WINDOW_HEIGHT); + win_fill_rect($hdc, 0, $y, WINDOW_WIDTH, 8, rgb(20 + intdiv($t, 3), 65 + intdiv($t, 2), 125 + $t)); + } + win_fill_ellipse($hdc, 230, 95, 720, 430, rgb(45, 115, 186)); + win_fill_ellipse($hdc, 315, 135, 550, 320, rgb(56, 132, 205)); + win_fill_rect($hdc, 0, 662, WINDOW_WIDTH, 98, rgb(40, 55, 86)); + win_fill_rect($hdc, 0, 690, WINDOW_WIDTH, 70, rgb(28, 38, 62)); + win_draw_text($hdc, 505, 176, '斗地主', 54, rgb(37, 93, 158), 1); + win_draw_text($hdc, 560, 238, '单机', 19, rgb(35, 86, 145), 1); + } + + private function drawTopBar(int $hdc, LandlordGame $game): void + { + win_fill_rect($hdc, 0, 0, WINDOW_WIDTH, 64, rgb(20, 46, 92)); + win_draw_text($hdc, 28, 18, '退出', 18, rgb(242, 245, 250), 1); + win_draw_text($hdc, 94, 18, '记牌器', 18, rgb(242, 245, 250), 1); + win_draw_text($hdc, 840, 16, '托管', 18, rgb(242, 245, 250), 1); + win_draw_text($hdc, 910, 16, '菜单', 18, rgb(242, 245, 250), 1); + win_draw_text($hdc, 995, 16, '分数 ' . $game->scores[0] . ' / ' . $game->scores[1] . ' / ' . $game->scores[2], 17, rgb(245, 224, 130), 1); + win_draw_text($hdc, 1088, 38, '倍 x' . $game->multiplier, 17, rgb(255, 238, 118), 1); + } + + private function drawCenterStatus(int $hdc, LandlordGame $game): void + { + if ($game->phase == 'bidding') { + $title = '叫地主阶段'; + $sub = $game->bidLevel == 0 ? '等待玩家叫地主' : '地主候选:' . $game->playerName($game->bidWinner) . ',抢地主次数:' . max(0, $game->bidLevel - 1); + $turn = '轮到:' . $game->playerName($game->bidTurn); + } else { + $title = '出牌阶段'; + $sub = $game->lastPlay['valid'] ? '上一手:' . $game->playerName($game->lastPlayer) . ' - ' . play_name($game->lastPlay) : '无人压牌,可任意领出'; + $turn = '当前:' . $game->playerName($game->currentTurn); + } + + win_draw_text($hdc, 492, 118, $title, 25, rgb(248, 240, 190), 1); + win_draw_text($hdc, 448, 154, $turn, 20, rgb(255, 236, 122), 1); + win_draw_text($hdc, 410, 184, $sub, 17, rgb(218, 234, 244), 0); + win_draw_text($hdc, 348, 470, $game->message, 18, rgb(248, 248, 235), 1); + } + + private function drawPlayer(int $hdc, LandlordGame $game, int $player, int $x, int $y, bool $right): void + { + $active = ($game->phase == 'bidding' && $game->bidTurn == $player) || ($game->phase == 'playing' && $game->currentTurn == $player); + $avatarColor = $player == 1 ? rgb(219, 82, 55) : rgb(236, 135, 46); + $ring = $active ? rgb(255, 226, 83) : rgb(215, 232, 245); + win_fill_ellipse($hdc, $x - 8, $y - 8, 76, 76, $ring); + win_fill_ellipse($hdc, $x, $y, 60, 60, $avatarColor); + win_draw_text($hdc, $x + 16, $y + 15, 'AI', 22, rgb(255, 245, 225), 1); + + $nameX = $right ? $x - 54 : $x - 8; + win_fill_rect($hdc, $nameX, $y + 72, 116, 28, rgb(35, 92, 145)); + win_draw_text($hdc, $nameX + 15, $y + 76, $game->playerName($player), 16, rgb(240, 248, 255), 1); + win_fill_rect($hdc, $nameX + 18, $y + 106, 84, 28, rgb(35, 54, 94)); + win_draw_text($hdc, $nameX + 30, $y + 110, '分 ' . $game->scores[$player], 16, rgb(255, 225, 86), 1); + + $cardX = $right ? $x - 110 : $x + 76; + $shown = min(8, count($game->hands[$player])); + for ($i = 0; $i < $shown; $i++) { + $this->drawMiniCardBack($hdc, $cardX + ($right ? -$i * 14 : $i * 14), $y + 85); + } + win_fill_rect($hdc, $cardX + ($right ? -32 : 58), $y + 114, 34, 32, rgb(52, 92, 152)); + win_draw_text($hdc, $cardX + ($right ? -24 : 66), $y + 118, (string)count($game->hands[$player]), 19, rgb(245, 245, 245), 1); + } + + private function drawBottom(int $hdc, LandlordGame $game): void + { + $x = 522; + for ($i = 0; $i < count($game->bottom); $i++) { + if ($game->phase == 'bidding') { + $this->drawSmallCardBack($hdc, $x + $i * 42, 30); + } else { + $this->drawSmallCard($hdc, $x + $i * 42, 30, $game->bottom[$i]); + } + } + } + + private function drawLastPlay(int $hdc, LandlordGame $game): void + { + if ($game->phase != 'playing' || count($game->lastCards) == 0) { + return; + } + $gap = count($game->lastCards) > 8 ? 34 : 46; + $total = CARD_W + max(0, count($game->lastCards) - 1) * $gap; + $x = intdiv(WINDOW_WIDTH - $total, 2); + for ($i = 0; $i < count($game->lastCards); $i++) { + $this->drawCard($hdc, $x + $i * $gap, 300, $game->lastCards[$i], false, false); + } + } + + private function drawButtons(int $hdc, LandlordGame $game): void + { + $enabled = $game->isHumanActionTurn(); + $this->drawButton($hdc, BTN_PLAY_X, BTN_Y, $game->primaryActionLabel(), $enabled, true); + $this->drawButton($hdc, BTN_PASS_X, BTN_Y, $game->secondaryActionLabel(), $enabled, false); + $this->drawButton($hdc, BTN_NEW_X, BTN_Y, '新一局', true, false); + } + + private function drawButton(int $hdc, int $x, int $y, string $label, bool $enabled, bool $primary): void + { + $fill = !$enabled ? rgb(87, 113, 145) : ($primary ? rgb(244, 190, 58) : rgb(245, 246, 238)); + $text = !$enabled ? rgb(185, 199, 212) : rgb(35, 45, 54); + win_fill_rect($hdc, $x + 3, $y + 4, BTN_W, BTN_H, rgb(25, 56, 92)); + win_fill_rect($hdc, $x, $y, BTN_W, BTN_H, $fill); + $this->drawRect($hdc, $x, $y, BTN_W, BTN_H, rgb(240, 224, 150)); + win_draw_text($hdc, $x + 28, $y + 11, $label, 18, $text, 1); + } + + private function drawHand(int $hdc, LandlordGame $game): void + { + $hand = $game->hands[0]; + $count = count($hand); + $gap = $this->handGap($count); + $startX = $this->handStartX($count, $gap); + + for ($i = 0; $i < $count; $i++) { + $selected = isset($game->selected[$i]); + $focused = $game->phase == 'playing' && $game->currentTurn == 0 && $selected; + $y = $selected ? 574 : 604; + $this->drawCard($hdc, $startX + $i * $gap, $y, $hand[$i], $selected, $focused); + } + } + + private function drawBottomScoreBar(int $hdc, LandlordGame $game): void + { + win_fill_rect($hdc, 0, 704, WINDOW_WIDTH, 56, rgb(32, 43, 70)); + win_fill_ellipse($hdc, 58, 684, 64, 64, rgb(225, 238, 246)); + win_fill_ellipse($hdc, 64, 690, 52, 52, rgb(79, 128, 160)); + win_fill_rect($hdc, 135, 712, 190, 34, rgb(64, 60, 92)); + win_draw_text($hdc, 158, 718, '玩家:你' . ($game->landlord == 0 ? '(地主)' : ''), 18, rgb(245, 245, 245), 1); + win_fill_rect($hdc, 360, 712, 180, 34, rgb(65, 72, 112)); + win_draw_text($hdc, 386, 718, '当前分数:' . $game->scores[0], 19, rgb(255, 226, 82), 1); + win_fill_rect($hdc, 575, 712, 145, 34, rgb(38, 86, 104)); + win_draw_text($hdc, 600, 718, '手牌:' . count($game->hands[0]) . '张', 18, rgb(240, 246, 248), 1); + win_fill_rect($hdc, 930, 712, 170, 34, rgb(45, 75, 118)); + win_draw_text($hdc, 958, 718, '当前倍率:x' . $game->multiplier, 19, rgb(255, 226, 82), 1); + } + + public function hitTestHand(LandlordGame $game, int $x, int $y): int + { + $count = count($game->hands[0]); + if ($count == 0) { + return -1; + } + $gap = $this->handGap($count); + $startX = $this->handStartX($count, $gap); + for ($i = $count - 1; $i >= 0; $i--) { + $selected = isset($game->selected[$i]); + $cy = $selected ? 574 : 604; + if (in_rect($x, $y, $startX + $i * $gap, $cy, CARD_W, CARD_H)) { + return $i; + } + } + return -1; + } + + private function handGap(int $count): int + { + return $count > 18 ? 31 : CARD_GAP; + } + + private function handStartX(int $count, int $gap): int + { + $total = CARD_W + max(0, $count - 1) * $gap; + return max(26, intdiv(WINDOW_WIDTH - $total, 2)); + } + + private function drawMiniCardBack(int $hdc, int $x, int $y): void + { + win_fill_rect($hdc, $x, $y, 26, 38, rgb(46, 86, 164)); + $this->drawRect($hdc, $x, $y, 26, 38, rgb(225, 235, 245)); + } + + private function drawSmallCardBack(int $hdc, int $x, int $y): void + { + win_fill_rect($hdc, $x, $y, 34, 48, rgb(46, 86, 164)); + $this->drawRect($hdc, $x, $y, 34, 48, rgb(225, 235, 245)); + win_fill_rect($hdc, $x + 6, $y + 6, 22, 36, rgb(65, 110, 190)); + } + + private function drawSmallCard(int $hdc, int $x, int $y, int $card): void + { + win_fill_rect($hdc, $x, $y, 34, 48, rgb(252, 252, 244)); + $this->drawRect($hdc, $x, $y, 34, 48, rgb(45, 56, 66)); + $rank = card_rank($card); + $red = ($rank == 17) || ($rank < 16 && (card_suit($card) == 1 || card_suit($card) == 3)); + $color = $red ? rgb(190, 25, 38) : rgb(25, 32, 34); + if ($rank >= 16) { + win_draw_text($hdc, $x + 4, $y + 12, 'J', 22, $color, 1); + } else { + win_draw_text($hdc, $x + 4, $y + 1, rank_label($rank), 22, $color, 1); + win_draw_text($hdc, $x + 6, $y + 23, card_suit_label($card), 16, $color, 1); + } + } + + private function drawCard(int $hdc, int $x, int $y, int $card, bool $selected, bool $focused): void + { + $rank = card_rank($card); + $red = ($rank == 17) || ($rank < 16 && (card_suit($card) == 1 || card_suit($card) == 3)); + $fill = $selected ? rgb(255, 247, 196) : rgb(252, 252, 244); + $border = $focused ? rgb(255, 210, 70) : rgb(55, 70, 62); + win_fill_rect($hdc, $x + 3, $y + 4, CARD_W, CARD_H, rgb(45, 54, 70)); + win_fill_rect($hdc, $x, $y, CARD_W, CARD_H, $fill); + $this->drawRect($hdc, $x, $y, CARD_W, CARD_H, $border); + if ($focused) { + $this->drawRect($hdc, $x - 2, $y - 2, CARD_W + 4, CARD_H + 4, rgb(255, 210, 70)); + } + $textColor = $red ? rgb(190, 25, 38) : rgb(20, 26, 32); + if ($rank >= 16) { + win_draw_text($hdc, $x + 7, $y + 8, 'J', 30, $textColor, 1); + win_draw_text($hdc, $x + 7, $y + 35, 'O', 30, $textColor, 1); + win_draw_text($hdc, $x + 7, $y + 62, 'K', 30, $textColor, 1); + win_draw_text($hdc, $x + 36, $y + 25, 'ER', 19, $textColor, 1); + return; + } + + win_draw_text($hdc, $x + 7, $y + 6, rank_label($rank), 36, $textColor, 1); + win_draw_text($hdc, $x + 9, $y + 43, card_suit_label($card), 24, $textColor, 1); + } + + private function drawRect(int $hdc, int $x, int $y, int $w, int $h, int $rgb): void + { + win_draw_line($hdc, $x, $y, $x + $w, $y, $rgb); + win_draw_line($hdc, $x + $w, $y, $x + $w, $y + $h, $rgb); + win_draw_line($hdc, $x + $w, $y + $h, $x, $y + $h, $rgb); + win_draw_line($hdc, $x, $y + $h, $x, $y, $rgb); + } +} + +class LandlordApp +{ + private int $hWnd = 0; + private LandlordGame $game; + private LandlordRenderer $renderer; + + public function __construct() + { + $this->game = new LandlordGame(); + } + + public function initWindow(): void + { + $this->hWnd = win_create_window('斗地主 - PHP AOT', WINDOW_WIDTH, WINDOW_HEIGHT); + if ($this->hWnd == 0) { + echo "错误:窗口创建失败\n"; + return; + } + win_show_window($this->hWnd, SW_SHOW); + $this->renderer = new LandlordRenderer($this->hWnd); + } + + public function run(): void + { + $running = true; + while ($running) { + while (true) { + $msg = win_peek_message(); + if (count($msg) == 0) { + break; + } + $type = (int)($msg[1] ?? 0); + if ($type == WM_LBUTTONDOWN) { + $this->handleClick(mouse_x((int)($msg[3] ?? 0)), mouse_y((int)($msg[3] ?? 0))); + } elseif ($type == WM_KEYDOWN && (int)($msg[2] ?? 0) == VK_N) { + $this->game->newGame(); + } elseif ($type == WM_QUIT) { + $running = false; + break; + } + } + + if (win_quit_requested()) { + $running = false; + } + if (!$running) { + break; + } + + $this->game->updateAi(); + $this->renderer->render($this->game); + usleep(16000); + } + } + + private function handleClick(int $x, int $y): void + { + if (in_rect($x, $y, BTN_NEW_X, BTN_Y, BTN_W, BTN_H)) { + $this->game->newGame(); + return; + } + if (in_rect($x, $y, BTN_PLAY_X, BTN_Y, BTN_W, BTN_H)) { + $this->game->humanPrimaryAction(); + return; + } + if (in_rect($x, $y, BTN_PASS_X, BTN_Y, BTN_W, BTN_H)) { + $this->game->humanSecondaryAction(); + return; + } + + $idx = $this->renderer->hitTestHand($this->game, $x, $y); + if ($idx >= 0) { + $this->game->toggleCardAt($idx); + } + } +} + +function main(): void +{ + date_default_timezone_set('Asia/Shanghai'); + echo "斗地主 - PHP AOT Win32 示例\n"; + echo "Win32 只传递底层鼠标事件,游戏逻辑由 PHP 实现。\n"; + + $app = new LandlordApp(); + $app->initWindow(); + $app->run(); +} + diff --git a/examples/landlord-win32/php-src/card_rules.php b/examples/landlord-win32/php-src/card_rules.php new file mode 100644 index 00000000..e89e73fa --- /dev/null +++ b/examples/landlord-win32/php-src/card_rules.php @@ -0,0 +1,258 @@ += 0x8000 ? $x - 0x10000 : $x; +} + +function mouse_y(int $lParam): int +{ + $y = ($lParam >> 16) & 0xFFFF; + return $y >= 0x8000 ? $y - 0x10000 : $y; +} + +function in_rect(int $x, int $y, int $rx, int $ry, int $rw, int $rh): bool +{ + return $x >= $rx && $x <= $rx + $rw && $y >= $ry && $y <= $ry + $rh; +} + +function card_rank(int $card): int +{ + if ($card == 52) { + return 16; + } + if ($card == 53) { + return 17; + } + return intdiv($card, 4) + 3; +} + +function card_suit(int $card): int +{ + return $card % 4; +} + +function rank_label(int $rank): string +{ + $labels = [ + 3 => '3', 4 => '4', 5 => '5', 6 => '6', 7 => '7', 8 => '8', 9 => '9', + 10 => '10', 11 => 'J', 12 => 'Q', 13 => 'K', 14 => 'A', 15 => '2', + 16 => '小王', 17 => '大王', + ]; + return $labels[$rank] ?? '?'; +} + +function card_label(int $card): string +{ + $rank = card_rank($card); + if ($rank >= 16) { + return 'JOKER'; + } + $suits = ['黑', '红', '梅', '方']; + return rank_label($rank) . $suits[card_suit($card)]; +} + +function card_suit_label(int $card): string +{ + $rank = card_rank($card); + if ($rank >= 16) { + return ''; + } + $suits = ['♠', '♥', '♣', '♦']; + return $suits[card_suit($card)]; +} + +function sort_cards(array &$cards): void +{ + usort($cards, function (int $a, int $b): int { + $ra = card_rank($a); + $rb = card_rank($b); + return $ra == $rb ? card_suit($a) <=> card_suit($b) : $ra <=> $rb; + }); +} + +function rank_counts(array $cards): array +{ + $counts = []; + foreach ($cards as $card) { + $rank = card_rank($card); + $counts[$rank] = ($counts[$rank] ?? 0) + 1; + } + ksort($counts); + return $counts; +} + +function is_consecutive(array $ranks): bool +{ + sort($ranks); + for ($i = 1; $i < count($ranks); $i++) { + if ($ranks[$i] != $ranks[$i - 1] + 1) { + return false; + } + } + return true; +} + +function analyze_play(array $cards): array +{ + $n = count($cards); + if ($n == 0) { + return ['valid' => false, 'type' => 'none', 'main' => 0, 'len' => 0]; + } + + $counts = rank_counts($cards); + $ranks = array_keys($counts); + $values = array_values($counts); + sort($values); + + if ($n == 2 && in_array(16, $ranks, true) && in_array(17, $ranks, true)) { + return ['valid' => true, 'type' => 'rocket', 'main' => 17, 'len' => 2]; + } + if ($n == 4 && count($counts) == 1) { + return ['valid' => true, 'type' => 'bomb', 'main' => $ranks[0], 'len' => 4]; + } + if ($n == 6 && $values == [1, 1, 4]) { + foreach ($counts as $rank => $count) { + if ($count == 4) { + return ['valid' => true, 'type' => 'four_two_single', 'main' => $rank, 'len' => 6]; + } + } + } + if ($n == 8 && $values == [2, 2, 4]) { + foreach ($counts as $rank => $count) { + if ($count == 4) { + return ['valid' => true, 'type' => 'four_two_pair', 'main' => $rank, 'len' => 8]; + } + } + } + if ($n == 1) { + return ['valid' => true, 'type' => 'single', 'main' => $ranks[0], 'len' => 1]; + } + if ($n == 2 && count($counts) == 1) { + return ['valid' => true, 'type' => 'pair', 'main' => $ranks[0], 'len' => 2]; + } + if ($n == 3 && count($counts) == 1) { + return ['valid' => true, 'type' => 'triple', 'main' => $ranks[0], 'len' => 3]; + } + if ($n == 4 && $values == [1, 3]) { + foreach ($counts as $rank => $count) { + if ($count == 3) { + return ['valid' => true, 'type' => 'triple_single', 'main' => $rank, 'len' => 4]; + } + } + } + if ($n == 5 && $values == [2, 3]) { + foreach ($counts as $rank => $count) { + if ($count == 3) { + return ['valid' => true, 'type' => 'triple_pair', 'main' => $rank, 'len' => 5]; + } + } + } + if ($n >= 5 && count($counts) == $n && max($ranks) < 15 && is_consecutive($ranks)) { + return ['valid' => true, 'type' => 'straight', 'main' => max($ranks), 'len' => $n]; + } + if ($n >= 6 && $n % 2 == 0 && min($values) == 2 && max($values) == 2 && max($ranks) < 15 && is_consecutive($ranks)) { + return ['valid' => true, 'type' => 'pair_sequence', 'main' => max($ranks), 'len' => $n]; + } + if ($n >= 6 && $n % 3 == 0 && min($values) == 3 && max($values) == 3 && max($ranks) < 15 && is_consecutive($ranks)) { + return ['valid' => true, 'type' => 'airplane', 'main' => max($ranks), 'len' => $n]; + } + + return ['valid' => false, 'type' => 'invalid', 'main' => 0, 'len' => $n]; +} + +function can_beat(array $play, array $last): bool +{ + if (!$play['valid']) { + return false; + } + if (!$last['valid']) { + return true; + } + if ($play['type'] == 'rocket') { + return $last['type'] != 'rocket'; + } + if ($play['type'] == 'bomb' && $last['type'] != 'bomb' && $last['type'] != 'rocket') { + return true; + } + return $play['type'] == $last['type'] && $play['len'] == $last['len'] && $play['main'] > $last['main']; +} + +function play_name(array $play): string +{ + if (!$play['valid']) { + return '无'; + } + $names = [ + 'single' => '单牌', 'pair' => '对子', 'triple' => '三张', + 'triple_single' => '三带一', 'triple_pair' => '三带一对', + 'four_two_single' => '四带二', + 'four_two_pair' => '四带两对', + 'straight' => '顺子', 'pair_sequence' => '连对', + 'airplane' => '飞机', 'bomb' => '炸弹', 'rocket' => '王炸', + ]; + return ($names[$play['type']] ?? $play['type']) . ' ' . rank_label($play['main']); +} + +function remove_cards(array &$hand, array $cards): void +{ + foreach ($cards as $card) { + $idx = array_search($card, $hand, true); + if ($idx !== false) { + unset($hand[$idx]); + } + } + $hand = array_values($hand); +} + +function hand_strength(array $hand): int +{ + $score = 0; + $counts = rank_counts($hand); + foreach ($hand as $card) { + $rank = card_rank($card); + if ($rank >= 16) { + $score += 7; + } elseif ($rank == 15) { + $score += 4; + } elseif ($rank >= 13) { + $score += 2; + } + } + foreach ($counts as $rank => $count) { + if ($count == 4) { + $score += 9; + } elseif ($count == 3) { + $score += 4; + } elseif ($count == 2 && $rank >= 12) { + $score += 2; + } + } + return $score; +} diff --git a/examples/landlord-win32/php-src/win32.stub.php b/examples/landlord-win32/php-src/win32.stub.php new file mode 100644 index 00000000..24b3fe74 --- /dev/null +++ b/examples/landlord-win32/php-src/win32.stub.php @@ -0,0 +1,16 @@ +\n\n"; +echo " 3. 运行: tpc \n\n"; /** * 递归复制目录 diff --git a/version.txt b/version.txt index 5b146698..3cf4368c 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1080 +1082 \ No newline at end of file