From 7dadb23fbed4f4fb9442deef3b578ef3f4b7e145 Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Mon, 15 Jun 2026 20:09:39 +0330 Subject: [PATCH] fix: rule game for kot and hakem kot --- cmd/server/settler.go | 9 ++++++++ internal/game/hokm.go | 11 ++++++++-- internal/game/hokm_test.go | 43 ++++++++++++++++++++++++++++++++++++++ internal/ws/protocol.go | 1 + internal/ws/room.go | 9 ++++++-- internal/ws/settler.go | 11 ++++++---- internal/ws/ws_test.go | 3 +++ 7 files changed, 79 insertions(+), 8 deletions(-) diff --git a/cmd/server/settler.go b/cmd/server/settler.go index 5bcd326..579b7c8 100644 --- a/cmd/server/settler.go +++ b/cmd/server/settler.go @@ -46,3 +46,12 @@ func (g gameSettler) RecordGame(room, playersJSON string, winnerTeam int, kot bo 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 +} diff --git a/internal/game/hokm.go b/internal/game/hokm.go index f765607..186d9ea 100644 --- a/internal/game/hokm.go +++ b/internal/game/hokm.go @@ -45,8 +45,9 @@ type TrickCard struct { // HandResult نتیجه یک هَند. type HandResult struct { WinnerTeam int `json:"winner_team"` - Kot bool `json:"kot"` // کُت: تیم بازنده هیچ دستی نبرد - Points int `json:"points"` // امتیاز این هَند (۱ معمولی، ۲ کُت) + Kot bool `json:"kot"` // کُت: تیم بازنده هیچ دستی نبرد + HakemKot bool `json:"hakem_kot"` // حاکم‌کُت: تیمِ حاکم کُت شد (۳ امتیاز) + Points int `json:"points"` // امتیاز این هَند (۱ معمولی، ۲ کُت، ۳ حاکم‌کُت) NextHakem int `json:"next_hakem"` } @@ -269,6 +270,11 @@ func (g *Game) checkHandOver() { points := 1 if kot { points = 2 + // حاکم‌کُت: اگر تیمِ بازنده همان تیمِ حاکم باشد (حاکم صفر دست برده)، + // کُت سنگین‌تر است ⇒ ۳ امتیاز. + if Team(g.Hakem) == loserTeam { + points = 3 + } } g.Scores[winnerTeam] += points @@ -281,6 +287,7 @@ func (g *Game) checkHandOver() { g.LastResult = &HandResult{ WinnerTeam: winnerTeam, Kot: kot, + HakemKot: points == 3, Points: points, NextHakem: nextHakem, } diff --git a/internal/game/hokm_test.go b/internal/game/hokm_test.go index 1cc76dd..063d9d2 100644 --- a/internal/game/hokm_test.go +++ b/internal/game/hokm_test.go @@ -114,6 +114,49 @@ func TestFullGamePlaysOut(t *testing.T) { 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 در صورت امکان). func legalMove(g *Game, seat int) Card { hand := g.hands[seat] diff --git a/internal/ws/protocol.go b/internal/ws/protocol.go index 54e66c6..084931a 100644 --- a/internal/ws/protocol.go +++ b/internal/ws/protocol.go @@ -58,6 +58,7 @@ type handOverMsg struct { Type string `json:"type"` // "hand_over" WinnerTeam int `json:"winner_team"` Kot bool `json:"kot"` + HakemKot bool `json:"hakem_kot"` Points int `json:"points"` Scores [2]int `json:"scores"` } diff --git a/internal/ws/room.go b/internal/ws/room.go index 5ed64ea..ed04e87 100644 --- a/internal/ws/room.go +++ b/internal/ws/room.go @@ -101,7 +101,12 @@ func (r *Room) run() { // start بازی را ساخته، matched را برای انسان‌ها فرستاده و وضعیت اولیه را پخش می‌کند. 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 { if s.client != nil { s.client.trySend(mustJSON(r.matchedFor(seat))) @@ -127,7 +132,7 @@ func (r *Room) react() { res := r.game.LastResult r.broadcast(mustJSON(handOverMsg{ 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 { slog.Error("next hand", "room", r.ID, "err", err) diff --git a/internal/ws/settler.go b/internal/ws/settler.go index d928980..1704592 100644 --- a/internal/ws/settler.go +++ b/internal/ws/settler.go @@ -7,12 +7,15 @@ type Settler interface { Refund(userID int64, tier string) // بازگرداندن ورودی در صورت لغو AwardWinner(userID int64, tier string) // جایزه/XP/جام به برنده RecordGame(room, playersJSON string, winnerTeam int, kot bool) + // TargetHands تعداد هَندِ لازم برای برد بازی در این tier (مثلاً مبتدی=۳). + TargetHands(tier string) int } // noopSettler پیش‌فرض (برای تست‌ها و حالت بدون اقتصاد). type noopSettler struct{} -func (noopSettler) ChargeEntry(int64, string) error { return nil } -func (noopSettler) Refund(int64, string) {} -func (noopSettler) AwardWinner(int64, string) {} -func (noopSettler) RecordGame(string, string, int, bool) {} +func (noopSettler) ChargeEntry(int64, string) error { return nil } +func (noopSettler) Refund(int64, string) {} +func (noopSettler) AwardWinner(int64, string) {} +func (noopSettler) RecordGame(string, string, int, bool) {} +func (noopSettler) TargetHands(string) int { return 7 } diff --git a/internal/ws/ws_test.go b/internal/ws/ws_test.go index 9bb9d20..2edebb2 100644 --- a/internal/ws/ws_test.go +++ b/internal/ws/ws_test.go @@ -346,6 +346,9 @@ func (s *recordingSettler) RecordGame(string, string, int, bool) { s.records++ s.mu.Unlock() } + +// برای سرعتِ تست، بازی پس از ۲ هَند تمام می‌شود. +func (s *recordingSettler) TargetHands(string) int { return 2 } func (s *recordingSettler) snapshot() (c, r, a, rec int) { s.mu.Lock() defer s.mu.Unlock()