343 lines
9.7 KiB
Go
343 lines
9.7 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
|
|
}
|
|
|
|
// 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
|
|
}
|