TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

1540 lines
52 KiB

<?php
class LandlordGame
{
public string $phase = 'bidding';
public array $scores = [500, 500, 500];
public array $hands = [[], [], []];
public array $bottom = [];
public array $lastCards = [];
public array $playedCards = [];
public array $lastPlay;
public int $lastPlayer = -1;
public int $currentTurn = 0;
public int $passCount = 0;
public int $winner = -1;
public int $landlord = -1;
public int $bidTurn = 0;
public int $bidStart = 0;
public int $bidWinner = -1;
public int $bidLevel = 0;
public int $bidActions = 0;
public int $bidPasses = 0;
public int $multiplier = 1;
public array $hasPlayed = [false, false, false];
public bool $settled = false;
public bool $robMode = false;
public array $selected = [];
public string $message = '';
public int $lastAiTime = 0;
public array $aiNames = [1 => '左侧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();
}