The final stage of the Manue decision lifecycle involves synthesizing various probabilistic estimates into a unified candidateScore. This process transforms win probabilities, deal-in risks, and exhaustive draw projections into a scoreDeltaProbDist (Score Delta Probability Distribution), which is then mapped to an averageRank based on current game scores and historical statistics.
The core evaluation logic resides in evaluateCandidateFromComponents internal/domain/ai/candidate_score.go44-56 This function takes the discrete outputs from the win estimator and danger estimator and combines them with global game statistics to project the state of the game at the end of the current round.
The result of this evaluation is encapsulated in the candidateScore struct, which contains the metrics used to compare different actionCandidate objects.
| Field | Description |
|---|---|
averageRank | The primary metric for decision making; lower is better. |
expectedPoints | The expected point change for the self player. |
winProb / dealInProb | Probabilities of winning or dealing-in. |
exhaustiveDrawProb | Probability of the round ending in an exhaustive draw. |
otherWinProb | Probability of any other player winning. |
averageWinPoints | Mean points gained if a win occurs. |
exhaustiveDrawAveragePoints | Mean points gained if the round ends in exhaustive draw. |
Sources: internal/domain/ai/candidate_score.go9-26
The following diagram illustrates how raw estimates are transformed into the final score through the candidateEvaluator.
"Synthesis of Candidate Score"
Sources: internal/domain/ai/candidate_score.go44-95 internal/domain/ai/evaluator.go122-167
Manue uses a specialized type system to handle the uncertainty of Mahjong outcomes.
The scoreDeltaProbDist type represents a map where keys are scoreDelta (an array of floats representing point changes for all 4 players) and values are the probabilities of those changes occurring.
replace: This is a critical function for temporal chaining. It takes an "immediate" distribution (e.g., "did I deal in on this tile?") and replaces the "no-event" outcome with a "future" distribution (e.g., "what happens if the round continues?") internal/domain/ai/candidate_score.go83expected: Calculates the expected value vector for all players internal/domain/ai/evaluator.go209Sources: internal/domain/ai/score_delta_prob_dist.go5-12 internal/domain/ai/score_delta_prob_dist.go51-69
Win outcomes are modeled using winScoreFactor internal/domain/ai/win_score.go18-45 This logic accounts for the complex point-shifting rules of Mahjong (Ron vs. Tsumo, Dealer vs. Non-Dealer).
dealerSelfDrawPaymentFactor) internal/domain/ai/win_score.go10nonDealerSelfDrawOtherPaymentFactor) internal/domain/ai/win_score.go11Sources: internal/domain/ai/win_score.go9-45
When a round ends in an exhaustive draw (Ryukyoku), players are rewarded or penalized based on whether they are in Tenpai (ready to win).
The AI estimates the likelihood of each player being in Tenpai at the end of the round using two main functions:
tenpaiProb: Estimates current Tenpai probability based on the number of melds and turns remaining using YamitenCounts internal/domain/ai/exhaustive_draw_score.go12-22notenExhaustiveDrawTenpaiProb: Estimates the probability that a currently Noten player will reach Tenpai before the deck is exhausted, using historical ExhaustiveDrawTenpaiTurnFreq internal/domain/ai/exhaustive_draw_score.go27-42Sources: internal/domain/ai/exhaustive_draw_score.go12-42
The exhaustiveDrawScoreDeltaDist function calculates all possible combinations of Tenpai/Noten states for the four players, weights them by probability using an aheadVectorProbDist, and maps them to the standard point exchanges via ryukyokuScoreDelta internal/domain/ai/exhaustive_draw_score.go77-91
Sources: internal/domain/ai/exhaustive_draw_score.go77-91 internal/domain/ai/exhaustive_draw_score.go58-65
The final averageRank is calculated by applying the scoreChanges distribution to the current game scores.
The averageRank function (called in evaluateCandidateFromComponents) uses RankStats to predict the final rank distribution internal/domain/ai/candidate_score.go87-93 It considers:
state.Scores()).scoreDeltaProbDist projected for the end of the current round.Sources: internal/domain/ai/candidate_score.go87-93
When selecting the best action, the AI follows a strict priority order implemented in compareCandidateScore internal/domain/ai/candidate_score.go28-42 and chooseBestCandidate:
chooseBestCandidate, if two discard actions are identical in value, the AI prefers discarding a "black" tile over a "red" tile to preserve the red dora in hand internal/domain/ai/candidate_test.go13-46"Candidate Comparison Logic"
Sources: internal/domain/ai/candidate_score.go28-42 internal/domain/ai/candidate_test.go98-151
The Go port includes several bug fixes and refinements compared to the original CoffeeScript implementation.
winScoreFactorDist internal/domain/ai/win_score.go51-64Sources: internal/domain/ai/win_score.go51-64 internal/domain/ai/candidate_test.go13-46
Refresh this wiki