feat: add ratel-online server, core, client, and api sources

Import source files from ratel-online organization repos into monorepo
structure for gomoku game development. Update README with project
structure and credits for original authors.
This commit is contained in:
tiennm99 committed 2026-04-09 17:09:41 +07:00
1 parent ee7edaf727
commit 6e3670aec8
121 files changed
+11204 -1

No files matched your search

+501
View File
@@ -0,0 +1,501 @@
package database
import (
"sort"
"strconv"
stringx "strings"
"sync/atomic"
"time"
"github.com/awesome-cap/hashmap"
"github.com/ratel-online/core/log"
modelx "github.com/ratel-online/core/model"
"github.com/ratel-online/core/network"
"github.com/ratel-online/core/util/async"
"github.com/ratel-online/core/util/json"
"github.com/ratel-online/core/util/strings"
"github.com/ratel-online/server/consts"
)
var roomIds int64 = 0
var players = hashmap.New() // 存储连接过服务器的全部用户
var connPlayers = hashmap.New()
var rooms = hashmap.New()
var roomPlayers = hashmap.New()
var roomSpectators = hashmap.New()
var roomKickedPlayers = hashmap.New()
var roomPropsSetter = map[string]func(r *Room, v string){
consts.RoomPropsSkill: func(r *Room, v string) {
r.EnableSkill = v == "on"
r.EnableLandlord = !r.EnableSkill
},
consts.RoomPropsLaiZi: func(r *Room, v string) {
r.EnableLaiZi = v == "on"
},
consts.RoomPropsDotShuffle: func(r *Room, v string) {
r.EnableDontShuffle = v == "on"
},
consts.RoomPropsPassword: func(r *Room, v string) {
if v == "off" {
r.Password = ""
} else {
r.Password = v
}
},
consts.RoomPropsChat: func(r *Room, v string) {
r.EnableChat = v == "on"
},
consts.RoomPropsPlayerNum: func(r *Room, v string) {
n, _ := strconv.Atoi(v)
if n < 2 || n > 50 {
n = consts.MaxPlayers
}
r.MaxPlayers = n
},
consts.RoomPropsShowIP: func(r *Room, v string) {
r.EnableShowIP = v == "on"
},
consts.RoomPropsJokerAsTarget: func(r *Room, v string) {
r.EnableJokerAsTarget = v == "on"
},
}
func init() {
async.Async(func() {
loopCount := 0
for {
loopCount++
if loopCount%60 == 0 {
log.Infof("[database.init] Room cleanup loop count: %d (running for %d hours)\n", loopCount, loopCount/60)
}
time.Sleep(1 * time.Minute)
rooms.Foreach(func(e *hashmap.Entry) {
roomCancel(e.Value().(*Room))
})
}
})
}
func Connected(conn *network.Conn, info *modelx.AuthInfo) *Player {
player := &Player{
ID: conn.ID(),
IP: conn.IP(),
Name: strings.Desensitize(info.Name),
Amount: 2000,
}
player.Conn(conn) // 初始化play对象
players.Set(conn.ID(), player) // 写入用户池
connPlayers.Set(conn.ID(), player) // 写入连接用户池
return player
}
func CreateRoom(creator int64, t int) *Room {
room := &Room{
ID: atomic.AddInt64(&roomIds, 1),
Type: t,
State: consts.RoomStateWaiting,
Creator: creator,
ActiveTime: time.Now(),
MaxPlayers: consts.MaxPlayers,
EnableLandlord: true,
EnableChat: true,
EnableShowIP: false,
}
switch room.Type {
case consts.GameTypeLaiZi:
room.EnableLaiZi = true
case consts.GameTypeSkill:
room.EnableLaiZi = true
room.EnableDontShuffle = true
room.EnableSkill = true
room.EnableLandlord = false
case consts.GameTypeRunFast:
room.MaxPlayers = 3
room.EnableLaiZi = false
room.EnableLandlord = false
room.EnableDontShuffle = true
case consts.GameTypeTexas:
room.MaxPlayers = 10
case consts.GameTypeLiar:
room.MaxPlayers = 4
room.EnableJokerAsTarget = true
}
roomPlayers.Set(room.ID, map[int64]bool{})
roomSpectators.Set(room.ID, map[int64]int{})
rooms.Set(room.ID, room)
return room
}
func deleteRoom(room *Room) {
if room != nil {
rooms.Del(room.ID)
roomPlayers.Del(room.ID)
roomSpectators.Del(room.ID)
if room.Game != nil {
room.Game.Clean()
}
}
}
func GetRooms() []*Room {
list := make([]*Room, 0)
rooms.Foreach(func(e *hashmap.Entry) {
list = append(list, e.Value().(*Room))
})
sort.Slice(list, func(i, j int) bool {
return list[i].ID < list[j].ID
})
return list
}
func GetRoom(roomId int64) *Room {
return getRoom(roomId)
}
func getRoom(roomId int64) *Room {
if v, ok := rooms.Get(roomId); ok {
return v.(*Room)
}
return nil
}
func getPlayer(playerId int64) *Player {
if v, ok := players.Get(playerId); ok {
return v.(*Player)
}
return nil
}
func SetRoomProps(room *Room, k, v string) {
// 根据房间类型限制可设置的属性
allowedProps := getAllowedPropsByGameType(room.Type)
// 检查属性是否允许设置
if !allowedProps[k] {
return // 不允许的属性直接返回,不执行设置
}
if setter, ok := roomPropsSetter[k]; ok {
setter(room, v)
}
}
// 根据游戏类型返回允许设置的属性列表
func getAllowedPropsByGameType(gameType int) map[string]bool {
switch gameType {
case consts.GameTypeLiar:
// 对于骗子酒馆,只允许设置指示牌规则和显示IP
return map[string]bool{
consts.RoomPropsJokerAsTarget: true,
consts.RoomPropsShowIP: true,
consts.RoomPropsPassword: true,
}
case consts.GameTypeUno, consts.GameTypeMahjong:
// 对于Uno和麻将,允许设置玩家数量和显示IP
return map[string]bool{
consts.RoomPropsPlayerNum: true,
consts.RoomPropsShowIP: true,
consts.RoomPropsPassword: true,
}
case consts.GameTypeTexas:
// 对于德州扑克,允许设置玩家数量和显示IP
return map[string]bool{
consts.RoomPropsPlayerNum: true,
consts.RoomPropsShowIP: true,
consts.RoomPropsPassword: true,
}
default:
// 其他游戏类型允许所有常规属性
return map[string]bool{
consts.RoomPropsLaiZi: true,
consts.RoomPropsDotShuffle: true,
consts.RoomPropsSkill: true,
consts.RoomPropsPassword: true,
consts.RoomPropsPlayerNum: true,
consts.RoomPropsChat: true,
consts.RoomPropsShowIP: true,
}
}
}
func getRoomPlayers(roomId int64) map[int64]bool {
if v, ok := roomPlayers.Get(roomId); ok {
return v.(map[int64]bool)
}
return nil
}
func getRoomSpectators(roomId int64) map[int64]int {
if v, ok := roomSpectators.Get(roomId); ok {
return v.(map[int64]int)
}
return nil
}
func IsValidPlayer(roomId, playerId int64) bool {
room := getRoom(roomId)
if room != nil {
room.Lock()
defer room.Unlock()
playersIds := getRoomPlayers(roomId)
if playersIds != nil {
_, exists := playersIds[playerId]
if exists {
return true
}
}
spectatorsIds := getRoomSpectators(roomId)
if spectatorsIds != nil {
_, exists := spectatorsIds[playerId]
if exists {
return true
}
}
return false
}
return false
}
// 加入房间
func JoinRoom(roomId, playerId int64) error {
// 资源检查
player := getPlayer(playerId)
if player == nil {
return consts.ErrorsExist
}
room := getRoom(roomId)
if room == nil {
return consts.ErrorsRoomInvalid
}
// 加锁防止并发异常
room.Lock()
defer room.Unlock()
if hasKicked(roomId, playerId) {
return consts.ErrorsJoinFailForKicked
}
room.ActiveTime = time.Now()
//房间人数及状态检查
if room.Players >= room.MaxPlayers || room.State == consts.RoomStateRunning {
spectatorsIds := getRoomSpectators(roomId)
spectatorsIds[playerId] = len(spectatorsIds)
player.RoomID = roomId
player.Role = RoleSpectator
} else {
playersIds := getRoomPlayers(roomId)
playersIds[playerId] = true
room.Players++
player.RoomID = roomId
player.Role = RolePlayer
if room.Creator == playerId {
player.Role = RoleOwner
}
}
return nil
}
func LeaveRoom(roomId, playerId int64) {
room := getRoom(roomId)
if room != nil {
room.Lock()
defer room.Unlock()
leaveRoom(room, getPlayer(playerId))
}
}
func Backfill(roomId int64) *Player {
room := getRoom(roomId)
if room != nil {
room.Lock()
defer room.Unlock()
return backfill(room)
}
return nil
}
func backfill(room *Room) *Player {
if room.Players >= room.MaxPlayers {
return nil
}
spectatorsIds := getRoomSpectators(room.ID)
if len(spectatorsIds) == 0 {
return nil
}
spectators := make([]struct {
id int64
index int
}, 0)
for id, index := range spectatorsIds {
spectators = append(spectators, struct {
id int64
index int
}{id: id, index: index})
}
sort.Slice(spectators, func(i, j int) bool {
return spectators[i].index < spectators[j].index
})
playerId := spectators[0].id
delete(spectatorsIds, playerId)
playersIds := getRoomPlayers(room.ID)
playersIds[playerId] = true
room.Players++
player := getPlayer(playerId)
if player != nil {
player.Role = RolePlayer
}
return player
}
func Kicking(roomId, playerId int64) {
room := getRoom(roomId)
if room != nil {
room.Lock()
defer room.Unlock()
leaveRoom(room, getPlayer(playerId))
kickedPlayers, ok := roomKickedPlayers.Get(roomId)
if !ok {
kickedPlayers = map[int64]bool{}
roomKickedPlayers.Set(roomId, kickedPlayers)
}
kickedPlayers.(map[int64]bool)[playerId] = true
}
}
func hasKicked(roomId, playerId int64) bool {
kickedPlayers, ok := roomKickedPlayers.Get(roomId)
if !ok {
return false
}
_, exists := kickedPlayers.(map[int64]bool)[playerId]
return exists
}
func leaveRoom(room *Room, player *Player) {
if room == nil || player == nil {
return
}
room.ActiveTime = time.Now()
playersIds := getRoomPlayers(room.ID)
if _, ok := playersIds[player.ID]; ok {
room.Players--
player.RoomID = 0
player.Role = ""
delete(playersIds, player.ID)
if len(playersIds) > 0 && room.Creator == player.ID {
for k := range playersIds {
room.Creator = k
if p := getPlayer(k); p != nil {
p.Role = RoleOwner
}
break
}
}
}
spectatorsIds := getRoomSpectators(room.ID)
if _, ok := spectatorsIds[player.ID]; ok {
player.RoomID = 0
player.Role = ""
delete(spectatorsIds, player.ID)
}
if len(playersIds) == 0 && len(spectatorsIds) == 0 {
deleteRoom(room)
}
}
func roomCancel(room *Room) {
if room.ActiveTime.Add(24 * time.Hour).Before(time.Now()) {
log.Infof("room %d is timeout 24 hours, removed.\n", room.ID)
deleteRoom(room)
return
}
living := false
playerIds := getRoomPlayers(room.ID)
for id := range playerIds {
if getPlayer(id).online {
living = true
break
}
}
spectatorIds := getRoomSpectators(room.ID)
for id := range spectatorIds {
if getPlayer(id).online {
living = true
break
}
}
if !living {
log.Infof("room %d is not living, removed.\n", room.ID)
deleteRoom(room)
}
}
func RoomPlayers(roomId int64) map[int64]bool {
return getRoomPlayers(roomId)
}
func RoomSpectators(roomId int64) map[int64]int {
return getRoomSpectators(roomId)
}
func broadcast(room *Room, msg string, exclude ...int64) {
room.ActiveTime = time.Now()
excludeSet := map[int64]bool{}
for _, exc := range exclude {
excludeSet[exc] = true
}
for playerId := range getRoomPlayers(room.ID) {
if player := getPlayer(playerId); player != nil && !excludeSet[playerId] {
_ = player.WriteString(">> " + msg)
}
}
for playerId := range getRoomSpectators(room.ID) {
if player := getPlayer(playerId); player != nil && !excludeSet[playerId] {
_ = player.WriteString(">> " + msg)
}
}
}
func Broadcast(roomId int64, msg string, exclude ...int64) {
room := getRoom(roomId)
if room == nil {
return
}
broadcast(room, msg, exclude...)
}
func BroadcastChat(player *Player, msg string, exclude ...int64) {
log.Infof("chat msg, player %s[%d] %s say: %s\n", player.Name, player.ID, player.IP, stringx.TrimSpace(msg))
Broadcast(player.RoomID, strings.Desensitize(msg), exclude...)
}
func BroadcastObject(roomId int64, object interface{}, exclude ...int64) {
room := getRoom(roomId)
if room == nil {
return
}
room.Lock()
defer room.Unlock()
excludeSet := map[int64]bool{}
for _, exc := range exclude {
excludeSet[exc] = true
}
msg := json.Marshal(object)
playerIds := getRoomPlayers(roomId)
for playerId := range playerIds {
if player := getPlayer(playerId); player != nil && !excludeSet[playerId] {
_ = player.WriteString(string(msg))
}
}
}
func GetPlayer(playerId int64) *Player {
return getPlayer(playerId)
}
+27
View File
@@ -0,0 +1,27 @@
package database
import (
"sync"
"github.com/ratel-online/core/model"
)
type Liar struct {
sync.Mutex
Room *Room `json:"room"`
PlayerIDs []int64 `json:"playerIds"`
Bullets map[int64]int `json:"bullets"`
Bong map[int64]int `json:"bong"`
Pokers model.Pokers `json:"pokers"`
States map[int64]chan int `json:"states"`
Hands map[int64]model.Pokers `json:"hands"`
Target *model.Poker `json:"target"`
Alive map[int64]bool `json:"alive"`
LastPlayerID int64 `json:"lastPlayerId"`
LastPokers model.Pokers `json:"lastPokers"`
Supervisors map[int64]bool `json:"supervisors"`
AllowJokers bool `json:"allowJokers"`
}
func (l *Liar) Clean() {
}
+209
View File
@@ -0,0 +1,209 @@
package database
import (
"bytes"
"fmt"
"sort"
"strconv"
"strings"
"github.com/feel-easy/mahjong/card"
"github.com/feel-easy/mahjong/consts"
"github.com/feel-easy/mahjong/event"
"github.com/feel-easy/mahjong/game"
"github.com/feel-easy/mahjong/tile"
"github.com/ratel-online/core/log"
rconsts "github.com/ratel-online/server/consts"
)
type Mahjong struct {
Room *Room `json:"room"`
PlayerIDs []int `json:"playerIds"`
States map[int]chan int `json:"states"`
Game *game.Game `json:"game"`
}
func (game *Mahjong) Clean() {
if game != nil {
for _, state := range game.States {
close(state)
}
}
}
type OP struct {
operation int
tiles []int
}
func circled(n int) string {
if n >= 1 && n <= 20 {
return string(rune(0x2460 + n - 1))
}
return strconv.Itoa(n)
}
type MahjongPlayer struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
func NewPlayer(user *Player) *MahjongPlayer {
return &MahjongPlayer{
ID: user.ID,
Name: user.Name,
}
}
func (p *MahjongPlayer) PlayerID() int {
return int(p.ID)
}
func (p *MahjongPlayer) NickName() string {
return p.Name
}
func (mp *MahjongPlayer) OnPlayTile(payload event.PlayTilePayload) {
p := GetPlayer(mp.ID)
p.WriteString(fmt.Sprintf("You play %s ! \n", tile.Tile(payload.Tile)))
Broadcast(p.RoomID, fmt.Sprintf("%s PlayTile %s !\n", payload.PlayerName, tile.Tile(payload.Tile)), p.ID)
}
func (mp *MahjongPlayer) Take(tiles []int, gameState game.State) (int, []int, error) {
p := GetPlayer(mp.ID)
Broadcast(p.RoomID, fmt.Sprintf("It's %s take mahjong! \n", p.Name), p.ID)
buf := bytes.Buffer{}
buf.WriteString(fmt.Sprintf("It's your take mahjong, %s! \n", p.Name))
buf.WriteString(gameState.String())
p.WriteString(buf.String())
askBuf := bytes.Buffer{}
tileOptions := make(map[string]*OP)
labelCounter := 1
if pvs, ok := gameState.SpecialPrivileges[int(p.ID)]; ok {
for _, pv := range pvs {
switch pv {
case consts.GANG:
askBuf.WriteString("You can 杠!!!\n")
label := strconv.Itoa(labelCounter)
ts := []int{gameState.LastPlayedTile, gameState.LastPlayedTile, gameState.LastPlayedTile}
tileOptions[label] = &OP{
operation: consts.GANG,
tiles: append(ts, gameState.LastPlayedTile),
}
askBuf.WriteString(fmt.Sprintf("%s. %s \n", circled(labelCounter), tile.ToTileString(ts)))
labelCounter++
case consts.PENG:
askBuf.WriteString("You can 碰!!!\n")
label := strconv.Itoa(labelCounter)
ts := []int{gameState.LastPlayedTile, gameState.LastPlayedTile}
tileOptions[label] = &OP{
operation: consts.PENG,
tiles: append(ts, gameState.LastPlayedTile),
}
askBuf.WriteString(fmt.Sprintf("%s. %s \n", circled(labelCounter), tile.ToTileString(ts)))
labelCounter++
case consts.CHI:
askBuf.WriteString("You can 吃!!!\n")
for _, ts := range card.CanChiTiles(tiles, gameState.LastPlayedTile) {
label := strconv.Itoa(labelCounter)
tileOptions[label] = &OP{
operation: consts.CHI,
tiles: append(ts, gameState.LastPlayedTile),
}
askBuf.WriteString(fmt.Sprintf("%s. %s \n", circled(labelCounter), tile.ToTileString(ts)))
labelCounter++
}
}
}
}
label := strconv.Itoa(labelCounter)
askBuf.WriteString(fmt.Sprintf("%s. %s \n", circled(labelCounter), "no"))
tileOptions[label] = &OP{
operation: 0,
tiles: []int{},
}
loopCount := 0
for {
loopCount++
if loopCount%100 == 0 {
log.Infof("[MahjongPlayer.Take] Player %d loop count: %d\n", mp.ID, loopCount)
}
p = getPlayer(p.ID)
p.WriteString(askBuf.String())
selectedLabel, err := p.AskForString(consts.PlayMahjongTimeout)
if err != nil {
switch err {
case rconsts.ErrorsExist:
p.WriteString("Don't quit a good game!\n")
selectedLabel = "E"
case rconsts.ErrorsTimeout:
// Default to "no" action (skip peng/chi/gang) on timeout
selectedLabel = label
default:
return 0, nil, err
}
}
selected, found := tileOptions[strings.ToUpper(selectedLabel)]
if !found {
BroadcastChat(p, fmt.Sprintf("%s say: %s\n", p.Name, selectedLabel))
continue
}
return selected.operation, selected.tiles, nil
}
}
func (mp *MahjongPlayer) Play(tiles []int, gameState game.State) (int, error) {
p := GetPlayer(mp.ID)
Broadcast(p.RoomID, fmt.Sprintf("It's %s turn! \n", p.Name), p.ID)
buf := bytes.Buffer{}
buf.WriteString(fmt.Sprintf("It's your turn, %s! \n", p.Name))
buf.WriteString(gameState.String())
p.WriteString(buf.String())
askBuf := bytes.Buffer{}
askBuf.WriteString("Select a tile to play:\n")
tileOptions := make(map[string]int)
sort.Ints(tiles)
for idx, i := range tiles {
label := strconv.Itoa(idx + 1)
tileOptions[label] = i
askBuf.WriteString(fmt.Sprintf("%s. %-6s", circled(idx+1), tile.Tile(i).String()))
if (idx+1)%6 == 0 {
askBuf.WriteString("\n")
} else {
askBuf.WriteString(" ")
}
}
askBuf.WriteString("\n")
loopCount := 0
for {
loopCount++
if loopCount%100 == 0 {
log.Infof("[MahjongPlayer.Play] Player %d loop count: %d\n", mp.ID, loopCount)
}
p = GetPlayer(p.ID)
p.WriteString(askBuf.String())
selectedLabel, err := p.AskForString(rconsts.PlayMahjongTimeout)
if err != nil {
switch err {
case rconsts.ErrorsExist:
p.WriteString("Don't quit a good game!\n")
selectedLabel = "E"
case rconsts.ErrorsTimeout:
// Default to the last tile (least likely to be useful) on timeout
selectedLabel = strconv.Itoa(len(tiles))
default:
return 0, err
}
}
selectedCard, found := tileOptions[strings.ToUpper(selectedLabel)]
if !found {
BroadcastChat(p, fmt.Sprintf("%s say: %s\n", p.Name, selectedLabel))
continue
}
mp.OnPlayTile(event.PlayTilePayload{
PlayerName: p.Name,
Tile: selectedCard,
})
return selectedCard, nil
}
}
+333
View File
@@ -0,0 +1,333 @@
package database
import (
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/ratel-online/core/log"
"github.com/ratel-online/core/model"
"github.com/ratel-online/core/network"
"github.com/ratel-online/core/protocol"
"github.com/ratel-online/core/util/arrays"
"github.com/ratel-online/core/util/json"
"github.com/ratel-online/core/util/poker"
"github.com/ratel-online/server/consts"
)
const initialRune = 'A'
type runeSequence struct {
currentRune rune
}
func (s *runeSequence) next() rune {
if s.currentRune == 0 {
s.currentRune = initialRune
}
currentRune := s.currentRune
s.currentRune++
return currentRune
}
type Role string
const (
RolePlayer Role = "player"
RoleOwner Role = "owner"
RoleSpectator Role = "spectator"
)
type Player struct {
ID int64 `json:"id"`
IP string `json:"ip"`
Name string `json:"name"`
Mode int `json:"mode"`
Type int `json:"type"`
Amount uint `json:"amount"`
RoomID int64 `json:"roomId"`
Role Role `json:"role"`
conn *network.Conn
data chan *protocol.Packet
read bool
state consts.StateID
online bool
}
func (p *Player) Write(bytes []byte) error {
return p.conn.Write(protocol.Packet{
Body: bytes,
})
}
func (p *Player) IsOnline() bool {
return p.online
}
func (p *Player) Offline() {
p.online = false
_ = p.conn.Close()
close(p.data)
room := getRoom(p.RoomID)
if room != nil {
room.Lock()
defer room.Unlock()
broadcast(room, fmt.Sprintf("%s lost connection! \n", p.Name))
if room.State == consts.RoomStateWaiting {
leaveRoom(room, p)
}
roomCancel(room)
}
}
func (p *Player) Listening() error {
loopCount := 0
for {
loopCount++
if loopCount%1000 == 0 {
log.Infof("[Player.Listening] Player %d loop count: %d, online: %v\n", p.ID, loopCount, p.online)
}
pack, err := p.conn.Read()
if err != nil {
log.Error(err)
return err
}
if p.read {
p.data <- pack
}
}
}
// 向客户端发生消息
func (p *Player) WriteString(data string) error {
time.Sleep(30 * time.Millisecond)
return p.conn.Write(protocol.Packet{
Body: []byte(data),
})
}
func (p *Player) WriteObject(data interface{}) error {
return p.conn.Write(protocol.Packet{
Body: json.Marshal(data),
})
}
func (p *Player) WriteError(err error) error {
if err == consts.ErrorsExist {
return err
}
return p.conn.Write(protocol.Packet{
Body: []byte(err.Error() + "\n"),
})
}
func (p *Player) AskForPacket(timeout ...time.Duration) (*protocol.Packet, error) {
p.StartTransaction()
defer p.StopTransaction()
return p.askForPacket(timeout...)
}
func (p *Player) askForPacket(timeout ...time.Duration) (*protocol.Packet, error) {
var packet *protocol.Packet
if len(timeout) > 0 {
select {
case packet = <-p.data:
case <-time.After(timeout[0]):
return nil, consts.ErrorsTimeout
}
} else {
packet = <-p.data
}
if packet == nil {
return nil, consts.ErrorsChanClosed
}
single := strings.ToLower(packet.String())
if single == "exit" || single == "e" {
return nil, consts.ErrorsExist
}
return packet, nil
}
func (p *Player) AskForInt(timeout ...time.Duration) (int, error) {
packet, err := p.AskForPacket(timeout...)
if err != nil {
return 0, err
}
return packet.Int()
}
func (p *Player) AskForInt64(timeout ...time.Duration) (int64, error) {
packet, err := p.AskForPacket(timeout...)
if err != nil {
return 0, err
}
return packet.Int64()
}
func (p *Player) AskForString(timeout ...time.Duration) (string, error) {
packet, err := p.AskForPacket(timeout...)
if err != nil {
return "", err
}
return packet.String(), nil
}
func (p *Player) AskForStringWithoutTransaction(timeout ...time.Duration) (string, error) {
packet, err := p.askForPacket(timeout...)
if err != nil {
return "", err
}
return packet.String(), nil
}
func (p *Player) StartTransaction() {
p.read = true
_ = p.WriteString(consts.IsStart)
}
func (p *Player) StopTransaction() {
p.read = false
_ = p.WriteString(consts.IsStop)
}
func (p *Player) State(s consts.StateID) {
p.state = s
}
func (p *Player) GetState() consts.StateID {
return p.state
}
func (p *Player) Conn(conn *network.Conn) {
p.conn = conn
p.data = make(chan *protocol.Packet, 8)
p.online = true
}
func (p Player) Model() model.Player {
modelPlayer := model.Player{
ID: p.ID,
Name: p.Name,
}
room := getRoom(p.RoomID)
if room != nil && room.Game != nil {
game := room.Game.(*Game)
modelPlayer.Pokers = len(game.Pokers[p.ID])
modelPlayer.Group = game.Groups[p.ID]
}
return modelPlayer
}
func (p Player) String() string {
return fmt.Sprintf("%s[%d]", p.Name, p.ID)
}
type RoomGame interface {
Clean()
}
type Room struct {
sync.Mutex
ID int64 `json:"id"`
Type int `json:"type"`
Game RoomGame `json:"gameId"`
State int `json:"state"`
Players int `json:"players"`
Banker int `json:"banker"`
Robots int `json:"robots"`
Creator int64 `json:"creator"`
ActiveTime time.Time `json:"activeTime"`
MaxPlayers int `json:"maxPlayers"`
Password string `json:"password"`
EnableChat bool `json:"enableChat"`
EnableLaiZi bool `json:"enableLaiZi"`
EnableSkill bool `json:"enableSkill"`
EnableLandlord bool `json:"enableLandlord"`
EnableDontShuffle bool `json:"enableDontShuffle"`
EnableShowIP bool `json:"enableShowIP"`
EnableJokerAsTarget bool `json:"enableJokerAsTarget"`
}
func (r *Room) Model() model.Room {
return model.Room{
ID: r.ID,
Type: r.Type,
TypeDesc: consts.GameTypes[r.Type],
Players: r.Players,
State: r.State,
StateDesc: consts.RoomStates[r.State],
Creator: r.Creator,
}
}
type Game struct {
Room *Room `json:"room"`
Players []int64 `json:"players"`
Groups map[int64]int `json:"groups"`
States map[int64]chan int `json:"states"`
Pokers map[int64]model.Pokers `json:"pokers"`
Universals []int `json:"universals"`
Decks int `json:"decks"`
Additional model.Pokers `json:"pocket"`
Multiple int `json:"multiple"`
FirstPlayer int64 `json:"firstPlayer"`
LastPlayer int64 `json:"lastPlayer"`
Robs []int64 `json:"robs"`
FirstRob int64 `json:"firstRob"`
LastRob int64 `json:"lastRob"`
FinalRob bool `json:"finalRob"`
LastFaces *model.Faces `json:"lastFaces"`
LastPokers model.Pokers `json:"lastPokers"`
Mnemonic map[int]int `json:"mnemonic"`
Skills map[int64]int `json:"skills"`
PlayTimes map[int64]int `json:"playTimes"`
PlayTimeOut map[int64]time.Duration `json:"playTimeOut"`
Rules poker.Rules `json:"rules"`
Discards model.Pokers `json:"discards"`
}
func (game *Game) Clean() {
if game != nil {
for _, state := range game.States {
close(state)
}
}
}
func (game *Game) Start() {
}
func (g Game) NextPlayer(curr int64) int64 {
idx := arrays.IndexOf(g.Players, curr)
return g.Players[(idx+1)%len(g.Players)]
}
func (g Game) PrevPlayer(curr int64) int64 {
idx := arrays.IndexOf(g.Players, curr)
return g.Players[(idx+len(g.Players))%len(g.Players)]
}
func (g Game) IsTeammate(player1, player2 int64) bool {
return g.Groups[player1] == g.Groups[player2]
}
func (g Game) IsLandlord(playerId int64) bool {
return g.Groups[playerId] == 1
}
func (g Game) Team(playerId int64) string {
if !g.Room.EnableLandlord {
return "team" + strconv.Itoa(g.Groups[playerId])
} else {
if !g.IsLandlord(playerId) {
return "peasant"
} else {
return "landlord"
}
}
}
+134
View File
@@ -0,0 +1,134 @@
package database
import (
"github.com/ratel-online/core/model"
)
type Texas struct {
Room *Room `json:"room"`
Players []*TexasPlayer `json:"players"`
Pot uint `json:"pot"`
BB int `json:"bb"`
SB int `json:"sb"`
Pool model.Pokers `json:"pool"`
Board model.Pokers `json:"board"`
MaxBetAmount uint `json:"maxBetAmount"`
MaxBetPlayer *TexasPlayer `json:"maxBetPlayer"`
Round string `json:"round"`
Folded int `json:"folded"`
AllIn int `json:"allIn"`
}
func (g *Texas) Clean() {
if g != nil {
for _, p := range g.Players {
close(p.State)
}
}
}
func (g *Texas) NextPlayer(id int64) *TexasPlayer {
if g == nil || len(g.Players) == 0 {
return nil
}
idx := -1
for i, a := range g.Players {
if a.ID == id {
idx = i
break
}
}
if idx == -1 {
return g.Players[0]
}
return g.Players[(idx+1)%len(g.Players)]
}
func (g *Texas) Player(id int64) *TexasPlayer {
for _, p := range g.Players {
if p.ID == id {
return p
}
}
return nil
}
func (g *Texas) SBPlayer() *TexasPlayer {
return g.Players[g.SB]
}
func (g *Texas) BBPlayer() *TexasPlayer {
return g.Players[g.BB]
}
func (g *Texas) Bet(player *TexasPlayer, amount uint) {
if amount > 0 {
player.Bet(amount)
g.Pot += amount
if player.Amount() == 0 {
player.AllIn = true
g.AllIn++
}
}
if g.MaxBetPlayer == nil {
g.MaxBetPlayer = player
}
if player.Bets > g.MaxBetAmount {
g.MaxBetAmount = player.Bets
g.MaxBetPlayer = player
}
}
func (g *Texas) RoundEnd(currentPlayerId int64) bool {
if g.AllIn == len(g.Players) ||
g.AllIn+g.Folded == len(g.Players) ||
(g.MaxBetPlayer != nil && g.MaxBetPlayer.ID == currentPlayerId) {
return true
}
// all players except one folded or allin
if g.AllIn+g.Folded == len(g.Players)-1 {
isEnd := true
for _, p := range g.Players {
if p.Folded {
continue
}
if p.Bets != g.MaxBetAmount {
isEnd = false
break
}
}
return isEnd
}
return false
}
type TexasPlayer struct {
ID int64 `json:"id"`
Name string `json:"name"`
State chan int `json:"state"`
Hand model.Pokers `json:"hand"`
Bets uint `json:"bets"`
Folded bool `json:"folded"`
AllIn bool `json:"allIn"`
}
func (p *TexasPlayer) Reset() {
p.Bets = 0
p.Folded = false
p.AllIn = false
p.Hand = nil
p.State = make(chan int, 1)
}
func (p *TexasPlayer) Amount() uint {
return GetPlayer(p.ID).Amount
}
func (p *TexasPlayer) Bet(amount uint) {
p.Bets += amount
GetPlayer(p.ID).Amount -= amount
}
func (p *TexasPlayer) Add(amount uint) {
GetPlayer(p.ID).Amount += amount
}
+184
View File
@@ -0,0 +1,184 @@
package database
import (
"bytes"
"fmt"
"strings"
"github.com/feel-easy/uno/card"
"github.com/feel-easy/uno/card/color"
"github.com/feel-easy/uno/event"
"github.com/feel-easy/uno/game"
"github.com/ratel-online/core/log"
"github.com/ratel-online/server/consts"
)
type UnoGame struct {
Room *Room `json:"room"`
Players []int `json:"players"`
States map[int]chan int `json:"states"`
Game *game.Game `json:"game"`
}
func (ug *UnoGame) HavePlay(player *Player) bool {
for _, id := range ug.Players {
if id == int(player.ID) && player.online {
return true
}
}
return false
}
func (un *UnoGame) NeedExit() bool {
return un.Room.Players <= 1
}
func (un *UnoGame) Clean() {
if un != nil {
for _, state := range un.States {
close(state)
}
}
}
type UnoPlayer struct {
ID int `json:"id"`
Name string `json:"name"`
}
func NewUnoPlayer(p *Player) game.Player {
return &UnoPlayer{
ID: int(p.ID),
Name: p.Name,
}
}
func (up *UnoPlayer) PlayerID() int {
return up.ID
}
func (up *UnoPlayer) NickName() string {
return up.Name
}
func contains(cards []card.Card, searchedCard card.Card) bool {
for _, card := range cards {
if card.Equal(searchedCard) {
return true
}
}
return false
}
func (up *UnoPlayer) NotifyCardsDrawn(cards []card.Card) {
p := getPlayer(int64(up.ID))
getPlayer(p.ID).WriteString(fmt.Sprintf("You drew %s!\n", cards))
}
func (up *UnoPlayer) NotifyNoMatchingCardsInHand(lastPlayedCard card.Card, hand []card.Card) {
p := getPlayer(int64(up.ID))
buf := bytes.Buffer{}
buf.WriteString(fmt.Sprintf("%s, none of your cards match %s! \n", p.Name, lastPlayedCard))
buf.WriteString(fmt.Sprintf("Your hand is %s \n", hand))
getPlayer(p.ID).WriteString(buf.String())
}
func (up *UnoPlayer) OnFirstCardPlayed(payload event.FirstCardPlayedPayload) {
p := getPlayer(int64(up.ID))
Broadcast(p.RoomID, fmt.Sprintf("First card is %s\n", payload.Card))
}
func (up *UnoPlayer) OnCardPlayed(payload event.CardPlayedPayload) {
p := getPlayer(int64(up.ID))
Broadcast(p.RoomID, fmt.Sprintf("%s played %s!\n", payload.PlayerName, payload.Card))
}
func (up *UnoPlayer) OnColorPicked(payload event.ColorPickedPayload) {
p := getPlayer(int64(up.ID))
Broadcast(p.RoomID, fmt.Sprintf("%s picked color %s!\n", payload.PlayerName, payload.Color))
}
func (up *UnoPlayer) OnPlayerPassed(payload event.PlayerPassedPayload) {
p := getPlayer(int64(up.ID))
Broadcast(p.RoomID, fmt.Sprintf("%s passed!\n", payload.PlayerName))
}
func (up *UnoPlayer) PickColor(gameState game.State) color.Color {
p := getPlayer(int64(up.ID))
loopCount := 0
for {
loopCount++
if loopCount%100 == 0 {
log.Infof("[UnoPlayer.PickColor] Player %d loop count: %d\n", up.ID, loopCount)
}
p = getPlayer(p.ID)
p.WriteString(fmt.Sprintf(
"Select a color: %s, %s, %s or %s ? \n",
color.Red,
color.Yellow,
color.Green,
color.Blue,
))
colorName, err := p.AskForString(consts.PlayTimeout)
if err != nil {
if err == consts.ErrorsTimeout {
return color.Red
}
p.WriteString(fmt.Sprintf("Unknown color '%s' \n", colorName))
continue
}
chosenColor, err := color.ByName(strings.ToLower(colorName))
if err != nil {
p.WriteString(fmt.Sprintf("Unknown color '%s' \n", colorName))
continue
}
return chosenColor
}
}
func (up *UnoPlayer) Play(playableCards []card.Card, gameState game.State) (card.Card, error) {
p := getPlayer(int64(up.ID))
Broadcast(p.RoomID, fmt.Sprintf("It's %s turn! \n", p.Name), p.ID)
buf := bytes.Buffer{}
buf.WriteString(fmt.Sprintf("It's your turn, %s! \n", p.Name))
buf.WriteString(gameState.String())
p.WriteString(buf.String())
runeSequence := runeSequence{}
cardOptions := make(map[string]card.Card)
for _, card := range playableCards {
label := string(runeSequence.next())
cardOptions[label] = card
}
cardSelectionLines := []string{"Select a card to play:"}
for label, card := range cardOptions {
cardSelectionLines = append(cardSelectionLines, fmt.Sprintf("%s %s", label, card))
}
cardSelectionMessage := strings.Join(cardSelectionLines, " \n ") + " \n "
loopCount := 0
for {
loopCount++
if loopCount%100 == 0 {
log.Infof("[UnoPlayer.Play] Player %d loop count: %d\n", up.ID, loopCount)
}
p = getPlayer(p.ID)
p.WriteString(cardSelectionMessage)
selectedLabel, err := p.AskForString(consts.PlayTimeout)
if err != nil {
if err == consts.ErrorsTimeout {
selectedLabel = "A"
} else {
return nil, err
}
}
selectedCard, found := cardOptions[strings.ToUpper(selectedLabel)]
if !found {
BroadcastChat(p, fmt.Sprintf("%s say: %s\n", p.Name, selectedLabel))
continue
}
if !contains(playableCards, selectedCard) {
p.WriteString(fmt.Sprintf("Cheat detected! Card %s is not in %s's hand! \n", selectedCard, p.Name))
continue
}
return selectedCard, nil
}
}