fix: rule game for kot and hakem kot

This commit is contained in:
2026-06-15 20:09:39 +03:30
parent 390ef96e7e
commit 7dadb23fbe
7 changed files with 79 additions and 8 deletions
+9
View File
@@ -46,3 +46,12 @@ func (g gameSettler) RecordGame(room, playersJSON string, winnerTeam int, kot bo
slog.Error("record game", "room", room, "err", err) slog.Error("record game", "room", room, "err", err)
} }
} }
// TargetHands تعداد هَندِ لازم برای برد بازی در این tier (مثلاً مبتدی=۳).
func (g gameSettler) TargetHands(tier string) int {
t := economy.FindTier(tier)
if t == nil || t.Hands <= 0 {
return 7
}
return t.Hands
}
+9 -2
View File
@@ -45,8 +45,9 @@ type TrickCard struct {
// HandResult نتیجه یک هَند. // HandResult نتیجه یک هَند.
type HandResult struct { type HandResult struct {
WinnerTeam int `json:"winner_team"` WinnerTeam int `json:"winner_team"`
Kot bool `json:"kot"` // کُت: تیم بازنده هیچ دستی نبرد Kot bool `json:"kot"` // کُت: تیم بازنده هیچ دستی نبرد
Points int `json:"points"` // امتیاز این هَند (۱ معمولی، ۲ کُت) HakemKot bool `json:"hakem_kot"` // حاکم‌کُت: تیمِ حاکم کُت شد (۳ امتیاز)
Points int `json:"points"` // امتیاز این هَند (۱ معمولی، ۲ کُت، ۳ حاکم‌کُت)
NextHakem int `json:"next_hakem"` NextHakem int `json:"next_hakem"`
} }
@@ -269,6 +270,11 @@ func (g *Game) checkHandOver() {
points := 1 points := 1
if kot { if kot {
points = 2 points = 2
// حاکم‌کُت: اگر تیمِ بازنده همان تیمِ حاکم باشد (حاکم صفر دست برده)،
// کُت سنگین‌تر است ⇒ ۳ امتیاز.
if Team(g.Hakem) == loserTeam {
points = 3
}
} }
g.Scores[winnerTeam] += points g.Scores[winnerTeam] += points
@@ -281,6 +287,7 @@ func (g *Game) checkHandOver() {
g.LastResult = &HandResult{ g.LastResult = &HandResult{
WinnerTeam: winnerTeam, WinnerTeam: winnerTeam,
Kot: kot, Kot: kot,
HakemKot: points == 3,
Points: points, Points: points,
NextHakem: nextHakem, NextHakem: nextHakem,
} }
+43
View File
@@ -114,6 +114,49 @@ func TestFullGamePlaysOut(t *testing.T) {
t.Logf("final scores: %v", g.Scores) t.Logf("final scores: %v", g.Scores)
} }
// TestTargetScoreRespected بررسی می‌کند بازی دقیقاً وقتی یک تیم به TargetScore
// رسید تمام می‌شود (نه زودتر، نه دیرتر) — برای میزهایی مثل مبتدی (۳ دست).
func TestTargetScoreRespected(t *testing.T) {
for _, target := range []int{1, 3, 5, 7} {
for trial := 0; trial < 20; trial++ {
g := NewGame(target, FirstHakem())
for g.Phase != PhaseGameOver {
switch {
case g.TrickDone:
if err := g.CollectTrick(); err != nil {
t.Fatal(err)
}
case g.Phase == PhaseChooseTrump:
if err := g.ChooseTrump(g.Hakem, g.hands[g.Hakem][0].Suit); err != nil {
t.Fatal(err)
}
case g.Phase == PhaseHandOver:
if err := g.NextHand(); err != nil {
t.Fatal(err)
}
default: // playing
if err := g.PlayCard(g.Turn, legalMove(g, g.Turn)); err != nil {
t.Fatalf("play: %v", err)
}
}
}
w := g.Scores[0]
if g.Scores[1] > w {
w = g.Scores[1]
}
if w < target {
t.Fatalf("target %d: game over but max score %d < target (scores %v)",
target, w, g.Scores)
}
// بازنده نباید به هدف رسیده باشد (بازی باید به‌محضِ رسیدنِ یک تیم تمام شود).
loser := g.Scores[0] + g.Scores[1] - w
if loser >= target {
t.Fatalf("target %d: both teams reached target %v", target, g.Scores)
}
}
}
}
// legalMove یک حرکت مجاز برای ربات انتخاب می‌کند (follow suit در صورت امکان). // legalMove یک حرکت مجاز برای ربات انتخاب می‌کند (follow suit در صورت امکان).
func legalMove(g *Game, seat int) Card { func legalMove(g *Game, seat int) Card {
hand := g.hands[seat] hand := g.hands[seat]
+1
View File
@@ -58,6 +58,7 @@ type handOverMsg struct {
Type string `json:"type"` // "hand_over" Type string `json:"type"` // "hand_over"
WinnerTeam int `json:"winner_team"` WinnerTeam int `json:"winner_team"`
Kot bool `json:"kot"` Kot bool `json:"kot"`
HakemKot bool `json:"hakem_kot"`
Points int `json:"points"` Points int `json:"points"`
Scores [2]int `json:"scores"` Scores [2]int `json:"scores"`
} }
+7 -2
View File
@@ -101,7 +101,12 @@ func (r *Room) run() {
// start بازی را ساخته، matched را برای انسان‌ها فرستاده و وضعیت اولیه را پخش می‌کند. // start بازی را ساخته، matched را برای انسان‌ها فرستاده و وضعیت اولیه را پخش می‌کند.
func (r *Room) start() { func (r *Room) start() {
r.game = game.NewGame(targetScore, game.FirstHakem()) // تعداد هَندِ لازم برای برد بازی از روی نوعِ میز (tier) تعیین می‌شود.
target := r.settler.TargetHands(r.tier)
if target <= 0 {
target = targetScore
}
r.game = game.NewGame(target, game.FirstHakem())
for seat, s := range r.seats { for seat, s := range r.seats {
if s.client != nil { if s.client != nil {
s.client.trySend(mustJSON(r.matchedFor(seat))) s.client.trySend(mustJSON(r.matchedFor(seat)))
@@ -127,7 +132,7 @@ func (r *Room) react() {
res := r.game.LastResult res := r.game.LastResult
r.broadcast(mustJSON(handOverMsg{ r.broadcast(mustJSON(handOverMsg{
Type: "hand_over", WinnerTeam: res.WinnerTeam, Kot: res.Kot, Type: "hand_over", WinnerTeam: res.WinnerTeam, Kot: res.Kot,
Points: res.Points, Scores: r.game.Scores, HakemKot: res.HakemKot, Points: res.Points, Scores: r.game.Scores,
})) }))
if err := r.game.NextHand(); err != nil { if err := r.game.NextHand(); err != nil {
slog.Error("next hand", "room", r.ID, "err", err) slog.Error("next hand", "room", r.ID, "err", err)
+7 -4
View File
@@ -7,12 +7,15 @@ type Settler interface {
Refund(userID int64, tier string) // بازگرداندن ورودی در صورت لغو Refund(userID int64, tier string) // بازگرداندن ورودی در صورت لغو
AwardWinner(userID int64, tier string) // جایزه/XP/جام به برنده AwardWinner(userID int64, tier string) // جایزه/XP/جام به برنده
RecordGame(room, playersJSON string, winnerTeam int, kot bool) RecordGame(room, playersJSON string, winnerTeam int, kot bool)
// TargetHands تعداد هَندِ لازم برای برد بازی در این tier (مثلاً مبتدی=۳).
TargetHands(tier string) int
} }
// noopSettler پیش‌فرض (برای تست‌ها و حالت بدون اقتصاد). // noopSettler پیش‌فرض (برای تست‌ها و حالت بدون اقتصاد).
type noopSettler struct{} type noopSettler struct{}
func (noopSettler) ChargeEntry(int64, string) error { return nil } func (noopSettler) ChargeEntry(int64, string) error { return nil }
func (noopSettler) Refund(int64, string) {} func (noopSettler) Refund(int64, string) {}
func (noopSettler) AwardWinner(int64, string) {} func (noopSettler) AwardWinner(int64, string) {}
func (noopSettler) RecordGame(string, string, int, bool) {} func (noopSettler) RecordGame(string, string, int, bool) {}
func (noopSettler) TargetHands(string) int { return 7 }
+3
View File
@@ -346,6 +346,9 @@ func (s *recordingSettler) RecordGame(string, string, int, bool) {
s.records++ s.records++
s.mu.Unlock() s.mu.Unlock()
} }
// برای سرعتِ تست، بازی پس از ۲ هَند تمام می‌شود.
func (s *recordingSettler) TargetHands(string) int { return 2 }
func (s *recordingSettler) snapshot() (c, r, a, rec int) { func (s *recordingSettler) snapshot() (c, r, a, rec int) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()