This commit is contained in:
2026-06-15 16:53:25 +03:30
commit 390ef96e7e
34 changed files with 4463 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
package game
import (
"crypto/rand"
"fmt"
"math/big"
"strings"
)
// Suit خال کارت.
type Suit uint8
const (
Spades Suit = iota
Hearts
Diamonds
Clubs
)
var suitChar = [4]byte{'S', 'H', 'D', 'C'}
var suitName = [4]string{"spades", "hearts", "diamonds", "clubs"}
func (s Suit) String() string { return suitName[s] }
// ParseSuit نام انگلیسی خال را به Suit تبدیل می‌کند.
func ParseSuit(name string) (Suit, error) {
switch strings.ToLower(name) {
case "spades", "s":
return Spades, nil
case "hearts", "h":
return Hearts, nil
case "diamonds", "d":
return Diamonds, nil
case "clubs", "c":
return Clubs, nil
}
return 0, fmt.Errorf("invalid suit: %q", name)
}
// Rank ارزش کارت؛ 2..10 معمولی، 11=J 12=Q 13=K 14=A.
type Rank uint8
const (
Jack Rank = 11
Queen Rank = 12
King Rank = 13
Ace Rank = 14
)
// Card یک کارت بازی.
type Card struct {
Suit Suit
Rank Rank
}
// String کد کوتاه کارت مثل "AS", "10H", "2C".
func (c Card) String() string {
var r string
switch c.Rank {
case Ace:
r = "A"
case King:
r = "K"
case Queen:
r = "Q"
case Jack:
r = "J"
default:
r = fmt.Sprintf("%d", c.Rank)
}
return r + string(suitChar[c.Suit])
}
// ParseCard کد کوتاه کارت را پارس می‌کند (مثلاً "10H", "AS").
func ParseCard(code string) (Card, error) {
code = strings.ToUpper(strings.TrimSpace(code))
if len(code) < 2 {
return Card{}, fmt.Errorf("invalid card: %q", code)
}
rankStr := code[:len(code)-1]
suitStr := code[len(code)-1:]
s, err := ParseSuit(suitStr)
if err != nil {
return Card{}, fmt.Errorf("invalid card %q: %w", code, err)
}
var r Rank
switch rankStr {
case "A":
r = Ace
case "K":
r = King
case "Q":
r = Queen
case "J":
r = Jack
default:
var n int
if _, err := fmt.Sscanf(rankStr, "%d", &n); err != nil || n < 2 || n > 10 {
return Card{}, fmt.Errorf("invalid rank in card %q", code)
}
r = Rank(n)
}
return Card{Suit: s, Rank: r}, nil
}
// NewDeck یک دسته ۵۲ کارتی مرتب می‌سازد.
func NewDeck() []Card {
deck := make([]Card, 0, 52)
for s := Suit(0); s < 4; s++ {
for r := Rank(2); r <= Ace; r++ {
deck = append(deck, Card{Suit: s, Rank: r})
}
}
return deck
}
// Shuffle دسته را با crypto/rand به‌صورت Fisher-Yates بُر می‌زند.
func Shuffle(deck []Card) {
for i := len(deck) - 1; i > 0; i-- {
jBig, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
if err != nil {
continue // عملاً رخ نمی‌دهد
}
j := int(jBig.Int64())
deck[i], deck[j] = deck[j], deck[i]
}
}
+333
View File
@@ -0,0 +1,333 @@
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"` // کُت: تیم بازنده هیچ دستی نبرد
Points int `json:"points"` // امتیاز این هَند (۱ معمولی، ۲ کُت)
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
}
g.Scores[winnerTeam] += points
// چرخش حاکمی: اگر تیم حاکم برد، حاکم می‌ماند؛ وگرنه به نفر بعد می‌رسد.
nextHakem := g.Hakem
if Team(g.Hakem) != winnerTeam {
nextHakem = (g.Hakem + 1) % 4
}
g.LastResult = &HandResult{
WinnerTeam: winnerTeam,
Kot: kot,
Points: points,
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
}
+128
View File
@@ -0,0 +1,128 @@
package game
import "testing"
func TestParseCardRoundTrip(t *testing.T) {
cases := []string{"AS", "10H", "2C", "KD", "JS", "QH"}
for _, code := range cases {
c, err := ParseCard(code)
if err != nil {
t.Fatalf("parse %q: %v", code, err)
}
if c.String() != code {
t.Errorf("roundtrip %q -> %q", code, c.String())
}
}
}
func TestBeats(t *testing.T) {
trump := Hearts
lead := Spades
// آتو بر غیرآتو می‌برد
if !beats(Card{Hearts, 2}, Card{Spades, Ace}, trump, lead) {
t.Error("trump 2 should beat non-trump Ace")
}
// بالاترین خال زمینه بر کارت خال دیگر (غیرآتو) می‌برد
if !beats(Card{Spades, 5}, Card{Diamonds, Ace}, trump, lead) {
t.Error("lead-suit card should beat off-suit card")
}
// کارت خارج از خال زمینه و غیرآتو نمی‌برد
if beats(Card{Diamonds, Ace}, Card{Spades, 2}, trump, lead) {
t.Error("off-suit non-trump should not beat lead suit")
}
}
func TestMustFollowSuit(t *testing.T) {
g := NewGame(7, 0)
if err := g.ChooseTrump(0, Hearts); err != nil {
t.Fatal(err)
}
// حاکم (seat 0) شروع می‌کند. یک کارت از دستش بازی می‌کنیم.
lead := g.hands[0][0]
if err := g.PlayCard(0, lead); err != nil {
t.Fatal(err)
}
// بازیکن بعدی اگر کارت هم‌خال دارد باید همان را بزند.
next := g.Turn
if hasSuit(g.hands[next], lead.Suit) {
// یک کارت با خال متفاوت پیدا کن
for _, c := range g.hands[next] {
if c.Suit != lead.Suit {
if err := g.PlayCard(next, c); err != ErrMustFollow {
t.Fatalf("expected ErrMustFollow, got %v", err)
}
return
}
}
}
}
// playFullGame یک بازی کامل را با یک ربات ساده تا پایان پیش می‌برد و
// تمام نامتغیرها (invariants) را بررسی می‌کند.
func TestFullGamePlaysOut(t *testing.T) {
g := NewGame(7, FirstHakem())
for hand := 0; g.Phase != PhaseGameOver; hand++ {
if hand > 100 {
t.Fatal("too many hands; engine likely stuck")
}
// انتخاب حکم توسط حاکم
if g.Phase != PhaseChooseTrump {
t.Fatalf("expected choose_trump, got %s", g.Phase)
}
if err := g.ChooseTrump(g.Hakem, g.hands[g.Hakem][0].Suit); err != nil {
t.Fatal(err)
}
// بررسی: هر بازیکن دقیقاً ۱۳ کارت دارد
for s := 0; s < 4; s++ {
if g.HandCount(s) != 13 {
t.Fatalf("seat %d has %d cards after deal", s, g.HandCount(s))
}
}
// بازی دست‌ها تا پایان هَند
for g.Phase == PhasePlaying {
if g.TrickDone {
if err := g.CollectTrick(); err != nil {
t.Fatalf("collect trick: %v", err)
}
continue
}
seat := g.Turn
card := legalMove(g, seat)
if err := g.PlayCard(seat, card); err != nil {
t.Fatalf("play %s by seat %d: %v", card, seat, err)
}
}
// بعد از هَند: یا تمام شده یا hand_over
if g.Phase == PhaseHandOver {
r := g.LastResult
if r.Points < 1 || r.Points > 2 {
t.Fatalf("unexpected points %d", r.Points)
}
if err := g.NextHand(); err != nil {
t.Fatal(err)
}
}
}
// در پایان یک تیم باید به امتیاز هدف رسیده باشد
if g.Scores[0] < g.TargetScore && g.Scores[1] < g.TargetScore {
t.Fatalf("game over but no team reached target: %v", g.Scores)
}
t.Logf("final scores: %v", g.Scores)
}
// legalMove یک حرکت مجاز برای ربات انتخاب می‌کند (follow suit در صورت امکان).
func legalMove(g *Game, seat int) Card {
hand := g.hands[seat]
if g.leadSet {
for _, c := range hand {
if c.Suit == g.LeadSuit {
return c
}
}
}
return hand[0]
}