603 lines
20 KiB
Go
603 lines
20 KiB
Go
package ws
|
|
|
|
import (
|
|
"log/slog"
|
|
"time"
|
|
|
|
"hakemsho/internal/game"
|
|
)
|
|
|
|
const targetScore = 7 // امتیاز لازم برای برد بازی
|
|
|
|
// actionKind نوع پیام ورودی به goroutine میز.
|
|
type actionKind int
|
|
|
|
const (
|
|
akInput actionKind = iota // ورودی بازیکن (choose_trump/play_card/leave)
|
|
akTimeout // سررسید تایمر نوبت
|
|
akDisconnect // قطع اتصال یک بازیکن
|
|
akReconnect // اتصال مجدد یک بازیکن
|
|
akCollect // جمعآوری دستِ کامل پس از نمایش
|
|
)
|
|
|
|
// roomAction یک رویداد برای پردازش در goroutine میز.
|
|
type roomAction struct {
|
|
kind actionKind
|
|
seat int
|
|
client *Client // برای akReconnect و تأیید فرستنده در akInput/akDisconnect
|
|
gen int // برای akTimeout: نسل تایمر (تشخیص تایمر کهنه)
|
|
msg inboundMsg // برای akInput
|
|
}
|
|
|
|
// seatInfo وضعیت یک جایگاه؛ فقط توسط goroutine میز خوانده/نوشته میشود (single-writer).
|
|
type seatInfo struct {
|
|
client *Client
|
|
isBot bool
|
|
userID int64
|
|
name string
|
|
connected bool
|
|
left bool // خروج دائمی داوطلبانه (در تسویه از بازگشت ورودی محروم)
|
|
coins int64 // برای نمایش سرِ میز
|
|
rankTier string // نشانِ رتبه برای نمایش سرِ میز
|
|
avatar string // seed آواتارِ انتخابیِ کاربر (برای نمایش سرِ میز)
|
|
frame string // قابِ آواتارِ انتخابیِ کاربر (کازمتیک)
|
|
}
|
|
|
|
// auto مشخص میکند جایگاه باید خودکار بازی شود (بات یا قطعشده).
|
|
func (s *seatInfo) auto() bool { return s.isBot || !s.connected }
|
|
|
|
// Room یک میز بازی؛ تنها goroutine خودش روی state بازی و seatها مینویسد.
|
|
type Room struct {
|
|
ID string
|
|
hub *Hub
|
|
tier string
|
|
private bool // میز دورهمی: بدون ورودی/جایزهی tier (فقط آمار و شرطِ سکه)
|
|
handsOverride int // تعداد دستِ سفارشیِ میز خصوصی (۰ یعنی از tier)
|
|
stake int64 // شرطِ سکهایِ هر بازیکن در میز خصوصی (۰ یعنی رایگان)
|
|
pot int64 // مجموعِ سکههای جمعآوریشده برای برنده
|
|
paid [4]bool // جایگاههایی که شرط را پرداختهاند (برای بازگشت هنگام لغو)
|
|
settler Settler
|
|
seats [4]*seatInfo
|
|
game *game.Game
|
|
actions chan roomAction
|
|
ended bool
|
|
turnGen int
|
|
timer *time.Timer // تایمر فعال (نوبت یا جمعآوری)؛ فقط goroutine میز به آن دست میزند
|
|
turnTimeout time.Duration
|
|
botDelay time.Duration
|
|
trickHold time.Duration
|
|
firstHandDelay time.Duration // پنجرهی بُر زدن+پخشِ ۵ کارت (ابتدای هَند)
|
|
dealDelay time.Duration // پنجرهی پخشِ بقیهی کارتها (پس از انتخابِ حکم)
|
|
// graceUntil: تا این لحظه هیچ حرکتِ خودکاری انجام نمیشود (هماهنگ با انیمیشنهای کلاینت).
|
|
graceUntil time.Time
|
|
chatAt [4]time.Time // آخرین زمانِ چتِ هر جایگاه (ضدِ اسپم)
|
|
// seen: حافظهی کارتهای بازیشده در هَندِ جاری (برای هوشِ باتِ کارتشمار).
|
|
// بیتِ (rank-2) از seen[suit] یعنی آن کارت رو شده. هر هَند ریست میشود.
|
|
seen [4]uint16
|
|
}
|
|
|
|
func newRoom(id string, seats [4]*seatInfo, hub *Hub) *Room {
|
|
return &Room{
|
|
ID: id,
|
|
hub: hub,
|
|
settler: hub.settler,
|
|
seats: seats,
|
|
actions: make(chan roomAction, 64),
|
|
turnTimeout: hub.turnTimeout,
|
|
botDelay: hub.botDelay,
|
|
trickHold: hub.trickHold,
|
|
firstHandDelay: hub.firstHandDelay,
|
|
dealDelay: hub.dealDelay,
|
|
}
|
|
}
|
|
|
|
// run حلقه اصلی میز.
|
|
func (r *Room) run() {
|
|
r.start()
|
|
for act := range r.actions {
|
|
if r.ended {
|
|
continue
|
|
}
|
|
switch act.kind {
|
|
case akInput:
|
|
r.handleInput(act)
|
|
case akTimeout:
|
|
if act.gen == r.turnGen {
|
|
r.autoMove(act.seat)
|
|
}
|
|
case akDisconnect:
|
|
r.handleDisconnect(act.seat, act.client)
|
|
case akReconnect:
|
|
r.handleReconnect(act.seat, act.client)
|
|
case akCollect:
|
|
r.collect()
|
|
}
|
|
if r.ended {
|
|
return // بازی تمام شد ⇒ خروج از goroutine
|
|
}
|
|
}
|
|
}
|
|
|
|
// start بازی را ساخته، matched را برای انسانها فرستاده و وضعیت اولیه را پخش میکند.
|
|
func (r *Room) start() {
|
|
// تعداد هَندِ لازم برای برد بازی از روی نوعِ میز (tier) تعیین میشود.
|
|
target := r.settler.TargetHands(r.tier)
|
|
if r.handsOverride > 0 {
|
|
target = r.handsOverride // میز خصوصی: تعداد دستِ انتخابیِ میزبان
|
|
}
|
|
if target <= 0 {
|
|
target = targetScore
|
|
}
|
|
r.game = game.NewGame(target, game.FirstHakem())
|
|
r.graceUntil = time.Now().Add(r.firstHandDelay) // بُر زدن + پخشِ ۵ کارت
|
|
// شرطِ سکهایِ میزِ خصوصی: از هر بازیکنِ انسان کسر و به pot اضافه میشود
|
|
// (هر که موجودی نداشت، رایگان بازی میکند و به pot اضافه نمیشود).
|
|
if r.stake > 0 {
|
|
for seat, s := range r.seats {
|
|
if s.isBot || s.userID == 0 {
|
|
continue
|
|
}
|
|
if err := r.settler.ChargeCoins(s.userID, r.stake, r.ID); err == nil {
|
|
r.pot += r.stake
|
|
r.paid[seat] = true
|
|
}
|
|
}
|
|
}
|
|
// اطلاعاتِ نمایشیِ بازیکنان (سکه و نشانِ رتبه) را یکبار هنگام شروع میگیریم.
|
|
for _, s := range r.seats {
|
|
if s.isBot || s.userID == 0 {
|
|
continue
|
|
}
|
|
s.coins, s.rankTier, s.avatar, s.frame = r.settler.PlayerProfile(s.userID)
|
|
}
|
|
for seat, s := range r.seats {
|
|
if s.client != nil {
|
|
s.client.trySend(mustJSON(r.matchedFor(seat)))
|
|
}
|
|
}
|
|
r.react()
|
|
}
|
|
|
|
func (r *Room) matchedFor(seat int) matchedMsg {
|
|
return matchedMsg{Type: "matched", Room: r.ID, Seat: seat, Players: r.playersInfo()}
|
|
}
|
|
|
|
// react پس از هر تغییر state: نتیجه را اعلام و تایمر بازیگر بعدی را مسلح میکند.
|
|
func (r *Room) react() {
|
|
// دستِ کامل (۴ کارت) را نشان بده، کمی نگه دار، سپس جمع کن.
|
|
if r.game.TrickDone {
|
|
r.broadcastState()
|
|
r.scheduleCollect()
|
|
return
|
|
}
|
|
switch r.game.Phase {
|
|
case game.PhaseHandOver:
|
|
res := r.game.LastResult
|
|
r.recordHandStats(res)
|
|
r.broadcast(mustJSON(handOverMsg{
|
|
Type: "hand_over", WinnerTeam: res.WinnerTeam, Kot: res.Kot,
|
|
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)
|
|
r.refundAll()
|
|
r.endRoom()
|
|
return
|
|
}
|
|
r.graceUntil = time.Now().Add(r.firstHandDelay) // هَندِ جدید ⇒ بُر زدن + پخشِ ۵ کارت
|
|
r.broadcastState()
|
|
r.armTimer()
|
|
case game.PhaseGameOver:
|
|
res := r.game.LastResult
|
|
r.recordHandStats(res)
|
|
r.broadcast(mustJSON(gameOverMsg{
|
|
Type: "game_over", WinnerTeam: res.WinnerTeam, Scores: r.game.Scores,
|
|
}))
|
|
r.settleGameOver(res)
|
|
r.endRoom()
|
|
default: // choose_trump یا playing
|
|
r.broadcastState()
|
|
r.armTimer()
|
|
}
|
|
}
|
|
|
|
// stopTimer تایمر فعال را لغو میکند تا پیامهای کهنه کانال را پر نکنند.
|
|
func (r *Room) stopTimer() {
|
|
if r.timer != nil {
|
|
r.timer.Stop()
|
|
r.timer = nil
|
|
}
|
|
}
|
|
|
|
// scheduleCollect پس از نمایش دستِ کامل، جمعآوری را زمانبندی میکند.
|
|
func (r *Room) scheduleCollect() {
|
|
r.stopTimer()
|
|
r.turnGen++ // هر تایمر نوبتِ کهنه را بیاثر کن
|
|
r.timer = time.AfterFunc(r.trickHold, func() {
|
|
select {
|
|
case r.actions <- roomAction{kind: akCollect}:
|
|
default:
|
|
}
|
|
})
|
|
}
|
|
|
|
// collect دستِ کامل را جمع کرده و به وضعیت بعدی میرود.
|
|
func (r *Room) collect() {
|
|
if err := r.game.CollectTrick(); err != nil {
|
|
return // قبلاً جمع شده یا نامعتبر
|
|
}
|
|
r.react()
|
|
}
|
|
|
|
// armTimer برای بازیگر فعلی (حاکم در انتخاب حکم، یا نوبتدار در بازی) تایمر میگذارد.
|
|
func (r *Room) armTimer() {
|
|
r.stopTimer()
|
|
if r.game.TrickDone {
|
|
return // در زمان نمایش دستِ کامل تایمر نوبت نگذار
|
|
}
|
|
var actor int
|
|
switch r.game.Phase {
|
|
case game.PhaseChooseTrump:
|
|
actor = r.game.Hakem
|
|
case game.PhasePlaying:
|
|
actor = r.game.Turn
|
|
default:
|
|
return
|
|
}
|
|
r.turnGen++
|
|
gen := r.turnGen
|
|
delay := r.turnTimeout
|
|
if r.seats[actor].auto() {
|
|
delay = r.botDelay
|
|
// تا پایانِ پنجرهی انیمیشنِ کلاینت (بُر زدن یا پخشِ کارت)، حرکتِ خودکار را
|
|
// عقب میاندازیم تا کارتها وسطِ انیمیشن روی میز نیفتند. نوبتِ انسان دستنخورده میماند.
|
|
if grace := time.Until(r.graceUntil); grace > delay {
|
|
delay = grace
|
|
}
|
|
}
|
|
r.timer = time.AfterFunc(delay, func() {
|
|
select {
|
|
case r.actions <- roomAction{kind: akTimeout, seat: actor, gen: gen}:
|
|
default:
|
|
}
|
|
})
|
|
}
|
|
|
|
// handleInput یک ورودی معتبر از بازیکن را اعمال میکند.
|
|
func (r *Room) handleInput(act roomAction) {
|
|
s := r.seats[act.seat]
|
|
if s.client == nil || s.client != act.client {
|
|
return // فرستنده دیگر مالک این جایگاه نیست (کهنه)
|
|
}
|
|
switch act.msg.Type {
|
|
case "leave":
|
|
r.handleLeave(act.seat)
|
|
case "chat":
|
|
r.handleChat(act.seat, act.msg.Text, act.msg.Emoji)
|
|
case "choose_trump":
|
|
suit, err := game.ParseSuit(act.msg.Suit)
|
|
if err != nil {
|
|
r.sendError(act.seat, "invalid suit")
|
|
return
|
|
}
|
|
if err := r.game.ChooseTrump(act.seat, suit); err != nil {
|
|
r.sendError(act.seat, err.Error())
|
|
return
|
|
}
|
|
r.seen = [4]uint16{} // شروعِ فازِ بازیِ هَند ⇒ ریستِ حافظهی کارتها
|
|
r.graceUntil = time.Now().Add(r.dealDelay) // انیمیشنِ پخشِ بقیهی کارتها
|
|
r.react()
|
|
case "reshuffle":
|
|
// بُرِ مجدد با مصرفِ یک بلیت: فقط حاکم و فقط پیش از انتخابِ حکم.
|
|
if r.game.Phase != game.PhaseChooseTrump || act.seat != r.game.Hakem {
|
|
r.sendError(act.seat, "بُر مجدد مجاز نیست")
|
|
return
|
|
}
|
|
if s.isBot || s.userID == 0 {
|
|
return
|
|
}
|
|
if err := r.settler.ChargeTicket(s.userID); err != nil {
|
|
r.sendError(act.seat, "بلیت کافی ندارید")
|
|
return
|
|
}
|
|
if err := r.game.Reshuffle(act.seat); err != nil {
|
|
r.sendError(act.seat, err.Error())
|
|
return
|
|
}
|
|
r.graceUntil = time.Now().Add(r.firstHandDelay) // انیمیشنِ بُر زدنِ مجدد
|
|
r.react()
|
|
case "play_card":
|
|
card, err := game.ParseCard(act.msg.Card)
|
|
if err != nil {
|
|
r.sendError(act.seat, "invalid card")
|
|
return
|
|
}
|
|
if err := r.playCard(act.seat, card); err != nil {
|
|
r.sendError(act.seat, err.Error())
|
|
return
|
|
}
|
|
r.react()
|
|
}
|
|
}
|
|
|
|
// autoMove حرکت خودکار برای جایگاه (بات/قطعشده/AFK) را انجام میدهد.
|
|
func (r *Room) autoMove(seat int) {
|
|
switch r.game.Phase {
|
|
case game.PhaseChooseTrump:
|
|
if seat != r.game.Hakem {
|
|
return
|
|
}
|
|
r.autoChooseTrump(seat)
|
|
r.react()
|
|
case game.PhasePlaying:
|
|
if seat != r.game.Turn {
|
|
return
|
|
}
|
|
r.autoPlayCard(seat)
|
|
r.react()
|
|
}
|
|
}
|
|
|
|
// autoChooseTrump حکم را با راهبردِ باتِ هوشمند (طول + قدرتِ خال) انتخاب میکند.
|
|
func (r *Room) autoChooseTrump(seat int) {
|
|
hand := r.game.Hand(seat)
|
|
if len(hand) == 0 {
|
|
return
|
|
}
|
|
_ = r.game.ChooseTrump(seat, botChooseTrump(hand))
|
|
r.seen = [4]uint16{} // شروعِ فازِ بازیِ هَند ⇒ ریستِ حافظهی کارتها
|
|
r.graceUntil = time.Now().Add(r.dealDelay) // انیمیشنِ پخشِ بقیهی کارتها
|
|
}
|
|
|
|
// autoPlayCard با هوشِ باتِ اکتشافی یک کارتِ مجاز و راهبردی بازی میکند.
|
|
func (r *Room) autoPlayCard(seat int) {
|
|
hand := r.game.Hand(seat)
|
|
if len(hand) == 0 {
|
|
return
|
|
}
|
|
choice := botPlay(hand, r.game.Trick, r.game.Trump, r.game.LeadSuit, r.seen)
|
|
_ = r.playCard(seat, choice)
|
|
}
|
|
|
|
// playCard کارت را بازی کرده و در صورت بُرِش با حکم، آن را برای آمار ثبت میکند.
|
|
func (r *Room) playCard(seat int, card game.Card) error {
|
|
wasFirst := len(r.game.Trick) == 0
|
|
if err := r.game.PlayCard(seat, card); err != nil {
|
|
return err
|
|
}
|
|
r.seen[card.Suit] |= 1 << (card.Rank - 2) // ثبت در حافظهی کارتشمارِ بات
|
|
// بُرِش: کارتِ غیرِاولِ دست، از خالِ حکم، در حالی که خالِ زمینه حکم نبوده
|
|
// ⇒ بازیکن خالِ زمینه را نداشته و با حکم بریده است.
|
|
if !wasFirst && card.Suit == r.game.Trump && r.game.LeadSuit != r.game.Trump {
|
|
if s := r.seats[seat]; !s.isBot && s.userID != 0 {
|
|
r.settler.RecordCut(s.userID)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// recordHandStats آمار هر بازیکنِ انسان را برای هَندِ پایانیافته ثبت میکند.
|
|
func (r *Room) recordHandStats(res *game.HandResult) {
|
|
if res == nil {
|
|
return
|
|
}
|
|
for seat, s := range r.seats {
|
|
if s.isBot || s.userID == 0 {
|
|
continue
|
|
}
|
|
won := game.Team(seat) == res.WinnerTeam
|
|
r.settler.RecordHand(s.userID, won, res.Kot, seat == res.Hakem)
|
|
}
|
|
}
|
|
|
|
// handleLeave خروج دائمی یک بازیکن؛ جایگاهش به بات تبدیل میشود.
|
|
func (r *Room) handleLeave(seat int) {
|
|
s := r.seats[seat]
|
|
s.isBot = true
|
|
s.connected = false
|
|
s.left = true
|
|
s.client = nil
|
|
r.broadcast(mustJSON(playerLeftMsg{Type: "player_left", Seat: seat}))
|
|
if r.connectedHumans() == 0 {
|
|
r.refundAll()
|
|
r.endRoom()
|
|
return
|
|
}
|
|
r.broadcastState()
|
|
r.armTimer()
|
|
}
|
|
|
|
// handleDisconnect قطع موقت یک بازیکن؛ جایگاه برای بازگشت باز میماند و خودکار بازی میشود.
|
|
func (r *Room) handleDisconnect(seat int, client *Client) {
|
|
s := r.seats[seat]
|
|
if s.client != client {
|
|
return // قبلاً با کلاینت جدید جایگزین شده (کهنه)
|
|
}
|
|
s.connected = false
|
|
s.client = nil
|
|
r.broadcast(mustJSON(playerStatusMsg{Type: "player_disconnected", Seat: seat}))
|
|
if r.connectedHumans() == 0 {
|
|
r.refundAll()
|
|
r.endRoom()
|
|
return
|
|
}
|
|
r.broadcastState()
|
|
r.armTimer()
|
|
}
|
|
|
|
// handleReconnect اتصال مجدد یک بازیکن به جایگاه قبلی.
|
|
func (r *Room) handleReconnect(seat int, client *Client) {
|
|
s := r.seats[seat]
|
|
s.client = client
|
|
s.connected = true
|
|
r.broadcast(mustJSON(playerStatusMsg{Type: "player_reconnected", Seat: seat}))
|
|
// وضعیت کامل را برای بازیکن بازگشته بفرست
|
|
client.trySend(mustJSON(r.matchedFor(seat)))
|
|
client.trySend(mustJSON(buildState(r.game, r.ID, seat, r.playersInfo(), int(r.turnTimeout.Milliseconds()))))
|
|
r.armTimer() // مهلت نوبت برای انسانِ بازگشته دوباره بلند میشود
|
|
}
|
|
|
|
// endRoom میز را بسته و فهرست انسانها را برای پاکسازی به هاب میدهد.
|
|
func (r *Room) endRoom() {
|
|
if r.ended {
|
|
return
|
|
}
|
|
r.ended = true
|
|
var ids []int64
|
|
var clients []*Client
|
|
for _, s := range r.seats {
|
|
if s.isBot {
|
|
continue
|
|
}
|
|
ids = append(ids, s.userID)
|
|
if s.client != nil {
|
|
clients = append(clients, s.client)
|
|
}
|
|
}
|
|
r.hub.endRoom <- endInfo{room: r, humanIDs: ids, clients: clients}
|
|
}
|
|
|
|
// settleGameOver جایزه را به برندگانِ انسان میدهد و بازی را ثبت میکند.
|
|
func (r *Room) settleGameOver(res *game.HandResult) {
|
|
// برندگانِ انسان (برای تقسیمِ potِ میزِ خصوصی).
|
|
var winners []int64
|
|
for seat, s := range r.seats {
|
|
if s.isBot || s.userID == 0 {
|
|
continue
|
|
}
|
|
won := game.Team(seat) == res.WinnerTeam
|
|
if won {
|
|
winners = append(winners, s.userID)
|
|
if !r.private {
|
|
r.settler.AwardWinner(s.userID, r.tier)
|
|
}
|
|
}
|
|
if !r.private {
|
|
r.settler.AwardRank(s.userID, r.tier, won) // امتیازِ رتبه (اگر میزِ رتبهبندی باشد)
|
|
}
|
|
r.settler.RecordGameResult(s.userID, won)
|
|
}
|
|
// تقسیمِ potِ شرطِ خصوصی بینِ برندگانِ انسان (باقیمانده به نفرِ اول).
|
|
if r.private && r.pot > 0 && len(winners) > 0 {
|
|
share := r.pot / int64(len(winners))
|
|
rem := r.pot - share*int64(len(winners))
|
|
for i, id := range winners {
|
|
amt := share
|
|
if i == 0 {
|
|
amt += rem
|
|
}
|
|
r.settler.AwardCoins(id, amt, r.ID)
|
|
}
|
|
r.pot = 0
|
|
}
|
|
r.settler.RecordGame(r.ID, r.playersJSON(res.WinnerTeam), res.WinnerTeam, res.Kot)
|
|
}
|
|
|
|
// refundAll ورودی را به بازیکنانِ انسان که داوطلبانه خارج نشدهاند بازمیگرداند.
|
|
func (r *Room) refundAll() {
|
|
if r.private {
|
|
// میزِ خصوصی: فقط شرطِ سکهایِ پرداختشده بازمیگردد (اگر شرطی بوده).
|
|
if r.stake > 0 {
|
|
for seat, s := range r.seats {
|
|
if s.isBot || s.left || s.userID == 0 || !r.paid[seat] {
|
|
continue
|
|
}
|
|
r.settler.AwardCoins(s.userID, r.stake, r.ID+":refund")
|
|
r.paid[seat] = false
|
|
}
|
|
r.pot = 0
|
|
}
|
|
return
|
|
}
|
|
for _, s := range r.seats {
|
|
if s.isBot || s.left || s.userID == 0 {
|
|
continue
|
|
}
|
|
r.settler.Refund(s.userID, r.tier)
|
|
}
|
|
}
|
|
|
|
// playersJSON خلاصهی بازیکنان را برای ثبت تاریخچه میسازد.
|
|
func (r *Room) playersJSON(winnerTeam int) string {
|
|
type rec struct {
|
|
Seat int `json:"seat"`
|
|
Name string `json:"name"`
|
|
Bot bool `json:"bot"`
|
|
Won bool `json:"won"`
|
|
}
|
|
out := make([]rec, 4)
|
|
for i, s := range r.seats {
|
|
out[i] = rec{Seat: i, Name: s.name, Bot: s.isBot, Won: game.Team(i) == winnerTeam}
|
|
}
|
|
return string(mustJSON(out))
|
|
}
|
|
|
|
func (r *Room) connectedHumans() int {
|
|
n := 0
|
|
for _, s := range r.seats {
|
|
if !s.isBot && s.connected && s.client != nil {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func (r *Room) playersInfo() []PlayerInfo {
|
|
out := make([]PlayerInfo, 4)
|
|
for i, s := range r.seats {
|
|
out[i] = PlayerInfo{
|
|
Seat: i, Name: s.name, Bot: s.isBot,
|
|
Connected: s.isBot || s.connected,
|
|
Coins: s.coins, RankTier: s.rankTier, Avatar: s.avatar, Frame: s.frame,
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// broadcastState نمای اختصاصی هر بازیکنِ انسانِ متصل را برای او ارسال میکند.
|
|
func (r *Room) broadcastState() {
|
|
players := r.playersInfo()
|
|
for seat, s := range r.seats {
|
|
if s.client != nil {
|
|
s.client.trySend(mustJSON(buildState(r.game, r.ID, seat, players, int(r.turnTimeout.Milliseconds()))))
|
|
}
|
|
}
|
|
}
|
|
|
|
// broadcast یک پیام یکسان را برای همه بازیکنانِ انسانِ متصل ارسال میکند.
|
|
func (r *Room) broadcast(b []byte) {
|
|
for _, s := range r.seats {
|
|
if s.client != nil {
|
|
s.client.trySend(b)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleChat یک پیام/شکلکِ بازیکن را با کنترلِ نرخ (ضدِ اسپم) و طولِ مجاز برای
|
|
// همهی میز پخش میکند تا کنارِ آواتارِ فرستنده نمایش داده شود.
|
|
func (r *Room) handleChat(seat int, text, emoji string) {
|
|
now := time.Now()
|
|
if now.Sub(r.chatAt[seat]) < 1200*time.Millisecond {
|
|
return // فاصلهی کمتر از ~۱.۲ ثانیه ⇒ نادیده (ضدِ اسپم)
|
|
}
|
|
clip := func(s string, n int) string {
|
|
r := []rune(s)
|
|
if len(r) > n {
|
|
return string(r[:n])
|
|
}
|
|
return s
|
|
}
|
|
text, emoji = clip(text, 60), clip(emoji, 16)
|
|
if text == "" && emoji == "" {
|
|
return
|
|
}
|
|
r.chatAt[seat] = now
|
|
r.broadcast(mustJSON(chatMsg{Type: "chat", Seat: seat, Text: text, Emoji: emoji}))
|
|
}
|
|
|
|
func (r *Room) sendError(seat int, msg string) {
|
|
if c := r.seats[seat].client; c != nil {
|
|
c.trySend(mustJSON(errorMsg{Type: "error", Message: msg}))
|
|
}
|
|
}
|