Files
back-hokm/internal/game/hokm.go
T
2026-06-24 21:01:21 +03:30

356 lines
10 KiB
Go

package game
import "errors"
// Phase مرحله بازی.
type Phase int
const (
PhaseChooseTrump Phase = iota // حاکم باید حکم بزند
PhasePlaying // در حال بازی دست‌ها (tricks)
PhaseHandOver // یک هَند تمام شد
PhaseGameOver // بازی تمام شد
)
func (p Phase) String() string {
switch p {
case PhaseChooseTrump:
return "choose_trump"
case PhasePlaying:
return "playing"
case PhaseHandOver:
return "hand_over"
case PhaseGameOver:
return "game_over"
}
return "unknown"
}
// خطاهای موتور.
var (
ErrWrongPhase = errors.New("wrong phase")
ErrNotYourTurn = errors.New("not your turn")
ErrNotHakem = errors.New("only hakem can choose trump")
ErrCardNotHeld = errors.New("card not in hand")
ErrMustFollow = errors.New("must follow lead suit")
ErrInvalidSeat = errors.New("invalid seat")
)
// TrickCard یک کارت انداخته‌شده روی زمین به‌همراه جایگاه بازیکن.
type TrickCard struct {
Seat int `json:"seat"`
Card Card `json:"card"`
}
// HandResult نتیجه یک هَند.
type HandResult struct {
WinnerTeam int `json:"winner_team"`
Kot bool `json:"kot"` // کُت: تیم بازنده هیچ دستی نبرد
HakemKot bool `json:"hakem_kot"` // حاکم‌کُت: تیمِ حاکم کُت شد (۳ امتیاز)
Points int `json:"points"` // امتیاز این هَند (۱ معمولی، ۲ کُت، ۳ حاکم‌کُت)
Hakem int `json:"hakem"` // حاکمِ همین هَند (پیش از چرخش)
NextHakem int `json:"next_hakem"`
}
// Team تیم یک جایگاه را برمی‌گرداند (0&2 تیم۰، 1&3 تیم۱).
func Team(seat int) int { return seat % 2 }
// Game وضعیت کامل یک میز حکم؛ فقط در حافظه نگه‌داری می‌شود.
type Game struct {
Phase Phase
Hakem int // جایگاه حاکم
Trump Suit // خال حکم
TrumpChosen bool // آیا حکم انتخاب شده
hands [4][]Card // کارت هر بازیکن (خصوصی)
deck []Card // باقیمانده دسته برای پخش پس از انتخاب حکم
Turn int // جایگاه نوبت فعلی
LeadSuit Suit // خال شروع‌کننده دست جاری
leadSet bool // آیا خال زمینه تعیین شده
Trick []TrickCard // کارت‌های دست جاری
TricksWon [2]int // تعداد دست‌های برده هر تیم در هَند جاری
TrickDone bool // دستِ کامل (۴ کارت) منتظر جمع‌آوری
pendingWinner int // برنده‌ی دستِ کاملِ منتظر
Scores [2]int // امتیاز کلی (تعداد هَند برده)
TargetScore int // امتیاز لازم برای برد بازی
LastResult *HandResult // نتیجه آخرین هَند (در PhaseHandOver/GameOver)
}
// NewGame یک میز با حاکم اولیه مشخص می‌سازد و اولین هَند را شروع می‌کند.
// hakem را می‌توان با FirstHakem از روی یک دسته تعیین کرد.
func NewGame(targetScore, hakem int) *Game {
g := &Game{
Hakem: hakem,
TargetScore: targetScore,
}
g.startHand()
return g
}
// FirstHakem با پخش کارت دور میز تا اولین آس، جایگاه حاکم اول را تعیین می‌کند.
func FirstHakem() int {
d := NewDeck()
Shuffle(d)
for i, c := range d {
if c.Rank == Ace {
return i % 4
}
}
return 0
}
// startHand دسته را بُر زده، ۵ کارت اول را به حاکم می‌دهد و منتظر انتخاب حکم می‌ماند.
func (g *Game) startHand() {
g.deck = NewDeck()
Shuffle(g.deck)
for i := range g.hands {
g.hands[i] = nil
}
// حاکم ۵ کارت اول را می‌گیرد و بر اساس آن حکم را انتخاب می‌کند.
g.hands[g.Hakem] = append(g.hands[g.Hakem], g.deck[:5]...)
g.Phase = PhaseChooseTrump
g.Trump = 0
g.TrumpChosen = false
g.Turn = g.Hakem
g.LeadSuit = 0
g.leadSet = false
g.Trick = nil
g.TricksWon = [2]int{}
g.TrickDone = false
}
// Reshuffle دستِ جاری را پیش از انتخابِ حکم دوباره بُر می‌زند و ۵ کارتِ تازه به
// همان حاکم می‌دهد (مصرفِ یک بلیت در لایه‌ی بالاتر انجام می‌شود).
func (g *Game) Reshuffle(seat int) error {
if g.Phase != PhaseChooseTrump {
return ErrWrongPhase
}
if seat != g.Hakem {
return ErrNotHakem
}
g.startHand() // همان حاکم، دستِ نو، باز هم منتظرِ انتخابِ حکم
return nil
}
// ChooseTrump حکم را توسط حاکم تعیین کرده و بقیه کارت‌ها را پخش می‌کند.
func (g *Game) ChooseTrump(seat int, s Suit) error {
if g.Phase != PhaseChooseTrump {
return ErrWrongPhase
}
if seat != g.Hakem {
return ErrNotHakem
}
g.Trump = s
g.TrumpChosen = true
g.dealRemaining()
g.Phase = PhasePlaying
g.Turn = g.Hakem // حاکم دست اول را شروع می‌کند
return nil
}
// dealRemaining باقی کارت‌ها را پخش می‌کند تا هر بازیکن ۱۳ کارت داشته باشد.
func (g *Game) dealRemaining() {
idx := 5 // ۵ کارت اول قبلاً به حاکم داده شد
deal := func(seat int) {
for len(g.hands[seat]) < 13 {
g.hands[seat] = append(g.hands[seat], g.deck[idx])
idx++
}
}
deal(g.Hakem)
for s := 0; s < 4; s++ {
if s != g.Hakem {
deal(s)
}
}
g.deck = nil
}
// PlayCard یک کارت را برای بازیکن seat بازی می‌کند.
func (g *Game) PlayCard(seat int, c Card) error {
if g.Phase != PhasePlaying {
return ErrWrongPhase
}
if g.TrickDone {
return ErrWrongPhase // دستِ کامل هنوز جمع نشده (CollectTrick)
}
if seat < 0 || seat > 3 {
return ErrInvalidSeat
}
if seat != g.Turn {
return ErrNotYourTurn
}
idx := indexOf(g.hands[seat], c)
if idx < 0 {
return ErrCardNotHeld
}
// قانون follow: اگر خال زمینه تعیین شده و بازیکن آن خال را دارد، باید همان را بازی کند.
if g.leadSet && c.Suit != g.LeadSuit && hasSuit(g.hands[seat], g.LeadSuit) {
return ErrMustFollow
}
// حذف کارت از دست
g.hands[seat] = append(g.hands[seat][:idx], g.hands[seat][idx+1:]...)
if !g.leadSet {
g.LeadSuit = c.Suit
g.leadSet = true
}
g.Trick = append(g.Trick, TrickCard{Seat: seat, Card: c})
if len(g.Trick) < 4 {
g.Turn = (g.Turn + 1) % 4
return nil
}
// دست کامل شد ⇒ برنده تعیین می‌شود ولی جمع‌آوری به CollectTrick موکول می‌شود
// تا کلاینت‌ها فرصت دیدن هر ۴ کارت روی زمین را داشته باشند.
g.pendingWinner = g.trickWinner()
g.TrickDone = true
return nil
}
// CollectTrick دستِ کامل را جمع کرده، برنده را ثبت و نوبت بعد را تنظیم می‌کند.
// پس از PhasePlaying و وقتی TrickDone باشد فراخوانی می‌شود.
func (g *Game) CollectTrick() error {
if !g.TrickDone {
return ErrWrongPhase
}
winner := g.pendingWinner
g.TricksWon[Team(winner)]++
g.Trick = nil
g.leadSet = false
g.Turn = winner
g.TrickDone = false
g.checkHandOver()
return nil
}
// trickWinner برنده دست جاری را تعیین می‌کند.
func (g *Game) trickWinner() int {
best := g.Trick[0]
for _, tc := range g.Trick[1:] {
if beats(tc.Card, best.Card, g.Trump, g.LeadSuit) {
best = tc
}
}
return best.Seat
}
// beats مشخص می‌کند آیا a کارت b را می‌برد (با توجه به حکم و خال زمینه).
func beats(a, b Card, trump, lead Suit) bool {
aTrump := a.Suit == trump
bTrump := b.Suit == trump
switch {
case aTrump && !bTrump:
return true
case !aTrump && bTrump:
return false
case aTrump && bTrump:
return a.Rank > b.Rank
default:
// هیچ‌کدام آتو نیستند: فقط کارت هم‌خالِ زمینه می‌تواند ببرد
if a.Suit != lead {
return false
}
if b.Suit != lead {
return true
}
return a.Rank > b.Rank
}
}
// checkHandOver بررسی می‌کند آیا تیمی به ۷ دست رسیده و هَند را تسویه می‌کند.
func (g *Game) checkHandOver() {
const tricksToWin = 7
var winnerTeam = -1
if g.TricksWon[0] >= tricksToWin {
winnerTeam = 0
} else if g.TricksWon[1] >= tricksToWin {
winnerTeam = 1
}
if winnerTeam == -1 {
return
}
loserTeam := 1 - winnerTeam
kot := g.TricksWon[loserTeam] == 0
points := 1
if kot {
points = 2
// حاکم‌کُت: اگر تیمِ بازنده همان تیمِ حاکم باشد (حاکم صفر دست برده)،
// کُت سنگین‌تر است ⇒ ۳ امتیاز.
if Team(g.Hakem) == loserTeam {
points = 3
}
}
g.Scores[winnerTeam] += points
// چرخش حاکمی: اگر تیم حاکم برد، حاکم می‌ماند؛ وگرنه به نفر بعد می‌رسد.
nextHakem := g.Hakem
if Team(g.Hakem) != winnerTeam {
nextHakem = (g.Hakem + 1) % 4
}
g.LastResult = &HandResult{
WinnerTeam: winnerTeam,
Kot: kot,
HakemKot: points == 3,
Points: points,
Hakem: g.Hakem, // حاکمِ این هَند (هنوز چرخش نکرده)
NextHakem: nextHakem,
}
if g.Scores[winnerTeam] >= g.TargetScore {
g.Phase = PhaseGameOver
} else {
g.Phase = PhaseHandOver
g.Hakem = nextHakem
}
}
// NextHand هَند بعدی را شروع می‌کند (پس از PhaseHandOver).
func (g *Game) NextHand() error {
if g.Phase != PhaseHandOver {
return ErrWrongPhase
}
g.startHand()
return nil
}
// Hand کپی کارت‌های یک بازیکن را برمی‌گرداند.
func (g *Game) Hand(seat int) []Card {
out := make([]Card, len(g.hands[seat]))
copy(out, g.hands[seat])
return out
}
// HandCount تعداد کارت‌های هر بازیکن.
func (g *Game) HandCount(seat int) int { return len(g.hands[seat]) }
// --- کمکی‌ها ---
func indexOf(hand []Card, c Card) int {
for i, x := range hand {
if x == c {
return i
}
}
return -1
}
func hasSuit(hand []Card, s Suit) bool {
for _, c := range hand {
if c.Suit == s {
return true
}
}
return false
}