init
This commit is contained in:
@@ -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]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
Reference in New Issue
Block a user