├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── cache ├── item.go ├── login.go ├── mail.go ├── main.go ├── manage.go ├── models │ └── game.go ├── ticker.go └── user.go ├── config_bak.yaml ├── docker-compose.yaml ├── games ├── demo │ └── fingerguessing │ │ ├── handle.go │ │ ├── player.go │ │ ├── room.go │ │ └── world.go ├── main.go └── migrate │ ├── dice │ ├── handle.go │ ├── player.go │ ├── room.go │ └── world.go │ ├── doudizhu │ ├── handle.go │ ├── player.go │ ├── room.go │ └── world.go │ ├── internal │ ├── cardcontrol │ │ ├── prizepool.go │ │ ├── rank.go │ │ └── utils.go │ └── cardrule │ │ ├── doudizhu.go │ │ ├── doudizhu_test.go │ │ ├── mahjong.go │ │ ├── mahjong_test.go │ │ ├── niuniu.go │ │ ├── paodekuai.go │ │ ├── paohuzi.go │ │ ├── sangong.go │ │ ├── texas.go │ │ └── zhajinhua.go │ ├── lottery │ ├── README │ ├── bairenniuniu.go │ ├── bairenzhajinhua.go │ ├── erbagang.go │ ├── handle.go │ ├── player.go │ └── room.go │ ├── mahjong │ ├── chaoshan.go │ ├── ermj.go.bak │ ├── guaisanjiao.go │ ├── guangdong.go │ ├── handle.go │ ├── hongzhong.go │ ├── hongzhonglaizigang.go.bak │ ├── huanghuang.go │ ├── hunan.go │ ├── mahjong.go │ ├── neimenggu.go │ ├── player.go │ ├── room.go │ ├── shanxituidaohu.go │ ├── sichuan.go │ ├── zhengzhou.go │ └── zhuanzhuan.go │ ├── niuniu │ ├── handle.go │ ├── player.go │ ├── room.go │ └── world.go │ ├── paodekuai │ ├── handle.go │ ├── player.go │ ├── room.go │ └── world.go │ ├── sangong │ ├── README │ ├── handle.go │ ├── player.go │ ├── room.go │ └── world.go │ ├── texas │ ├── README │ ├── handle.go │ ├── player.go │ ├── room.go │ ├── tournament.go │ └── world.go │ ├── xiaojiu │ ├── README │ ├── handle.go │ ├── player.go │ ├── room.go │ └── world.go │ └── zhajinhua │ ├── handle.go │ ├── player.go │ ├── room.go │ └── world.go ├── go.mod ├── go.sum ├── hall ├── gm.go ├── handle.go ├── mail.go ├── main.go ├── player.go ├── tool.go └── world.go ├── init.sql ├── internal ├── cardutils │ ├── card.go │ └── card_test.go ├── dbo │ └── pool.go ├── env │ └── env.go ├── errcode │ └── errcode.go ├── gameutils │ ├── item.go │ ├── jwt.go │ ├── time.go │ ├── time_test.go │ ├── util.go │ └── util_test.go ├── internal.go ├── pb │ ├── login.proto │ ├── service.proto │ └── userdata.proto └── rpc │ ├── cache.go │ └── call.go ├── login ├── handle.go ├── main.go └── user.go ├── service ├── action.go ├── bag.go ├── data.go ├── enter.go ├── handle.go ├── player.go ├── roomutils │ ├── room.go │ ├── room1.go │ └── room2.go ├── script.go ├── service.go ├── system │ ├── daily_signin.go │ ├── login.go │ ├── shop.go │ └── tool.go ├── tool.go └── world.go └── tests ├── config.yaml ├── package-lock.json ├── scripts └── common.lua ├── tables ├── fuck.tbl ├── item.tbl ├── robot.tbl ├── room.tbl └── signin.tbl ├── temp ├── assets │ ├── bootstrap.min.css │ ├── game.js │ └── md5.min.js ├── client.html ├── fingerguessing.html └── fingerguessingx4.html └── web ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public └── vite.svg ├── src ├── App.tsx ├── assets │ └── react.svg ├── fingerguessing.tsx ├── index.css ├── main.tsx ├── utils │ └── game.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | logs/ 3 | dockerdata -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode/ 3 | 4 | # grpc 5 | internal/pb/*.pb.go 6 | 7 | log/ 8 | nohup.out 9 | tables.zip 10 | *_server 11 | *_robot 12 | *.exe 13 | ./config.yaml 14 | 15 | *.swp 16 | error.log 17 | 18 | # web 19 | tests/web/node_modules 20 | tests/web/.next 21 | 22 | # docker compose 23 | dockerdata -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hub.atomgit.com/amd64/golang:1.21.1-bookworm AS gobuilder 2 | 3 | ENV CGO_ENABLED 0 4 | ENV GOPROXY https://goproxy.cn,direct 5 | 6 | WORKDIR /build 7 | 8 | COPY go.mod go.sum ./ 9 | RUN go mod download 10 | COPY . . 11 | RUN go build -ldflags="-s -w" -o ./bin/hall_server ./hall/*.go 12 | RUN go build -ldflags="-s -w" -o ./bin/cache_server ./cache/*.go 13 | RUN go build -ldflags="-s -w" -o ./bin/games_server ./games/*.go 14 | RUN go build -ldflags="-s -w" -o ./bin/login_server ./login/*.go 15 | RUN go install github.com/guogeer/quasar/v2/...@v2.0.5 16 | 17 | FROM hub.atomgit.com/amd64/node:20.6.1-bookworm AS nodebuilder 18 | WORKDIR /build 19 | COPY tests/web/package.json . 20 | RUN npm install 21 | COPY tests/web . 22 | RUN npm run build 23 | 24 | FROM hub.atomgit.com/amd64/debian:12.1-slim 25 | 26 | WORKDIR /app 27 | COPY tests/config.yaml config.yaml 28 | COPY tests tests 29 | COPY --from=gobuilder /build/bin . 30 | COPY --from=gobuilder /go/bin/gateway gateway_server 31 | COPY --from=gobuilder /go/bin/router router_server 32 | COPY --from=nodebuilder /build/dist demo 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 项目计划 2 | 3 | 过往棋牌、休闲项目代码总结,按如下计划更新 4 | - 移除了相关的业务(√) 5 | - 项目可以在本地正常运行(√) 6 | - 添加可体验的小游戏demo/fingerGuessing(√) 7 | - 开放各种地方麻将。旧线上生产项目代码,合并到新项目中。因缺少前端配合测试,完成度90%(√) 8 | - 开放斗地主。旧线上生产项目代码,合并到新项目中。因缺少前端配合测试,完成度95%(√) 9 | - 开放牛牛。旧线上生产项目代码,合并到新项目中。因缺少前端配合测试,完成度90%(√) 10 | - 开放小九。完成度75%(√) 11 | - 开放跑得快。旧线上生产项目代码,合并到新项目中。因缺少前端配合测试,完成度90%(√) 12 | - 开放十三水。完成度70%(√) 13 | - 当前每个玩法都编译为一个程序,优化为玩法统一编译为一个game_server(√) 14 | - 优化石头剪刀布demo。前端支持九宫格方式展示(√) 15 | - 增加Dockerfile文件。方便本地体验(√) 16 | - cache重构,引入gorm (√) 17 | - 增加http/quic gateway。大厅的请求支持短链接,适应网络不稳定的情况(×) 18 | - 开放管理后台。增加付费到账demo(×) 19 | - 开放压大小(×) 20 | 21 | ### 命名风格 22 | 23 | - 驼峰。首字母小写 24 | - 网络协议ID、协议字段 25 | - protobuf字段 26 | - lua脚本 27 | - 配置表表名、列名 28 | - 驼峰。首字母大写 29 | - protobuf消息名 30 | - 下划线 31 | - SQL表名、字段、索引等 32 | 33 | ### 快速体验 34 | ```sh 35 | docker compose up -d 36 | # 访问demo http://localhost:9501 37 | ``` 38 | 39 | ### 本地部署 40 | 41 | 1、初始化grpc脚本 42 | ```sh 43 | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 44 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 45 | export PATH="$PATH:$(go env GOPATH)/bin" 46 | sudo apt install protobuf-compiler 47 | protoc --proto_path=./ --go-grpc_out=./ --go-grpc_opt=paths=source_relative --go_out=./ --go_opt=paths=source_relative internal/pb/*.proto 48 | ``` 49 | 2、安装依赖的服务 50 | ```sh 51 | go install github.com/guogeer/quasar/v2/... 52 | # 若未设置$GOPATH 53 | cp ~/go/bin/gateway gateway_server 54 | cp ~/go/bin/router router_server 55 | # 若设置了$GOPATH 56 | # cp $GOPATH/bin/gateway gateway_server 57 | # cp $GOPATH/bin/router router_server 58 | # 初始化配置 59 | cp config_bak.yaml config.yaml #根据实际部署修改配置 60 | nohup ./router_server --port 9010 1>/dev/null 2>>error.log & 61 | # 配置对外的地址,如example.com 62 | nohup ./gateway_server --port 8201 --proxy example.com 1>/dev/null 2>>error.log & 63 | ``` 64 | 3、启动业务(调试模式) 65 | 66 | 3.1 创建go.work 67 | ``` 68 | go 1.21.1 69 | 70 | use ( 71 | ./gofishing-game 72 | ./quasar 73 | ) 74 | 75 | ``` 76 | 3.2 启动服务 77 | ```sh 78 | # go run ./quasar/gateway --port 9010 79 | # go run ./quasar/router --port 8201 80 | go run ./gofishing-game/cache --port 9000 81 | go run ./gofishing-game/hall --port 9022 82 | go run ./gofishing-game/login --port 9501 83 | go run ./gofishing-game/games --server_id game_1 --port 9021 84 | ``` 85 | 4、调试工具 86 | 新增了client.html调试工具 87 | - 自动登录并连接网关 88 | - 打开控制台可以看到消息历史 89 | - 可以模拟消息请求 90 | - 支持url自定义参数open_id(默认test001)、addr(默认localhost:9501) 91 | 新增了games/demo/fingerguessing.html体验页面,可体验石头剪刀布 92 | -------------------------------------------------------------------------------- /cache/item.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "gofishing-game/internal" 8 | "gofishing-game/internal/dbo" 9 | "gofishing-game/internal/pb" 10 | 11 | "github.com/guogeer/quasar/v2/log" 12 | ) 13 | 14 | // 每日定时拆分item_log 15 | func splitItemLog() { 16 | db := dbo.Get() 17 | table := "item_log" 18 | lastTable := table + "_" + time.Now().Add(-23*time.Hour).Format("20060102") 19 | removeTable := table + "_" + time.Now().Add(-90*24*time.Hour).Format("20060102") 20 | // 保留N天日志 21 | log.Info("split table " + table + " drop " + removeTable) 22 | db.Exec("drop table if exists " + removeTable) 23 | db.Exec("create table if not exists " + lastTable + " like " + table) 24 | // 例如:rename table item_log to item_log_temp,item_log_20210102 to item_log, item_log_temp to item_log_20210102 25 | db.Exec("rename table " + table + " to " + table + "_temp, " + lastTable + " to " + table + ", " + table + "_temp to " + lastTable) 26 | } 27 | 28 | // 批量增加物品日志 29 | func (cc *Cache) AddSomeItemLog(ctx context.Context, req *pb.AddSomeItemLogReq) (*pb.EmptyResp, error) { 30 | uid := req.Uid 31 | way := req.Way 32 | uuid := req.Uuid 33 | db := dbo.Get() 34 | 35 | createTime := time.Unix(req.CreateTs, 0).Format(internal.LongDateFmt) 36 | for _, item := range req.Items { 37 | db.Exec("insert item_log(uid,way,uuid,item_id,num,balance,create_time) values(?,?,?,?,?,?,?)", 38 | uid, way, uuid, item.Id, item.Num, item.Balance, createTime) 39 | } 40 | return &pb.EmptyResp{}, nil 41 | } 42 | 43 | // 批量增加物品 44 | func (cc *Cache) AddSomeItem(ctx context.Context, req *pb.AddSomeItemReq) (*pb.EmptyResp, error) { 45 | uid := req.Uid 46 | return cc.SaveBin(ctx, &pb.SaveBinReq{ 47 | Uid: uid, 48 | Bin: &pb.UserBin{ 49 | Offline: &pb.OfflineBin{Items: req.Items}, 50 | }, 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /cache/mail.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/guogeer/quasar/v2/utils" 7 | 8 | "gofishing-game/cache/models" 9 | "gofishing-game/internal/dbo" 10 | "gofishing-game/internal/pb" 11 | ) 12 | 13 | // 邮件 14 | func (cc *Cache) QuerySomeMail(ctx context.Context, req *pb.QuerySomeMailReq) (*pb.QuerySomeMailResp, error) { 15 | db := dbo.Get() 16 | 17 | var params []any 18 | where := " where 1=1" 19 | if req.Id > 0 { 20 | where += " and `id`=?" 21 | params = append(params, req.Id) 22 | } 23 | if req.Type > 0 { 24 | where += " and `type`=?" 25 | params = append(params, req.Type) 26 | } 27 | if req.RecvId > 0 { 28 | where += " and `recv_id`=?" 29 | params = append(params, req.RecvId) 30 | } 31 | if req.Status > 0 { 32 | where += " and `status`=?" 33 | params = append(params, req.Status) 34 | } 35 | where += " order by id desc" 36 | if req.Num > 0 { 37 | where += " limit ?" 38 | params = append(params, req.Num) 39 | } 40 | 41 | var mails []models.Mail 42 | db.Find(&mails).Where(where, params...) 43 | 44 | var pbMails []*pb.Mail 45 | for _, mail := range mails { 46 | pbMail := &pb.Mail{} 47 | utils.DeepCopy(pbMail, mail) 48 | pbMails = append(pbMails, pbMail) 49 | } 50 | return &pb.QuerySomeMailResp{Mails: pbMails}, nil 51 | } 52 | 53 | func (cc *Cache) SendMail(ctx context.Context, req *pb.SendMailReq) (*pb.SendMailResp, error) { 54 | db := dbo.Get() 55 | 56 | mail := models.Mail{ 57 | Type: int(req.Mail.Type), 58 | RecvUid: int(req.Mail.RecvId), 59 | SendUid: int(req.Mail.SendId), 60 | Title: req.Mail.Title, 61 | Body: req.Mail.Body, 62 | } 63 | result := db.Create(&mail) 64 | return &pb.SendMailResp{Id: int64(mail.Id)}, result.Error 65 | } 66 | 67 | func (cc *Cache) OperateMail(ctx context.Context, req *pb.OperateMailReq) (*pb.OperateMailResp, error) { 68 | db := dbo.Get() 69 | 70 | // 邮件状态只能递增 71 | result := db.Model(models.Mail{}).UpdateColumn("`status`=?", req.NewStatus).Where("id=? and `status`=?", req.Id, req.CurStatus) 72 | return &pb.OperateMailResp{EffectRows: int32(result.RowsAffected)}, result.Error 73 | } 74 | -------------------------------------------------------------------------------- /cache/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | 8 | "gofishing-game/cache/models" 9 | "gofishing-game/internal/dbo" 10 | "gofishing-game/internal/env" 11 | "gofishing-game/internal/pb" 12 | 13 | "github.com/guogeer/quasar/v2/cmd" 14 | "github.com/guogeer/quasar/v2/log" 15 | "google.golang.org/grpc" 16 | ) 17 | 18 | var port = flag.Int("port", 9000, "cache server port") 19 | 20 | func main() { 21 | flag.Parse() 22 | 23 | // init database 24 | t := env.Config().DB.Game 25 | dbo.SetSource(t.User, t.Password, t.Addr, t.Name) 26 | db, _ := dbo.Get().DB() 27 | if n := t.MaxIdleConns; n > 0 { 28 | db.SetMaxIdleConns(n) 29 | } 30 | if n := t.MaxOpenConns; n > 0 { 31 | db.SetMaxOpenConns(n) 32 | } 33 | dbo.Get().AutoMigrate( 34 | models.ClientVersion{}, 35 | models.Dict{}, 36 | models.ItemLog{}, 37 | models.Mail{}, 38 | models.OnlineLog{}, 39 | models.Script{}, 40 | models.Table{}, 41 | models.UserBin{}, 42 | models.UserInfo{}, 43 | models.UserPlate{}, 44 | ) 45 | 46 | go func() { Tick() }() 47 | 48 | addr := fmt.Sprintf(":%v", *port) 49 | lis, err := net.Listen("tcp", addr) 50 | if err != nil { 51 | log.Fatalf("failed to listen: %v", err) 52 | } 53 | log.Infof("start cache server and listen %s", addr) 54 | cmd.RegisterService(&cmd.ServiceConfig{Name: "cache", Addr: addr}) 55 | log.Debug("register service ok") 56 | //opts := []grpc.ServerOption{ 57 | // grpc.UnaryInterceptor(ensureValidToken), 58 | //} 59 | opts := []grpc.ServerOption{} 60 | grpcServer := grpc.NewServer(opts...) 61 | pb.RegisterCacheServer(grpcServer, &Cache{}) 62 | grpcServer.Serve(lis) 63 | } 64 | -------------------------------------------------------------------------------- /cache/manage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // GM管理后台 4 | 5 | import ( 6 | "context" 7 | 8 | "gofishing-game/cache/models" 9 | "gofishing-game/internal/dbo" 10 | "gofishing-game/internal/pb" 11 | 12 | "gorm.io/gorm/clause" 13 | ) 14 | 15 | // 加载配置表 16 | func (cc *Cache) LoadTable(ctx context.Context, req *pb.LoadTableReq) (*pb.LoadTableResp, error) { 17 | db := dbo.Get() 18 | 19 | var table models.Table 20 | err := db.Where("name=?", req.Name).Take(&table).Error 21 | return &pb.LoadTableResp{File: &pb.TableConfig{ 22 | Name: req.Name, 23 | Content: table.Content, 24 | Version: int32(table.Version), 25 | }}, err 26 | } 27 | 28 | // 加载全部配置表 29 | func (cc *Cache) LoadAllTable(req *pb.EmptyReq, stream pb.Cache_LoadAllTableServer) error { 30 | db := dbo.Get() 31 | 32 | var tables []models.Table 33 | db.Find(&tables) 34 | for _, table := range tables { 35 | if err := stream.Send(&pb.TableConfig{Version: int32(table.Version), Name: table.Name, Content: table.Content}); err != nil { 36 | return err 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | // 加载脚本 43 | func (cc *Cache) LoadScript(ctx context.Context, req *pb.LoadScriptReq) (*pb.LoadScriptResp, error) { 44 | db := dbo.Get() 45 | 46 | var script models.Script 47 | err := db.Where("name=?", req.Name).Take(&script).Error 48 | return &pb.LoadScriptResp{File: &pb.ScriptFile{Name: req.Name, Body: script.Body}}, err 49 | } 50 | 51 | // 加载全部配置表 52 | func (cc *Cache) LoadAllScript(req *pb.EmptyReq, stream pb.Cache_LoadAllScriptServer) error { 53 | db := dbo.Get() 54 | 55 | var scripts []models.Script 56 | db.Find(&scripts) 57 | for _, script := range scripts { 58 | if err := stream.Send(&pb.ScriptFile{Name: script.Name, Body: script.Body}); err != nil { 59 | return err 60 | } 61 | } 62 | return nil 63 | } 64 | 65 | // 查询字典 66 | func (cc *Cache) QueryDict(ctx context.Context, req *pb.QueryDictReq) (*pb.QueryDictResp, error) { 67 | db := dbo.Get() 68 | 69 | var dict models.Dict 70 | db.Where("key=?", req.Key).Take(&dict) 71 | return &pb.QueryDictResp{Value: []byte(dict.Value)}, nil 72 | } 73 | 74 | // 更新字典 75 | func (cc *Cache) UpdateDict(ctx context.Context, req *pb.UpdateDictReq) (*pb.EmptyResp, error) { 76 | db := dbo.Get() 77 | 78 | err := db.Model(models.Dict{}).Clauses(&clause.OnConflict{UpdateAll: true}).Create(&models.Dict{Key: req.Key, Value: string(req.Value)}).Error 79 | return &pb.EmptyResp{}, err 80 | } 81 | -------------------------------------------------------------------------------- /cache/models/game.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | ) 7 | 8 | type UserInfo struct { 9 | Id int `gorm:"primaryKey;autoIncrement"` 10 | Nickname string `gorm:"type:varchar(50)"` 11 | Sex int 12 | Icon string `gorm:"type:varchar(255)"` 13 | PlateIcon string `gorm:"type:varchar(255)"` 14 | TimeZone float32 15 | Email string `gorm:"type:varchar(64)"` 16 | Ip string `gorm:"type:varchar(32)"` 17 | ClientVersion string `gorm:"type:varchar(32)"` 18 | Mac string `gorm:"type:varchar(24)"` 19 | Imei string `gorm:"type:varchar(24)"` 20 | Imsi string `gorm:"type:varchar(24)"` 21 | ChanId string `gorm:"type:varchar(32)"` 22 | ServerLocation string `gorm:"type:varchar(32)"` 23 | CreateTime time.Time `gorm:"autoCreateTime"` 24 | } 25 | 26 | type ItemLog struct { 27 | Id int `gorm:"primaryKey;autoIncrement"` 28 | Uid int 29 | ItemId int 30 | Way string `gorm:"type:varchar(64)"` 31 | Num int 32 | Balance int 33 | Uuid string `gorm:"type:varchar(64)"` 34 | CreateTime time.Time `gorm:"autoCreateTime"` 35 | } 36 | 37 | type OnlineLog struct { 38 | Id int `gorm:"primaryKey;autoIncrement"` 39 | Uid int `gorm:"index:uid_date_idx,unique"` 40 | CurDate string `gorm:"index:uid_date_idx,unique"` 41 | Ip string `gorm:"type:varchar(32)"` 42 | ClientVersion string `gorm:"type:varchar(32)"` 43 | Mac string `gorm:"type:varchar(24)"` 44 | Imei string `gorm:"type:varchar(24)"` 45 | Imsi string `gorm:"type:varchar(24)"` 46 | ChanId string `gorm:"type:varchar(32)"` 47 | LoginTime time.Time 48 | OfflineTime sql.NullTime 49 | } 50 | 51 | type UserPlate struct { 52 | Id int `gorm:"primaryKey;autoIncrement"` 53 | Uid int 54 | Plate string `gorm:"type:varchar(16)"` 55 | OpenId string `gorm:"type:varchar(48);uniqueIndex"` 56 | CreateTime time.Time `gorm:"autoCreateTime"` 57 | } 58 | 59 | type UserBin struct { 60 | Id int `gorm:"primaryKey;autoIncrement"` 61 | Uid int `gorm:"index:user_bin_idx,unique"` 62 | Class string `gorm:"type:varchar(16);index:user_bin_idx,unique"` 63 | Bin []byte `gorm:"type:blob"` 64 | UpdateTime time.Time `gorm:"autoUpdateTime"` 65 | } 66 | 67 | type Mail struct { 68 | Id int `gorm:"primaryKey;autoIncrement"` 69 | Type int 70 | SendUid int 71 | RecvUid int `gorm:"index"` 72 | Status int 73 | Title string `gorm:"type:varchar(64)"` 74 | Body string `gorm:"type:text"` 75 | SendTime time.Time `gorm:"autoCreateTime"` 76 | } 77 | 78 | type Dict struct { 79 | Id int `gorm:"primaryKey;autoIncrement"` 80 | Key string `gorm:"type:varchar(32);index"` 81 | Value string `gorm:"type:json"` 82 | UpdateTime time.Time `gorm:"autoUpdateTime"` 83 | } 84 | 85 | type Table struct { 86 | Id int `gorm:"primaryKey;autoIncrement"` 87 | Name string `gorm:"type:varchar(32);index"` 88 | Version int 89 | Content string `gorm:"type:text"` 90 | UpdateTime time.Time `gorm:"onUpdateTime"` 91 | } 92 | 93 | type Script struct { 94 | Id int `gorm:"primaryKey;autoIncrement"` 95 | Name string `gorm:"type:varchar(32);index"` 96 | Body string `gorm:"type:text"` 97 | UpdateTime time.Time `gorm:"onUpdateTime"` 98 | } 99 | 100 | type ClientVersion struct { 101 | Id int `gorm:"primaryKey;autoIncrement"` 102 | ChanId string `gorm:"type:varchar(32)"` 103 | Version string `gorm:"type:varchar(16)"` 104 | AllowIP string `gorm:"type:varchar(64)"` 105 | AllowUid string `gorm:"type:varchar(64)"` 106 | ChangeLog string `gorm:"type:text"` 107 | Reward string `gorm:"type:varchar(64)"` 108 | UpdateTime time.Time `gorm:"onUpdateTime"` 109 | } 110 | -------------------------------------------------------------------------------- /cache/ticker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "gofishing-game/internal" 5 | "time" 6 | 7 | "github.com/guogeer/quasar/v2/config" 8 | "github.com/guogeer/quasar/v2/log" 9 | ) 10 | 11 | func tick1d() { 12 | splitItemLog() 13 | } 14 | 15 | func Tick() { 16 | nextDateStr := time.Now().Add(24 * time.Hour).Format(internal.ShortDateFmt) 17 | nextDate, _ := config.ParseTime(nextDateStr) 18 | dayTimer := time.NewTimer(time.Until(nextDate)) 19 | for { 20 | <-dayTimer.C 21 | log.Infof("call timer %s", time.Now().Format(internal.LongDateFmt)) 22 | dayTimer.Reset(24 * time.Hour) 23 | tick1d() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cache/user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/guogeer/quasar/v2/utils" 7 | 8 | "gofishing-game/cache/models" 9 | "gofishing-game/internal" 10 | "gofishing-game/internal/dbo" 11 | "gofishing-game/internal/pb" 12 | ) 13 | 14 | func (cc *Cache) QueryUserInfo(ctx context.Context, req *pb.QueryUserInfoReq) (*pb.QueryUserInfoResp, error) { 15 | db := dbo.Get() 16 | 17 | var userInfo models.UserInfo 18 | db.Take(&userInfo, req.Uid) 19 | var userPlate models.UserPlate 20 | db.First(&userPlate, req.Uid) 21 | return &pb.QueryUserInfoResp{Info: &pb.UserInfo{ 22 | Uid: int32(req.Uid), 23 | ServerLocation: userInfo.ServerLocation, 24 | CreateTime: userInfo.CreateTime.Format(internal.LongDateFmt), 25 | ChanId: userInfo.ChanId, 26 | Sex: int32(userInfo.Sex), 27 | Icon: userInfo.Icon, 28 | PlateIcon: userInfo.PlateIcon, 29 | Nickname: userInfo.Nickname, 30 | OpenId: userPlate.OpenId, 31 | }}, nil 32 | } 33 | 34 | func (cc *Cache) SetUserInfo(ctx context.Context, req *pb.SetUserInfoReq) (*pb.EmptyResp, error) { 35 | db := dbo.Get() 36 | db.Model(models.UserInfo{}).Updates(map[string]any{ 37 | "sex": req.Sex, 38 | "nickname": req.Nickname, 39 | "icon": req.Icon, 40 | "email": req.Email, 41 | }) 42 | return &pb.EmptyResp{}, nil 43 | } 44 | 45 | func (cc *Cache) QuerySimpleUserInfo(ctx context.Context, req *pb.QuerySimpleUserInfoReq) (*pb.QuerySimpleUserInfoResp, error) { 46 | db := dbo.Get() 47 | simpleInfo := &pb.SimpleUserInfo{Uid: req.Uid} 48 | if req.OpenId != "" { 49 | var userPlate models.UserPlate 50 | db.Take(&userPlate, req.OpenId) 51 | simpleInfo.Uid = int32(userPlate.Uid) 52 | } 53 | userInfo, _ := cc.QueryUserInfo(ctx, &pb.QueryUserInfoReq{Uid: simpleInfo.Uid}) 54 | utils.DeepCopy(simpleInfo, userInfo) 55 | 56 | var userBin models.UserBin 57 | db.Where("uid=? and `class`=?", simpleInfo.Uid, "global").Take(&userBin) 58 | var globalBin pb.GlobalBin 59 | dbo.PB(&globalBin).Scan(userBin.Bin) 60 | 61 | simpleInfo.Level = globalBin.Level 62 | return &pb.QuerySimpleUserInfoResp{Info: simpleInfo}, nil 63 | } 64 | -------------------------------------------------------------------------------- /config_bak.yaml: -------------------------------------------------------------------------------- 1 | environment: release 2 | serverKey: helloworld 3 | clientKey: hellokitty 4 | enableDebug: false 5 | db: 6 | game: 7 | user: root 8 | password: password 9 | address: "localhost:3306" 10 | name: game 11 | maxIdleConns: 10 12 | maxOpenConns: 20 13 | manage: 14 | user: root 15 | password: password 16 | address: "localhost:3306" 17 | name: manage 18 | maxIdleConns: 10 19 | maxOpenConns: 20 20 | serverList: 21 | - name: router 22 | address: "localhost:9010" 23 | tablePath: tests/tables 24 | scriptPath: tests/scripts 25 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | # version: '3' 2 | services: 3 | router_server: 4 | restart: always 5 | # image: registry.cn-hangzhou.aliyuncs.com/guogeer/gofishing-game:v2.0.1 6 | image: registry.cn-hangzhou.aliyuncs.com/guogeer/gofishing-game:v2.0.4 7 | container_name: "router_server" 8 | command: ./router_server 9 | ports: 10 | - "9010:9010" 11 | networks: 12 | - gamenet 13 | gateway_server: 14 | image: registry.cn-hangzhou.aliyuncs.com/guogeer/gofishing-game:v2.0.4 15 | restart: always 16 | container_name: "gateway_server" 17 | # 配置gateway对外地址 18 | command: ./gateway_server --port 8020 --proxy localhost 19 | ports: 20 | - "8020:8020" 21 | networks: 22 | - gamenet 23 | cache_server: 24 | image: registry.cn-hangzhou.aliyuncs.com/guogeer/gofishing-game:v2.0.4 25 | restart: always 26 | container_name: "cache_server" 27 | command: ./cache_server --port 9000 28 | ports: 29 | - "9000:9000" 30 | networks: 31 | - gamenet 32 | hall_server: 33 | image: registry.cn-hangzhou.aliyuncs.com/guogeer/gofishing-game:v2.0.4 34 | restart: always 35 | container_name: "hall_server" 36 | command: ./hall_server --port 19001 37 | networks: 38 | - gamenet 39 | games_server: 40 | image: registry.cn-hangzhou.aliyuncs.com/guogeer/gofishing-game:v2.0.4 41 | restart: always 42 | container_name: "games_server" 43 | command: ./games_server --port 19002 44 | networks: 45 | - gamenet 46 | login_server: 47 | image: registry.cn-hangzhou.aliyuncs.com/guogeer/gofishing-game:v2.0.4 48 | restart: always 49 | container_name: "login_server" 50 | command: ./login_server --rootpath demo 51 | ports: 52 | - "9501:9501" 53 | networks: 54 | - gamenet 55 | mariadb: 56 | image: hub.atomgit.com/amd64/mariadb:10.6 57 | container_name: "mariadb" 58 | environment: 59 | MYSQL_ROOT_PASSWORD: 123456 60 | volumes: 61 | - /etc/localtime:/etc/localtime 62 | - /tmp/dockerdata/mariadb:/var/lib/mysql 63 | ports: 64 | - "3306:3306" 65 | networks: 66 | - gamenet 67 | networks: 68 | gamenet: 69 | driver: bridge -------------------------------------------------------------------------------- /games/demo/fingerguessing/handle.go: -------------------------------------------------------------------------------- 1 | package fingerguessing 2 | 3 | import ( 4 | "gofishing-game/internal/gameutils" 5 | "gofishing-game/service" 6 | "gofishing-game/service/roomutils" 7 | 8 | "github.com/guogeer/quasar/v2/cmd" 9 | ) 10 | 11 | func init() { 12 | cmd.Bind("chooseGesture", funcChooseGesture, (*fingerGuessingArgs)(nil), cmd.WithServer((*fingerGuessingWorld)(nil).GetName())) 13 | } 14 | 15 | var fingerGuessingGuestures = []string{"rock", "scissor", "paple"} 16 | 17 | type fingerGuessingArgs struct { 18 | Gesture string `json:"gesture"` 19 | } 20 | 21 | func getFingerGuessingPlayer(player *service.Player) *fingerGuessingPlayer { 22 | return player.GameAction.(*fingerGuessingPlayer) 23 | } 24 | 25 | func funcChooseGesture(ctx *cmd.Context, data any) { 26 | args := data.(*fingerGuessingArgs) 27 | ply := service.GetPlayerByContext(ctx) 28 | if ply == nil { 29 | return 30 | } 31 | 32 | e := getFingerGuessingPlayer(ply).ChooseGesture(args.Gesture) 33 | ply.WriteErr("chooseGesture", e, map[string]any{"gesture": args.Gesture, "uid": ply.Id}) 34 | roomObj := roomutils.GetRoomObj(ply) 35 | if roomObj.GetSeatIndex() != roomutils.NoSeat { 36 | roomObj.Room().Broadcast("chooseGesture", gameutils.MergeError(nil, map[string]any{"uid": ply.Id, "gesture": args.Gesture, "seatIndex": roomObj.GetSeatIndex()}), ply.Id) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /games/demo/fingerguessing/player.go: -------------------------------------------------------------------------------- 1 | package fingerguessing 2 | 3 | import ( 4 | "gofishing-game/internal/errcode" 5 | "gofishing-game/internal/gameutils" 6 | "gofishing-game/internal/pb" 7 | "gofishing-game/service" 8 | "gofishing-game/service/roomutils" 9 | "slices" 10 | 11 | "github.com/guogeer/quasar/v2/config" 12 | ) 13 | 14 | var errInvalidGesture = errcode.New("invalid_gesture", "错误的手势") 15 | 16 | type fingerGuessingPlayer struct { 17 | *service.Player 18 | 19 | gesture string 20 | winPlay int 21 | losePlay int 22 | totalPlay int 23 | } 24 | 25 | var _ service.GameAction = (*fingerGuessingPlayer)(nil) 26 | 27 | func (ply *fingerGuessingPlayer) TryEnter() errcode.Error { 28 | return nil 29 | } 30 | 31 | func (ply *fingerGuessingPlayer) TryLeave() errcode.Error { 32 | return nil 33 | } 34 | 35 | func (ply *fingerGuessingPlayer) BeforeEnter() { 36 | room := roomutils.GetRoomObj(ply.Player).CustomRoom().(*fingerGuessingRoom) 37 | ply.SetClientValue("roomInfo", room.GetClientInfo()) 38 | ply.SetClientValue("gesture", ply.gesture) 39 | } 40 | 41 | func (ply *fingerGuessingPlayer) AfterEnter() { 42 | } 43 | 44 | func (ply *fingerGuessingPlayer) BeforeLeave() { 45 | } 46 | 47 | func (ply *fingerGuessingPlayer) Load(data any) { 48 | bin := data.(*pb.UserBin) 49 | if bin.Room.FingerGuessing != nil { 50 | ply.winPlay = int(bin.Room.FingerGuessing.WinPlay) 51 | ply.losePlay = int(bin.Room.FingerGuessing.LosePlay) 52 | ply.totalPlay = int(bin.Room.FingerGuessing.TotalPlay) 53 | } 54 | } 55 | 56 | func (ply *fingerGuessingPlayer) Save(data any) { 57 | bin := data.(*pb.UserBin) 58 | if bin.Room == nil { 59 | bin.Room = &pb.RoomBin{} 60 | } 61 | bin.Room.FingerGuessing = &pb.FingerGuessingRoom{ 62 | WinPlay: int32(ply.winPlay), 63 | LosePlay: int32(ply.losePlay), 64 | TotalPlay: int32(ply.totalPlay), 65 | } 66 | } 67 | 68 | func (ply *fingerGuessingPlayer) ChooseGesture(gesture string) errcode.Error { 69 | if slices.Index(fingerGuessingGuestures, gesture) < 0 { 70 | return errInvalidGesture 71 | } 72 | if ply.gesture != "" { 73 | return errcode.New("choose_gesture_already", "choose gesture already") 74 | } 75 | ply.gesture = gesture 76 | 77 | room := roomutils.GetRoomObj(ply.Player).Room() 78 | if room.Status == 0 { 79 | return errcode.New("room_not_playing", "room not playing") 80 | } 81 | cost, _ := config.String("room", room.SubId, "cost") 82 | 83 | costItems := gameutils.ParseNumbericItems(cost) 84 | for _, item := range costItems { 85 | if !ply.BagObj().IsEnough(item.GetId(), item.GetNum()) { 86 | return errcode.MoreItem(item.GetId()) 87 | } 88 | } 89 | 90 | ply.BagObj().CostSomeItems(costItems, "choose_guesture") 91 | 92 | var isAllChoose bool 93 | for _, roomp := range room.GetAllPlayers() { 94 | other := getFingerGuessingPlayer(roomp) 95 | if other.gesture == "" { 96 | isAllChoose = false 97 | break 98 | } 99 | } 100 | if isAllChoose { 101 | room.GameOver() 102 | } 103 | 104 | return nil 105 | } 106 | 107 | func (ply *fingerGuessingPlayer) Compare(gesture string) int { 108 | if ply.gesture == gesture { 109 | return 0 110 | } 111 | index := slices.Index(fingerGuessingGuestures, gesture) 112 | if (index+1)%3 == slices.Index(fingerGuessingGuestures, ply.gesture) { 113 | return 1 114 | } 115 | return -1 116 | } 117 | 118 | func (ply *fingerGuessingPlayer) GameOver(guesture string) (int64, int) { 119 | cmp := ply.Compare(guesture) 120 | room := roomutils.GetRoomObj(ply.Player).Room() 121 | 122 | cost, _ := config.String("room", room.SubId, "cost") 123 | costItems := gameutils.ParseNumbericItems(cost) 124 | 125 | if cmp == 0 { 126 | ply.BagObj().AddSomeItems(costItems, "finger_guessing_back") 127 | } 128 | if cmp <= 0 { 129 | return gameutils.CountItems(costItems, gameutils.ItemIdGold), cmp 130 | } 131 | 132 | var baseAward, constAward string 133 | config.Scan("room", room.SubId, "baseAward, constAward", &baseAward, &constAward) 134 | awardItems := append(gameutils.ParseNumbericItems(baseAward), gameutils.ParseNumbericItems(constAward)...) 135 | ply.BagObj().AddSomeItems(awardItems, "finger_guessing_award") 136 | 137 | return gameutils.CountItems(awardItems, gameutils.ItemIdGold) * int64(cmp), cmp 138 | 139 | } 140 | -------------------------------------------------------------------------------- /games/demo/fingerguessing/room.go: -------------------------------------------------------------------------------- 1 | package fingerguessing 2 | 3 | import ( 4 | "gofishing-game/internal/gameutils" 5 | "gofishing-game/service/roomutils" 6 | "math/rand" 7 | 8 | "github.com/guogeer/quasar/v2/cmd" 9 | ) 10 | 11 | type clientFingerGuessingUser struct { 12 | Uid int `json:"uid"` 13 | Gesture string `json:"gesture"` 14 | SeatIndex int `json:"seatIndex"` 15 | Chip int64 `json:"chip"` 16 | Nickname string `json:"nickname"` 17 | } 18 | 19 | type clientFingerGuessingRoom struct { 20 | Status int `json:"status"` 21 | SeatPlayers []clientFingerGuessingUser `json:"seatPlayers"` 22 | Countdown int64 `json:"countdown"` 23 | } 24 | 25 | type fingerGuessingRoom struct { 26 | *roomutils.Room 27 | } 28 | 29 | func (room *fingerGuessingRoom) GetClientInfo() clientFingerGuessingRoom { 30 | info := clientFingerGuessingRoom{ 31 | Status: room.Status, 32 | SeatPlayers: []clientFingerGuessingUser{}, 33 | } 34 | info.Countdown = room.Countdown() 35 | for _, seatPlayer := range room.GetSeatPlayers() { 36 | p := seatPlayer.GameAction.(*fingerGuessingPlayer) 37 | seatIndex := roomutils.GetRoomObj(seatPlayer).GetSeatIndex() 38 | user := clientFingerGuessingUser{Uid: p.Id, Gesture: p.gesture, SeatIndex: seatIndex, Nickname: p.Nickname} 39 | user.Chip = p.BagObj().NumItem(gameutils.ItemIdGold) 40 | info.SeatPlayers = append(info.SeatPlayers, user) 41 | } 42 | 43 | return info 44 | } 45 | 46 | func (room *fingerGuessingRoom) StartGame() { 47 | room.Room.StartGame() 48 | 49 | room.Broadcast("startGame", cmd.M{"countdown": room.Countdown()}) 50 | } 51 | 52 | type userResult struct { 53 | WinChip int64 `json:"winChip"` 54 | SeatIndex int `json:"seatIndex"` 55 | Gesture string `json:"gesture"` 56 | Cmp int `json:"cmp"` 57 | } 58 | 59 | func (room *fingerGuessingRoom) GameOver() { 60 | gesture := fingerGuessingGuestures[rand.Intn(len(fingerGuessingGuestures))] 61 | users := make([]userResult, 0, 4) 62 | for _, seatPlayer := range room.GetSeatPlayers() { 63 | p := seatPlayer.GameAction.(*fingerGuessingPlayer) 64 | if p.gesture != "" { 65 | winChip, cmp := p.GameOver(gesture) 66 | roomObj := roomutils.GetRoomObj(seatPlayer) 67 | users = append(users, userResult{WinChip: winChip, Cmp: cmp, SeatIndex: roomObj.GetSeatIndex(), Gesture: p.gesture}) 68 | } 69 | } 70 | 71 | for _, seatPlayer := range room.GetSeatPlayers() { 72 | p := seatPlayer.GameAction.(*fingerGuessingPlayer) 73 | p.gesture = "" 74 | } 75 | 76 | room.Room.GameOver() 77 | room.Broadcast("gameOver", cmd.M{ 78 | "gesture": gesture, 79 | "result": users, 80 | "countdown": room.Countdown(), 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /games/demo/fingerguessing/world.go: -------------------------------------------------------------------------------- 1 | package fingerguessing 2 | 3 | import ( 4 | "gofishing-game/service" 5 | "gofishing-game/service/roomutils" 6 | ) 7 | 8 | type fingerGuessingWorld struct { 9 | } 10 | 11 | func GetWorld() *fingerGuessingWorld { 12 | return service.GetWorld((*fingerGuessingWorld)(nil).GetName()).(*fingerGuessingWorld) 13 | } 14 | 15 | func init() { 16 | w := &fingerGuessingWorld{} 17 | service.AddWorld(w) 18 | } 19 | 20 | func (w *fingerGuessingWorld) GetName() string { 21 | return "fingerGuessing" 22 | } 23 | 24 | func (w *fingerGuessingWorld) NewPlayer() *service.Player { 25 | p := &fingerGuessingPlayer{} 26 | p.Player = service.NewPlayer(p) 27 | p.Player.DataObj().Push(p) 28 | 29 | return p.Player 30 | } 31 | 32 | func (w *fingerGuessingWorld) NewRoom(subId int) *roomutils.Room { 33 | room := &fingerGuessingRoom{} 34 | room.Room = roomutils.NewRoom(subId, room) 35 | return room.Room 36 | } 37 | 38 | func GetPlayer(uid int) *fingerGuessingPlayer { 39 | if p := service.GetPlayer(uid); p != nil { 40 | return p.GameAction.(*fingerGuessingPlayer) 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /games/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "gofishing-game/service" 5 | "gofishing-game/service/roomutils" 6 | 7 | _ "gofishing-game/games/demo/fingerguessing" 8 | _ "gofishing-game/games/migrate/dice" 9 | _ "gofishing-game/games/migrate/doudizhu" 10 | _ "gofishing-game/games/migrate/lottery" 11 | _ "gofishing-game/games/migrate/mahjong" 12 | _ "gofishing-game/games/migrate/niuniu" 13 | _ "gofishing-game/games/migrate/paodekuai" 14 | _ "gofishing-game/games/migrate/sangong" 15 | _ "gofishing-game/games/migrate/texas" 16 | _ "gofishing-game/games/migrate/xiaojiu" 17 | _ "gofishing-game/games/migrate/zhajinhua" 18 | ) 19 | 20 | func main() { 21 | roomutils.LoadGames() 22 | service.Start() 23 | } 24 | -------------------------------------------------------------------------------- /games/migrate/dice/handle.go: -------------------------------------------------------------------------------- 1 | package dice 2 | 3 | import ( 4 | "gofishing-game/service" 5 | 6 | "github.com/guogeer/quasar/v2/cmd" 7 | ) 8 | 9 | func init() { 10 | cmd.BindFunc(Bet, (*diceArgs)(nil), cmd.WithServer((*DiceWorld)(nil).GetName())) 11 | cmd.BindFunc(GetLastHistory, (*diceArgs)(nil), cmd.WithServer((*DiceWorld)(nil).GetName())) 12 | } 13 | 14 | type diceArgs struct { 15 | Area int `json:"area,omitempty"` 16 | Gold int64 `json:"gold,omitempty"` 17 | PageNum int `json:"pageNum,omitempty"` 18 | } 19 | 20 | func GetPlayerByContext(ctx *cmd.Context) *DicePlayer { 21 | if ply := service.GetGatewayPlayer(ctx.Ssid); ply != nil { 22 | return ply.GameAction.(*DicePlayer) 23 | } 24 | return nil 25 | } 26 | 27 | func Bet(ctx *cmd.Context, iArgs interface{}) { 28 | args := iArgs.(*diceArgs) 29 | ply := GetPlayerByContext(ctx) 30 | if ply == nil { 31 | 32 | return 33 | } 34 | e := ply.Bet(args.Area, args.Gold) 35 | ply.WriteErr("bet", e, map[string]any{"area": args.Area, "gold": args.Gold}) 36 | } 37 | 38 | func GetLastHistory(ctx *cmd.Context, iArgs interface{}) { 39 | args := iArgs.(*diceArgs) 40 | ply := GetPlayerByContext(ctx) 41 | if ply == nil { 42 | return 43 | } 44 | room := ply.Room() 45 | if room == nil { 46 | return 47 | } 48 | ply.WriteJSON("getLastHistory", map[string]any{"last": room.GetLastHistory(args.PageNum)}) 49 | } 50 | -------------------------------------------------------------------------------- /games/migrate/dice/player.go: -------------------------------------------------------------------------------- 1 | package dice 2 | 3 | import ( 4 | "gofishing-game/internal/errcode" 5 | "gofishing-game/internal/gameutils" 6 | "gofishing-game/service" 7 | "gofishing-game/service/roomutils" 8 | 9 | "github.com/guogeer/quasar/v2/config" 10 | ) 11 | 12 | var ( 13 | errAlreadyBet = errcode.New("bet_already", "bet already") 14 | ) 15 | 16 | type DicePlayer struct { 17 | *service.Player 18 | 19 | areas [MaxBetArea]int64 20 | } 21 | 22 | func (ply *DicePlayer) TryEnter() errcode.Error { 23 | return nil 24 | } 25 | 26 | func (ply *DicePlayer) BeforeEnter() { 27 | ply.SetClientValue("areas", ply.areas) 28 | } 29 | 30 | func (ply *DicePlayer) BeforeLeave() { 31 | 32 | } 33 | 34 | func (ply *DicePlayer) AfterEnter() { 35 | 36 | } 37 | 38 | func (ply *DicePlayer) TryLeave() errcode.Error { 39 | if ply.countAllBet() > 0 { 40 | return errAlreadyBet 41 | } 42 | return nil 43 | } 44 | 45 | func (ply *DicePlayer) Room() *DiceRoom { 46 | if room := roomutils.GetRoomObj(ply.Player).CustomRoom(); room != nil { 47 | return room.(*DiceRoom) 48 | } 49 | return nil 50 | } 51 | 52 | func (ply *DicePlayer) countAllBet() int64 { 53 | var sum int64 54 | for _, n := range ply.areas { 55 | sum += n 56 | } 57 | return sum 58 | } 59 | 60 | func (ply *DicePlayer) GameOver() { 61 | for k := range ply.areas { 62 | ply.areas[k] = 0 63 | } 64 | } 65 | 66 | func (ply *DicePlayer) OnLeave() { 67 | } 68 | 69 | func (ply *DicePlayer) Bet(area int, gold int64) errcode.Error { 70 | room := ply.Room() 71 | if room == nil { 72 | return errcode.Retry 73 | } 74 | if room.Status == 0 { 75 | return errcode.Retry 76 | } 77 | if area < 0 || area >= len(ply.areas) || gold <= 0 { 78 | return errcode.Retry 79 | } 80 | n, _ := config.Int("config", "diceMinGold", "value") 81 | if !ply.BagObj().IsEnough(gameutils.ItemIdGold, max(n, gold)) { 82 | return errcode.MoreItem(gameutils.ItemIdGold) 83 | } 84 | n, _ = config.Int("config", "diceMaxGold", "value") 85 | if ply.countAllBet()+gold > n { 86 | return errcode.New("too_much_bet", "too much bet") 87 | } 88 | ply.areas[area] += gold 89 | ply.BagObj().Add(gameutils.ItemIdGold, -gold, "dice_bet") 90 | room.OnBet(ply, area, gold) 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /games/migrate/dice/room.go: -------------------------------------------------------------------------------- 1 | package dice 2 | 3 | import ( 4 | "fmt" 5 | "gofishing-game/internal/gameutils" 6 | "gofishing-game/service" 7 | "gofishing-game/service/roomutils" 8 | "math/rand" 9 | "time" 10 | 11 | "github.com/guogeer/quasar/v2/config" 12 | "github.com/guogeer/quasar/v2/log" 13 | "github.com/guogeer/quasar/v2/utils" 14 | ) 15 | 16 | const ( 17 | MaxBetArea = 18 18 | syncTime = 1000 * time.Millisecond 19 | ) 20 | 21 | type DiceRoom struct { 22 | *roomutils.Room 23 | 24 | betArea [MaxBetArea]int64 25 | chips []int64 26 | last [64]int // 历史记录 27 | lasti int // 历史记录索引 28 | } 29 | 30 | func (room *DiceRoom) OnEnter(player *service.Player) { 31 | ply := player.GameAction.(*DicePlayer) 32 | log.Infof("player %d enter room %d", ply.Id, room.Id) 33 | 34 | ply.SetClientValue("roomInfo", map[string]any{ 35 | "status": room.Status, 36 | "subId": room.Room.SubId, 37 | "chips": room.chips, 38 | "countdown": room.Countdown(), 39 | "betArea": room.betArea, 40 | }) 41 | } 42 | 43 | func (room *DiceRoom) OnBet(ply *DicePlayer, area int, gold int64) { 44 | room.betArea[area] += gold 45 | } 46 | 47 | func (room *DiceRoom) StartGame() { 48 | room.Room.StartGame() 49 | for k := range room.betArea { 50 | room.betArea[k] = 0 51 | } 52 | } 53 | 54 | func (room *DiceRoom) Award() { 55 | // 骰子 56 | var winArea [MaxBetArea]int64 57 | dice1, dice2 := rand.Intn(6)+1, rand.Intn(6)+1 58 | // dice1, dice2 = 2, 2 59 | sum := dice1 + dice2 60 | if dice1 == dice2 { 61 | a := []int{0, 11, 12, 13, 15, 16, 17} 62 | k := a[dice1] 63 | winArea[k] = 1 64 | winArea[14] = 1 65 | } else { 66 | if sum > 2 && sum < 7 { 67 | winArea[0] = 1 68 | } 69 | if sum > 6 && sum < 12 { 70 | winArea[10] = 1 71 | } 72 | } 73 | if sum > 2 && sum < 12 { 74 | winArea[sum-2] = 1 75 | } 76 | room.Sync() 77 | room.last[room.lasti] = dice1*10 + dice2 78 | room.lasti = (room.lasti + 1) % len(room.last) 79 | 80 | bigWinner, _ := config.Int("config", "diceBigWinner", "value", 50000) 81 | for _, one := range room.GetAllPlayers() { 82 | p := one.GameAction.(*DicePlayer) 83 | 84 | var gold int64 85 | for k, v := range p.areas { 86 | if winArea[k] == 0 { 87 | continue 88 | } 89 | gold += v * winArea[k] 90 | } 91 | 92 | if gold >= bigWinner { 93 | msg := fmt.Sprintf("恭喜%v在骰子场赢得%d万金币", p.Nickname, gold/10000) 94 | service.BroadcastNotification(msg) 95 | } 96 | 97 | p.BagObj().Add(gameutils.ItemIdGold, gold, "dice_award") 98 | p.WriteJSON("award", map[string]any{"winGold": gold, "countdown": room.Countdown(), "dice2": []int{dice1, dice2}}) 99 | } 100 | room.GameOver() 101 | } 102 | 103 | func (room *DiceRoom) OnTime() { 104 | room.Sync() 105 | utils.NewTimer(room.OnTime, syncTime) 106 | } 107 | 108 | func (room *DiceRoom) Sync() { 109 | room.Broadcast("sync", map[string]any{ 110 | "onlineNum": len(room.GetAllPlayers()), 111 | "betArea": room.betArea, 112 | }) 113 | } 114 | 115 | func (room *DiceRoom) GetLastHistory(n int) []int { 116 | var last []int 117 | N := len(room.last) 118 | if N == 0 { 119 | return last 120 | } 121 | for i := (N - n + room.lasti) % N; i != room.lasti; i = (i + 1) % N { 122 | d := room.last[i] 123 | if d > 0 { 124 | last = append(last, d) 125 | } 126 | } 127 | return last 128 | } 129 | -------------------------------------------------------------------------------- /games/migrate/dice/world.go: -------------------------------------------------------------------------------- 1 | package dice 2 | 3 | import ( 4 | "gofishing-game/service" 5 | "gofishing-game/service/roomutils" 6 | 7 | "github.com/guogeer/quasar/v2/config" 8 | "github.com/guogeer/quasar/v2/utils" 9 | ) 10 | 11 | func init() { 12 | service.AddWorld(&DiceWorld{}) 13 | } 14 | 15 | type DiceWorld struct{} 16 | 17 | func (w *DiceWorld) NewRoom(subId int) *roomutils.Room { 18 | r := &DiceRoom{} 19 | r.Room = roomutils.NewRoom(subId, r) 20 | 21 | var chips []int64 22 | config.Scan("config", "diceChips", "value", config.JSON(&chips)) 23 | // 定时同步 24 | utils.NewTimer(r.OnTime, syncTime) 25 | r.StartGame() 26 | return r.Room 27 | } 28 | 29 | func (w *DiceWorld) GetName() string { 30 | return "dice" 31 | } 32 | 33 | func (w *DiceWorld) NewPlayer() *service.Player { 34 | p := &DicePlayer{} 35 | p.Player = service.NewPlayer(p) 36 | return p.Player 37 | } 38 | -------------------------------------------------------------------------------- /games/migrate/doudizhu/handle.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "gofishing-game/service" 5 | 6 | "github.com/guogeer/quasar/v2/cmd" 7 | ) 8 | 9 | type Args struct { 10 | Cards []int `json:"cards,omitempty"` 11 | Choice int `json:"choice,omitempty"` 12 | Type int `json:"type,omitempty"` 13 | } 14 | 15 | func init() { 16 | cmd.BindFunc(Discard, (*Args)(nil), cmd.WithServer((*DoudizhuWorld)(nil).GetName())) 17 | cmd.BindFunc(Pass, (*Args)(nil), cmd.WithServer((*DoudizhuWorld)(nil).GetName())) 18 | cmd.BindFunc(Jiaodizhu, (*Args)(nil), cmd.WithServer((*DoudizhuWorld)(nil).GetName())) 19 | cmd.BindFunc(Jiaofen, (*Args)(nil), cmd.WithServer((*DoudizhuWorld)(nil).GetName())) 20 | cmd.BindFunc(Qiangdizhu, (*Args)(nil), cmd.WithServer((*DoudizhuWorld)(nil).GetName())) 21 | cmd.BindFunc(AutoPlay, (*Args)(nil), cmd.WithServer((*DoudizhuWorld)(nil).GetName())) 22 | } 23 | 24 | func GetPlayerByContext(ctx *cmd.Context) *DoudizhuPlayer { 25 | if p := service.GetGatewayPlayer(ctx.Ssid); p != nil { 26 | return p.GameAction.(*DoudizhuPlayer) 27 | } 28 | return nil 29 | } 30 | 31 | func Discard(ctx *cmd.Context, iArgs any) { 32 | args := iArgs.(*Args) 33 | ply := GetPlayerByContext(ctx) 34 | if ply == nil { 35 | 36 | return 37 | } 38 | ply.Discard(args.Cards) 39 | } 40 | 41 | func Pass(ctx *cmd.Context, iArgs any) { 42 | // args := iArgs.(*Args) 43 | ply := GetPlayerByContext(ctx) 44 | if ply == nil { 45 | 46 | return 47 | } 48 | ply.Pass() 49 | } 50 | 51 | func Jiaodizhu(ctx *cmd.Context, iArgs any) { 52 | args := iArgs.(*Args) 53 | ply := GetPlayerByContext(ctx) 54 | if ply == nil { 55 | 56 | return 57 | } 58 | ply.Jiaodizhu(args.Choice) 59 | } 60 | 61 | func Jiaofen(ctx *cmd.Context, iArgs any) { 62 | args := iArgs.(*Args) 63 | ply := GetPlayerByContext(ctx) 64 | if ply == nil { 65 | 66 | return 67 | } 68 | ply.Jiaofen(args.Choice) 69 | } 70 | 71 | func Qiangdizhu(ctx *cmd.Context, iArgs any) { 72 | args := iArgs.(*Args) 73 | ply := GetPlayerByContext(ctx) 74 | if ply == nil { 75 | 76 | return 77 | } 78 | ply.Qiangdizhu(args.Choice) 79 | } 80 | 81 | func AutoPlay(ctx *cmd.Context, iArgs any) { 82 | args := iArgs.(*Args) 83 | ply := GetPlayerByContext(ctx) 84 | if ply == nil { 85 | 86 | return 87 | } 88 | ply.SetAutoPlay(args.Type) 89 | } 90 | -------------------------------------------------------------------------------- /games/migrate/doudizhu/world.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "gofishing-game/games/migrate/internal/cardrule" 5 | "gofishing-game/internal/cardutils" 6 | "gofishing-game/service" 7 | "gofishing-game/service/roomutils" 8 | ) 9 | 10 | type DoudizhuWorld struct{} 11 | 12 | func init() { 13 | w := &DoudizhuWorld{} 14 | service.AddWorld(w) 15 | 16 | var cards = []int{0xf0, 0xf1} 17 | for color := 0; color < 4; color++ { 18 | for value := 0x02; value <= 0x0e; value++ { 19 | c := (color << 4) | value 20 | cards = append(cards, c) 21 | } 22 | } 23 | 24 | cardutils.AddCardSystem(w.GetName(), cards) 25 | } 26 | 27 | func (w *DoudizhuWorld) NewRoom(subId int) *roomutils.Room { 28 | r := &DoudizhuRoom{ 29 | helper: cardrule.NewDoudizhuHelper(w.GetName()), 30 | currentTimes: 1, 31 | } 32 | r.Room = roomutils.NewRoom(subId, r) 33 | r.SetPlay(OptZhadan3) 34 | r.SetNoPlay(OptZhadan4) 35 | r.SetNoPlay(OptZhadan5) 36 | 37 | r.SetPlay(OptQiangdizhu) 38 | r.SetNoPlay(OptJiaodizhu) 39 | r.SetNoPlay(OptJiaofen) 40 | 41 | return r.Room 42 | } 43 | 44 | func (w *DoudizhuWorld) GetName() string { 45 | return "ddz" 46 | } 47 | 48 | func (w *DoudizhuWorld) NewPlayer() *service.Player { 49 | p := &DoudizhuPlayer{} 50 | p.cards = make([]int, 1024) 51 | 52 | p.Player = service.NewPlayer(p) 53 | p.initGame() 54 | return p.Player 55 | } 56 | -------------------------------------------------------------------------------- /games/migrate/internal/cardcontrol/prizepool.go: -------------------------------------------------------------------------------- 1 | package cardcontrol 2 | 3 | import ( 4 | "fmt" 5 | "gofishing-game/service" 6 | 7 | "github.com/guogeer/quasar/v2/config" 8 | "github.com/guogeer/quasar/v2/utils/randutils" 9 | ) 10 | 11 | // 暗池 12 | type InvisiblePrizePool struct { 13 | subId int 14 | Cap int64 `json:"cap,omitempty"` 15 | Tax int64 `json:"tax,omitempty"` 16 | } 17 | 18 | func NewInvisiblePrizePool(subId int) *InvisiblePrizePool { 19 | pp := &InvisiblePrizePool{subId: subId} 20 | key := fmt.Sprintf("invisible_prize_pool_%d", subId) 21 | service.UpdateDict(key, pp) 22 | return pp 23 | } 24 | 25 | // n>0:表示玩家赢取 26 | func (pp *InvisiblePrizePool) Add(n int64) { 27 | value := -n 28 | percent, _ := config.Float("lottery", pp.subId, "invisiblePrizePoolPercent") 29 | 30 | var tax int64 31 | if value > 0 { 32 | tax = int64(float64(value) * percent / 100) 33 | value = value - tax 34 | } 35 | pp.Cap += value 36 | pp.Tax += tax 37 | } 38 | 39 | // -1:吐分 40 | // 0:正常 41 | // 1:吃分 42 | func (pp *InvisiblePrizePool) Check() int { 43 | subId := pp.subId 44 | line, _ := config.Int("lottery", subId, "warningLine") 45 | 46 | percent, _ := config.Float("lottery", subId, "prizePoolControlPercent") 47 | if !randutils.IsPercentNice(percent) { 48 | return 0 49 | } 50 | n := pp.Cap 51 | switch { 52 | case n <= -line: 53 | return -1 54 | case n >= line: 55 | return 1 56 | } 57 | return 0 58 | } 59 | 60 | // n>0:表示玩家赢取 61 | func (pp *InvisiblePrizePool) IsValid(n int64) bool { 62 | subId := pp.subId 63 | line, _ := config.Int("lottery", subId, "warningLine") 64 | 65 | cur := pp.Cap 66 | if n-cur >= line { 67 | return false 68 | } 69 | return true 70 | } 71 | 72 | // 奖池 73 | type PrizePool struct { 74 | Rank []RankUserInfo `json:"rank,omitempty"` 75 | subId int 76 | rankLen int 77 | Cap int64 `json:"cap,omitempty"` 78 | LastPrize int64 `json:"lastPrize,omitempty"` 79 | } 80 | 81 | func NewPrizePool(subId int) *PrizePool { 82 | pool := &PrizePool{ 83 | Rank: make([]RankUserInfo, 0, 10), 84 | rankLen: 3, 85 | subId: subId, 86 | } 87 | service.UpdateDict("lottery_prize_pool", pool) 88 | return pool 89 | } 90 | 91 | func (pool *PrizePool) Add(n int64) int64 { 92 | pool.Cap += n 93 | 94 | limit, _ := config.Int("lottery", pool.subId, "prizePoolLimit") 95 | if limit > 0 && pool.Cap > limit { 96 | pool.Cap = limit 97 | } 98 | return pool.Cap 99 | } 100 | 101 | func (pool *PrizePool) SetLastPrize(n int64) { 102 | pool.LastPrize = n 103 | } 104 | 105 | func (pool *PrizePool) GetLastPrize() int64 { 106 | return pool.LastPrize 107 | } 108 | 109 | func (pool *PrizePool) ClearRank() { 110 | if pool.Rank != nil { 111 | pool.Rank = pool.Rank[:0] 112 | } 113 | } 114 | 115 | func (pool *PrizePool) UpdateRank(user service.UserInfo, gold int64) { 116 | rankList := &RankList{top: pool.Rank, len: pool.rankLen} 117 | if rankList.Update(user, gold) != nil { 118 | pool.Rank = rankList.top 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /games/migrate/internal/cardcontrol/rank.go: -------------------------------------------------------------------------------- 1 | package cardcontrol 2 | 3 | import "gofishing-game/service" 4 | 5 | type RankUserInfo struct { 6 | service.UserInfo 7 | Gold int64 `json:"gold,omitempty"` 8 | Award string `json:"award,omitempty"` 9 | Prize int64 `json:"prize,omitempty"` 10 | } 11 | 12 | type RankList struct { 13 | top []RankUserInfo 14 | len int 15 | } 16 | 17 | func NewRankList(q []RankUserInfo, n int) *RankList { 18 | return &RankList{top: q, len: n} 19 | } 20 | 21 | func (lst *RankList) GetRank(uid int) int { 22 | for i, user := range lst.top { 23 | if user.Id == uid { 24 | return i 25 | } 26 | } 27 | return -1 28 | } 29 | 30 | func (lst *RankList) Top() []RankUserInfo { 31 | return lst.top 32 | } 33 | 34 | func (lst *RankList) Update(user service.UserInfo, gold int64) *RankUserInfo { 35 | if gold == 0 { 36 | return nil 37 | } 38 | 39 | rank := lst.top 40 | pos := len(rank) 41 | uid := user.Id 42 | // 如果玩家已经在排行榜中,先移除 43 | for k := 0; k < len(rank); k++ { 44 | if rank[k].Id == uid { 45 | pos = k 46 | break 47 | } 48 | } 49 | // 前移一位 50 | for k := pos; k+1 < len(rank); k++ { 51 | rank[k] = rank[k+1] 52 | } 53 | if n := len(rank); pos < n { 54 | rank = rank[:n-1] 55 | } 56 | 57 | pos = len(rank) 58 | // 从第一名开始遍历一个金币更少的 59 | for k := 0; k < len(rank); k++ { 60 | if rank[k].Gold < gold { 61 | pos = k 62 | break 63 | } 64 | } 65 | 66 | if len(rank) < lst.len { 67 | rank = append(rank, RankUserInfo{}) 68 | } 69 | // 后移一位 70 | for k := len(rank) - 2; k >= pos; k-- { 71 | rank[k+1] = rank[k] 72 | } 73 | if pos < len(rank) { 74 | rank[pos] = RankUserInfo{UserInfo: user} 75 | rank[pos].Gold = gold 76 | rank[pos].Prize = gold 77 | lst.top = rank 78 | // log.Debug("update rank list", lst.top) 79 | return &rank[pos] 80 | } 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /games/migrate/internal/cardcontrol/utils.go: -------------------------------------------------------------------------------- 1 | package cardcontrol 2 | 3 | import ( 4 | "math/rand" 5 | "sort" 6 | 7 | "github.com/guogeer/quasar/v2/utils/randutils" 8 | ) 9 | 10 | // ///////////////////////////////////////////////////////// 11 | // 庄家作弊 12 | func HelpDealer(data sort.Interface, percent float64) { 13 | var rank int 14 | for i := 0; i+1 < data.Len(); i++ { 15 | if randutils.IsPercentNice(percent) { 16 | rank++ 17 | } 18 | } 19 | sort.Sort(data) 20 | data.Swap(rank, data.Len()-1) 21 | for i := 0; i+1 < data.Len(); i++ { 22 | end := data.Len() - 1 - i 23 | t := rand.Intn(end) 24 | data.Swap(t, end-1) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /games/migrate/internal/cardrule/mahjong_test.go: -------------------------------------------------------------------------------- 1 | package cardrule 2 | 3 | import ( 4 | "gofishing-game/internal/cardutils" 5 | "testing" 6 | ) 7 | 8 | func TestMahjong(t *testing.T) { 9 | var cards []int 10 | for _, c := range []int{ 11 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 12 | 21, 22, 23, 24, 25, 26, 27, 28, 29, 13 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 14 | 100, 110, 120, 15 | } { 16 | cards = append(cards, c, c, c, c) 17 | } 18 | 19 | samples := []int{1, 1, 1, 2, 2, 2, 3, 3, 3, 21, 22, 23, 22, 22} 20 | // samples = []int{4, 5, 6, 7, 7, 7, 25, 26, 28, 45, 46, 47, 100, 100} 21 | // samples = []int{2, 3, 4, 22, 22, 26, 27, 28, 41, 42, 43, 100} 22 | // samples = []int{1, 4, 5, 7, 8, 8, 9, 41, 43, 43, 47} 23 | cardutils.AddCardSystem("mahjong", cards) 24 | 25 | /*cs := NewCardSet() 26 | cs.Shuffle() 27 | cs.MoveBack([]int{1, 2, 3, 4, 5, 2, 2, 2}) 28 | cs.MoveFront(1) 29 | c1 := cs.Deal() 30 | cs.MoveFront(2) 31 | c2 := cs.Deal() 32 | cs.MoveFront(1) 33 | c3 := cs.Deal() 34 | cs.MoveFront(3) 35 | t.Log(c1, c2, c3, cs.randCards) 36 | return 37 | */ 38 | 39 | cards = make([]int, MaxMahjongCard) 40 | for _, c := range samples { 41 | cards[c]++ 42 | } 43 | helper := &MahjongHelper{Qingyise: true, name: "mahjong"} 44 | // opts := helper.SplitN(cards, 30) 45 | // t.Log(opts) 46 | 47 | remainingCards := make([]int, 255) 48 | for _, c := range cardutils.GetCardSystem("mahjong").GetAllCards() { 49 | remainingCards[c] = 4 50 | } 51 | ctx := &Context{ 52 | ValidCards: remainingCards, 53 | } 54 | /* 55 | samples = []int{1, 1, 2, 2, 3, 3, 4, 4, 9, 21, 22, 23, 24} 56 | samples = []int{9, 8, 8, 7, 7, 7, 6, 5} 57 | samples = []int{3, 4, 5, 6, 7, 8, 44, 44, 46, 46, 49} 58 | samples = []int{4, 6, 7, 7, 9} 59 | samples = []int{2, 2, 3, 3, 4} 60 | samples = []int{2, 4, 4, 6, 7} 61 | samples = []int{3, 3, 26, 22, 23, 24, 24, 25} 62 | */ 63 | samples = []int{3, 3, 3, 4, 7} 64 | cards = make([]int, MaxMahjongCard) 65 | for _, c := range cardutils.GetCardSystem("mahjong").GetAllCards() { 66 | remainingCards[c] = 4 67 | } 68 | for _, c := range samples { 69 | cards[c]++ 70 | remainingCards[c]-- 71 | } 72 | remainingCards[46] = 0 73 | ctx.Cards = cards 74 | dc, w := helper.Weight(ctx) 75 | t.Log(dc, w) 76 | } 77 | -------------------------------------------------------------------------------- /games/migrate/internal/cardrule/sangong.go: -------------------------------------------------------------------------------- 1 | // 湖南、广东地区的三公玩法 2 | package cardrule 3 | 4 | // "github.com/guogeer/husky/log" 5 | 6 | // 0~9 0~9点数 7 | // 333 3个三 8 | // 100 三公 9 | 10 | type SangongHelper struct { 11 | } 12 | 13 | func NewSangongHelper() *SangongHelper { 14 | return &SangongHelper{} 15 | } 16 | 17 | func (helper *SangongHelper) Color(c int) int { 18 | return c >> 4 19 | } 20 | 21 | // return: 类型、JQK数量 22 | func (helper *SangongHelper) GetType(cards []int) (int, int) { 23 | var sum, jqk, card3 int 24 | for _, c := range cards { 25 | points := c & 0x0f 26 | switch points { 27 | case 11, 12, 13: // J、Q、K 28 | points = 10 29 | case 14: // A 30 | points = 1 31 | } 32 | sum += points 33 | 34 | if c&0x0f == 3 { 35 | card3++ 36 | } 37 | 38 | switch c & 0x0f { 39 | case 11, 12, 13: 40 | jqk++ 41 | } 42 | } 43 | if card3 == 3 { 44 | return 333, jqk 45 | } 46 | if jqk == 3 { 47 | return 100, jqk 48 | } 49 | return sum % 10, jqk 50 | } 51 | 52 | // K>Q>J...>2>A 53 | // 黑桃>红桃>梅花>方块 54 | func (helper *SangongHelper) LessCard(c1, c2 int) bool { 55 | if c1 == c2 { 56 | panic("unkown error") 57 | } 58 | 59 | scoreList := []int{ 60 | 0, 0, 61 | 2, 3, 4, 5, 6, 7, 8, 9, 10, // 2~10 62 | 11, 12, 13, // J、Q、K 63 | 1, // A 64 | } 65 | // 牌面相同,花色不同 66 | if c1&0x0f == c2&0x0f && c1 < c2 { 67 | return true 68 | } 69 | // 牌面小于 70 | if scoreList[c1&0x0f] < scoreList[c2&0x0f] { 71 | return true 72 | } 73 | return false 74 | } 75 | 76 | // 最大的牌 77 | func (helper *SangongHelper) GetMaxCard(cards []int) int { 78 | n := len(cards) 79 | 80 | c := cards[0] 81 | for i := 1; i < n; i++ { 82 | if helper.LessCard(c, cards[i]) { 83 | // log.Debug(c, cards[i]) 84 | c = cards[i] 85 | } 86 | } 87 | return c 88 | } 89 | 90 | func (helper *SangongHelper) Less(first, second []int) bool { 91 | typ1, jqk1 := helper.GetType(first) 92 | typ2, jqk2 := helper.GetType(second) 93 | // log.Debug(typ1, jqk1) 94 | // log.Debug(typ2, jqk2) 95 | if typ1 != typ2 { 96 | return typ1 < typ2 97 | } 98 | if jqk1 != jqk2 { 99 | return jqk1 < jqk2 100 | } 101 | return helper.LessMaxCard(first, second) 102 | } 103 | 104 | // 比较最大的牌 105 | func (helper *SangongHelper) LessMaxCard(first, second []int) bool { 106 | // sort 107 | max1 := helper.GetMaxCard(first) 108 | max2 := helper.GetMaxCard(second) 109 | // log.Debug(max1, max2) 110 | return helper.LessCard(max1, max2) 111 | } 112 | -------------------------------------------------------------------------------- /games/migrate/internal/cardrule/texas.go: -------------------------------------------------------------------------------- 1 | package cardrule 2 | 3 | // 2017-8-21 4 | // 德州扑克牌型 5 | // 同花大顺(Royal Flush):最高为Ace(一点)的同花顺。 6 | // 同花顺(Straight Flush):同一花色,顺序的牌。 7 | // 四条(Four of a Kind):有四张同一点数的牌。 8 | // 葫芦(Fullhouse):三张同一点数的牌,加一对其他点数的牌。 9 | // 同花(Flush):五张同一花色的牌。 10 | // 顺子(Straight):五张顺连的牌。 11 | // 三条(Three of a kind):有三张同一点数的牌。 12 | // 两对(Two Pairs):两张相同点数的牌,加另外两张相同点数的牌。 13 | // 一对(One Pair):两张相同点数的牌。 14 | // 高牌(High Card):不符合上面任何一种牌型的牌型,由单牌且不连续不同花的组成,以点数决定大小。 15 | 16 | // "github.com/guogeer/husky/log" 17 | 18 | const ( 19 | _ = iota 20 | TexasHighCard // 高牌 21 | TexasOnePair // 对子 22 | TexasTwoPair // 两队 23 | TexasThreeOfAKind // 三条 24 | TexasStraight // 顺子 25 | TexasFlush // 同花 26 | TexasFullHouse // 葫芦 27 | TexasFourOfAKind // 四条 28 | TexasStraightFlush // 同花顺 29 | TexasRoyalFlush // 皇家同花顺 30 | TexasTypeAll 31 | ) 32 | 33 | type TexasHelper struct { 34 | size int 35 | } 36 | 37 | func NewTexasHelper() *TexasHelper { 38 | return &TexasHelper{size: 5} 39 | } 40 | 41 | func (helper *TexasHelper) Size() int { 42 | return helper.size 43 | } 44 | 45 | func (helper *TexasHelper) Value(c int) int { 46 | return c & 0x0f 47 | } 48 | 49 | func (helper *TexasHelper) Color(c int) int { 50 | return c >> 4 51 | } 52 | 53 | func (helper *TexasHelper) GetType(cards []int) (int, []int) { 54 | var color int 55 | var values [32]int 56 | for _, c := range cards { 57 | v := helper.Value(c) 58 | values[v]++ 59 | 60 | color |= 1 << uint(helper.Color(c)) 61 | } 62 | 63 | var pairs, kind, straight int 64 | for v, n := range values { 65 | if n == 2 { 66 | pairs++ 67 | } 68 | if kind < n { 69 | kind = n 70 | } 71 | if straight == 0 && n > 0 { 72 | straight = v 73 | } 74 | } 75 | 76 | size := helper.Size() 77 | for v := straight; v < straight+size; v++ { 78 | if values[v] != 1 { 79 | straight = 0 80 | break 81 | } 82 | } 83 | if straight > 0 { 84 | straight += len(cards) - 1 85 | } else { 86 | // A2345 87 | straight = 0x5 88 | for _, v := range []int{0x2, 0x3, 0x4, 0x5, 0xe} { 89 | if values[v] != 1 { 90 | straight = 0 91 | } 92 | } 93 | } 94 | 95 | if straight > 0 { 96 | if color&(color-1) == 0 { 97 | if straight == 0x0e { 98 | return TexasRoyalFlush, []int{straight} 99 | } 100 | return TexasStraightFlush, []int{straight} 101 | } 102 | return TexasStraight, []int{straight} 103 | } 104 | 105 | rank := make([]int, 0, 8) 106 | for n := len(cards); n > 0; n-- { 107 | for v := len(values) - 1; v > 0; v-- { 108 | if values[v] == n { 109 | rank = append(rank, v) 110 | } 111 | } 112 | } 113 | 114 | if color&(color-1) == 0 { 115 | return TexasFlush, rank 116 | } 117 | if kind == 4 { 118 | return TexasFourOfAKind, rank 119 | } 120 | if kind == 3 && pairs == 1 { 121 | return TexasFullHouse, rank 122 | } 123 | if kind == 3 && pairs == 0 { 124 | return TexasThreeOfAKind, rank 125 | } 126 | if pairs == 2 { 127 | return TexasTwoPair, rank 128 | } 129 | if pairs == 1 { 130 | return TexasOnePair, rank 131 | } 132 | return TexasHighCard, rank 133 | } 134 | 135 | func (helper *TexasHelper) Equal(first, second []int) bool { 136 | return !helper.Less(first, second) && 137 | !helper.Less(second, first) 138 | } 139 | 140 | func (helper *TexasHelper) Less(first, second []int) bool { 141 | typ1, rank1 := helper.GetType(first) 142 | typ2, rank2 := helper.GetType(second) 143 | // log.Debug(typ1, rank1) 144 | // log.Debug(typ2, rank2) 145 | if typ1 == typ2 { 146 | for i := 0; i < len(rank1); i++ { 147 | if rank1[i] != rank2[i] { 148 | return rank1[i] < rank2[i] 149 | } 150 | } 151 | return false 152 | } 153 | return typ1 < typ2 154 | } 155 | 156 | func (helper *TexasHelper) Match(cards []int) []int { 157 | size := helper.Size() 158 | ans := make([]int, size) 159 | sample := make([]int, size) 160 | for bitMap := 0; bitMap < 1< 0 { 169 | sample = append(sample, cards[k]) 170 | } 171 | } 172 | if ans[0] == 0 || helper.Less(ans, sample) { 173 | copy(ans, sample) 174 | } 175 | } 176 | } 177 | return ans 178 | } 179 | -------------------------------------------------------------------------------- /games/migrate/internal/cardrule/zhajinhua.go: -------------------------------------------------------------------------------- 1 | package cardrule 2 | 3 | import ( 4 | "gofishing-game/internal/cardutils" 5 | "math/rand" 6 | ) 7 | 8 | const ( 9 | _ = iota 10 | ZhajinhuaSanpai // 散牌 11 | ZhajinhuaDuizi // 对子 12 | ZhajinhuaShunzi // 顺子 13 | ZhajinhuaTonghua // 金花 14 | ZhajinhuaTonghuashun // 顺金 15 | ZhajinhuaBaozi // 豹子 16 | ZhajinhuaAAA // AAA 17 | ZhajinhuaTypeAll 18 | ) 19 | 20 | type ZhajinhuaHelper struct { 21 | name string 22 | size int 23 | options map[string]bool 24 | } 25 | 26 | func NewZhajinhuaHelper(name string) *ZhajinhuaHelper { 27 | return &ZhajinhuaHelper{ 28 | name: name, 29 | size: 3, 30 | options: make(map[string]bool), 31 | } 32 | } 33 | 34 | func (helper *ZhajinhuaHelper) SetOption(option string) { 35 | helper.options[option] = true 36 | } 37 | 38 | func (helper *ZhajinhuaHelper) Size() int { 39 | return helper.size 40 | } 41 | 42 | func (helper *ZhajinhuaHelper) Color(c int) int { 43 | return c >> 4 44 | } 45 | 46 | func (helper *ZhajinhuaHelper) Value(c int) int { 47 | return c & 0x0f 48 | } 49 | 50 | func (helper *ZhajinhuaHelper) GetType(cards []int) (int, []int) { 51 | var color int 52 | var values [32]int 53 | for _, c := range cards { 54 | v := helper.Value(c) 55 | values[v]++ 56 | color |= 1 << uint(helper.Color(c)) 57 | } 58 | 59 | var kind, straight int 60 | for v, n := range values { 61 | if kind < n { 62 | kind = n 63 | } 64 | if straight == 0 && n > 0 { 65 | straight = v 66 | } 67 | } 68 | for i := straight; i < straight+len(cards); i++ { 69 | if values[i] != 1 { 70 | straight = 0 71 | } 72 | } 73 | if straight > 0 { 74 | straight += len(cards) - 1 75 | } else { 76 | // A23 77 | straight = 0x3 78 | for _, v := range []int{0xe, 0x2, 0x3} { 79 | if values[v] != 1 { 80 | straight = 0 81 | } 82 | } 83 | } 84 | 85 | rank := make([]int, 0, 4) 86 | if straight > 0 { 87 | rank = append(rank, straight) 88 | } else { 89 | for n := len(cards); n > 0; n-- { 90 | for v := len(values) - 1; v > 0; v-- { 91 | if values[v] == n { 92 | rank = append(rank, v) 93 | } 94 | } 95 | } 96 | } 97 | 98 | if kind == 3 { 99 | // AAA 100 | if _, ok := helper.options["AAA"]; ok && helper.Value(cards[0]) == 0x0e { 101 | return ZhajinhuaAAA, rank 102 | } 103 | return ZhajinhuaBaozi, rank 104 | } 105 | if kind == 2 { 106 | return ZhajinhuaDuizi, rank 107 | } 108 | 109 | isSameColor := color&(color-1) == 0 110 | if isSameColor && straight > 0 { 111 | return ZhajinhuaTonghuashun, rank 112 | } 113 | if straight > 0 { 114 | return ZhajinhuaShunzi, rank 115 | } 116 | if isSameColor { 117 | return ZhajinhuaTonghua, rank 118 | } 119 | return ZhajinhuaSanpai, rank 120 | } 121 | 122 | func (helper *ZhajinhuaHelper) Less(first, second []int) bool { 123 | typ1, rank1 := helper.GetType(first) 124 | typ2, rank2 := helper.GetType(second) 125 | // log.Debug(typ1, rank1, typ2, rank2) 126 | if typ1 == typ2 { 127 | for i := 0; i < len(rank1); i++ { 128 | if rank1[i] != rank2[i] { 129 | return rank1[i] < rank2[i] 130 | } 131 | } 132 | return false 133 | } 134 | return typ1 < typ2 135 | } 136 | 137 | // 作弊,生成指定的牌型 138 | func (helper *ZhajinhuaHelper) Cheat(typ int, table []int) []int { 139 | validCards := make([]int, 0, 64) 140 | resultSet := make([]int, 0, 1024) 141 | for _, c := range cardutils.GetCardSystem(helper.name).GetAllCards() { 142 | for i := 0; i < table[c]; i++ { 143 | validCards = append(validCards, c) 144 | } 145 | } 146 | 147 | for i := 0; i < len(validCards); i++ { 148 | for j := i + 1; j < len(validCards); j++ { 149 | for k := j + 1; k < len(validCards); k++ { 150 | c1, c2, c3 := validCards[i], validCards[j], validCards[k] 151 | if typ1, _ := helper.GetType([]int{c1, c2, c3}); typ == typ1 { 152 | h := (c1 << 16) | (c2 << 8) | c3 153 | resultSet = append(resultSet, h) 154 | } 155 | } 156 | } 157 | } 158 | if len(resultSet) == 0 { 159 | return nil 160 | } 161 | h := resultSet[rand.Intn(len(resultSet))] 162 | return []int{h >> 16, h >> 8 & 0xff, h & 0xff} 163 | } 164 | -------------------------------------------------------------------------------- /games/migrate/lottery/README: -------------------------------------------------------------------------------- 1 | 各种小游戏 2 | 1、水果机 3 | 2、骰子场 4 | 3、二八杠 5 | 4、百人牛牛 6 | 5、百人三张 7 | 6、水果机 8 | 7、色子2 9 | -------------------------------------------------------------------------------- /games/migrate/lottery/bairenniuniu.go: -------------------------------------------------------------------------------- 1 | package lottery 2 | 3 | import ( 4 | "container/list" 5 | "gofishing-game/games/migrate/internal/cardrule" 6 | "gofishing-game/internal/cardutils" 7 | "gofishing-game/service" 8 | "gofishing-game/service/roomutils" 9 | "math/rand" 10 | 11 | "github.com/guogeer/quasar/v2/log" 12 | "github.com/guogeer/quasar/v2/utils" 13 | ) 14 | 15 | var gNiuNiuHelper = cardrule.NewNiuNiuHelper((*bairenniuniuWorld)(nil).GetName()) 16 | var gNiuNiuMultiples = []int{1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 17 | 0, // 五小牛 18 | 5, // 炸弹牛 19 | 4, // 五花牛 20 | 4, // 四花牛 21 | } 22 | 23 | func init() { 24 | helper := gNiuNiuHelper 25 | helper.SetOption(cardrule.NNZhaDanNiu) 26 | helper.SetOption(cardrule.NNWuHuaNiu) 27 | helper.SetOption(cardrule.NNSiHuaNiu) 28 | 29 | var cards []int 30 | for color := 0; color < 4; color++ { 31 | for value := 2; value <= 14; value++ { 32 | c := (color << 4) | value 33 | cards = append(cards, c) 34 | } 35 | } 36 | 37 | w := &bairenniuniuWorld{} 38 | cardutils.AddCardSystem(w.GetName(), cards) 39 | service.AddWorld(w) 40 | AddHandlers(w.GetName()) 41 | } 42 | 43 | type bairenniuniuHelper struct{} 44 | 45 | func (h bairenniuniuHelper) Less(fromCards, toCards []int) bool { 46 | helper := gNiuNiuHelper 47 | return helper.Less(fromCards, toCards) 48 | } 49 | 50 | func (h *bairenniuniuHelper) count(cards []int) (int, int) { 51 | helper := gNiuNiuHelper 52 | typ, _ := helper.Weight(cards) 53 | multiples := gNiuNiuMultiples[typ] 54 | return typ, multiples 55 | } 56 | 57 | type bairenniuniu struct { 58 | room *lotteryRoom 59 | } 60 | 61 | func (ent *bairenniuniu) OnEnter(player *lotteryPlayer) { 62 | } 63 | 64 | func (ent *bairenniuniu) StartDealCard() { 65 | } 66 | 67 | func (ent *bairenniuniu) winPrizePool(cards []int) float64 { 68 | return 0.0 69 | } 70 | 71 | func (ent *bairenniuniu) Cheat(multiples int) []int { 72 | room := ent.room 73 | helper := gNiuNiuHelper 74 | allowTypes := make([]int, 0, 8) 75 | log.Debug("cheat multiples", multiples) 76 | 77 | values := sortArrayValues(gNiuNiuMultiples) 78 | for i := len(values) - 1; i > 0; i-- { 79 | current := values[i] 80 | allowTypes = allowTypes[:0] 81 | for t, m := range gNiuNiuMultiples { 82 | if current <= multiples && current == m { 83 | allowTypes = append(allowTypes, t) 84 | } 85 | } 86 | if len(allowTypes) > 0 { 87 | t := allowTypes[rand.Intn(len(allowTypes))] 88 | table := room.CardSet().GetRemainingCards() 89 | if cards := helper.Cheat(t, table); cards != nil { 90 | room.CardSet().Cheat(cards...) 91 | return cards 92 | } 93 | } 94 | } 95 | 96 | log.Warnf("bairenniuniu cheat cards by multiples %d fail", multiples) 97 | return nil 98 | } 99 | 100 | type bairenniuniuWorld struct{} 101 | 102 | func (w *bairenniuniuWorld) NewRoom(subId int) *roomutils.Room { 103 | room := &lotteryRoom{ 104 | robSeat: roomutils.NoSeat, 105 | betAreas: make([]int64, 4), 106 | dealerQueue: list.New(), 107 | helper: &bairenniuniuHelper{}, 108 | multipleSamples: []int{0, 0, 330000, 870000, 980000, 1000000}, 109 | } 110 | room.Room = roomutils.NewRoom(subId, room) 111 | 112 | room.lotteryGame = &bairenniuniu{ 113 | room: room, 114 | } 115 | 116 | for i := 0; i < len(room.last); i++ { 117 | room.last[i] = -1 118 | } 119 | deals := make([]lotteryDeal, 5) 120 | for i := range deals { 121 | deals[i].Cards = make([]int, 5) 122 | } 123 | room.deals = deals 124 | utils.NewTimer(room.OnTime, syncTime) 125 | return room.Room 126 | } 127 | 128 | func (w *bairenniuniuWorld) GetName() string { 129 | return "brnn" 130 | } 131 | 132 | func (w *bairenniuniuWorld) NewPlayer() *service.Player { 133 | p := &lotteryPlayer{} 134 | p.Player = service.NewPlayer(p) 135 | p.betAreas = make([]int64, 4) 136 | return p.Player 137 | } 138 | -------------------------------------------------------------------------------- /games/migrate/lottery/bairenzhajinhua.go: -------------------------------------------------------------------------------- 1 | package lottery 2 | 3 | // 扎金花押注场 4 | import ( 5 | "container/list" 6 | "gofishing-game/games/migrate/internal/cardrule" 7 | "gofishing-game/internal/cardutils" 8 | "gofishing-game/service" 9 | "gofishing-game/service/roomutils" 10 | "math/rand" 11 | 12 | "github.com/guogeer/quasar/v2/config" 13 | "github.com/guogeer/quasar/v2/log" 14 | "github.com/guogeer/quasar/v2/utils" 15 | "github.com/guogeer/quasar/v2/utils/randutils" 16 | ) 17 | 18 | var gZhajinhuaHelper = cardrule.NewZhajinhuaHelper((*bairenniuniuWorld)(nil).GetName()) 19 | var gZhajinhuaMultiples = []int{ 20 | 0, 21 | 1, // 散牌 22 | 1, // 对子 23 | 2, // 顺子 24 | 3, // 金花 25 | 4, // 顺金 26 | 5, // 豹子 27 | } 28 | 29 | func init() { 30 | var cards []int 31 | for color := 0; color < 4; color++ { 32 | for value := 2; value <= 14; value++ { 33 | c := (color << 4) | value 34 | cards = append(cards, c) 35 | } 36 | } 37 | 38 | w := &BairenzhajinhuaWorld{} 39 | cardutils.AddCardSystem(w.GetName(), cards) 40 | service.AddWorld(w) 41 | AddHandlers(w.GetName()) 42 | } 43 | 44 | type bairenzhajinhuaHelper struct { 45 | } 46 | 47 | func (h *bairenzhajinhuaHelper) count(cards []int) (int, int) { 48 | helper := gZhajinhuaHelper 49 | typ, _ := helper.GetType(cards) 50 | return typ, gZhajinhuaMultiples[typ] 51 | } 52 | 53 | func (h bairenzhajinhuaHelper) Less(fromCards, toCards []int) bool { 54 | helper := gZhajinhuaHelper 55 | return helper.Less(fromCards[:], toCards[:]) 56 | } 57 | 58 | type bairenzhajinhua struct { 59 | room *lotteryRoom 60 | } 61 | 62 | func (ent *bairenzhajinhua) OnEnter(player *lotteryPlayer) { 63 | } 64 | 65 | func (ent *bairenzhajinhua) winPrizePool(cards []int) float64 { 66 | return 0.0 67 | } 68 | 69 | func (ent *bairenzhajinhua) StartDealCard() { 70 | room := ent.room 71 | helper := gZhajinhuaHelper 72 | 73 | var samples []int 74 | config.Scan("lottery", room.SubId, "cardSamples", &samples) 75 | if len(samples) > 0 { 76 | start := rand.Intn(len(room.deals)) 77 | for i := range room.deals { 78 | k := (start + i) % len(room.deals) 79 | 80 | cards := room.deals[k].Cards 81 | typ := randutils.Index(samples) 82 | // log.Debug("cheat type", typ) 83 | table := room.CardSet().GetRemainingCards() 84 | if a := helper.Cheat(typ, table); a != nil { 85 | copy(cards, a) 86 | room.CardSet().Cheat(a...) 87 | } 88 | } 89 | } 90 | } 91 | 92 | func (ent *bairenzhajinhua) Cheat(multiples int) []int { 93 | room := ent.room 94 | helper := gZhajinhuaHelper 95 | allowTypes := make([]int, 0, 8) 96 | log.Debug("cheat multiples", multiples) 97 | 98 | values := sortArrayValues(gZhajinhuaMultiples) 99 | for i := len(values) - 1; i > 0; i-- { 100 | current := values[i] 101 | allowTypes = allowTypes[:0] 102 | for t, m := range gZhajinhuaMultiples { 103 | if current <= multiples && current == m { 104 | allowTypes = append(allowTypes, t) 105 | } 106 | } 107 | if len(allowTypes) > 0 { 108 | t := allowTypes[rand.Intn(len(allowTypes))] 109 | table := room.CardSet().GetRemainingCards() 110 | if cards := helper.Cheat(t, table); cards != nil { 111 | room.CardSet().Cheat(cards...) 112 | log.Debug("current type", t) 113 | cardutils.Print(cards) 114 | return cards 115 | } 116 | } 117 | } 118 | log.Warnf("bairenzhajinhua cheat cards by multiples %d fail", multiples) 119 | return nil 120 | } 121 | 122 | type BairenzhajinhuaWorld struct{} 123 | 124 | func (w *BairenzhajinhuaWorld) NewRoom(subId int) *roomutils.Room { 125 | room := &lotteryRoom{ 126 | robSeat: roomutils.NoSeat, 127 | betAreas: make([]int64, 4), 128 | dealerQueue: list.New(), 129 | helper: &bairenzhajinhuaHelper{}, 130 | multipleSamples: []int{0, 0, 310000, 790000, 990000, 1000000}, 131 | } 132 | room.Room = roomutils.NewRoom(subId, room) 133 | room.lotteryGame = &bairenzhajinhua{ 134 | room: room, 135 | } 136 | 137 | for i := 0; i < len(room.last); i++ { 138 | room.last[i] = -1 139 | } 140 | deals := make([]lotteryDeal, 5) 141 | for i := range deals { 142 | deals[i].Cards = make([]int, 3) 143 | } 144 | room.deals = deals 145 | 146 | // room.StartGame() 147 | // 定时同步 148 | utils.NewTimer(room.OnTime, syncTime) 149 | return room.Room 150 | } 151 | 152 | func (w *BairenzhajinhuaWorld) GetName() string { 153 | return "brzjh" 154 | } 155 | 156 | func (w *BairenzhajinhuaWorld) NewPlayer() *service.Player { 157 | p := &lotteryPlayer{} 158 | p.Player = service.NewPlayer(p) 159 | p.betAreas = make([]int64, 4) 160 | return p.Player 161 | } 162 | -------------------------------------------------------------------------------- /games/migrate/lottery/handle.go: -------------------------------------------------------------------------------- 1 | package lottery 2 | 3 | import ( 4 | "gofishing-game/internal/errcode" 5 | "gofishing-game/service" 6 | 7 | "github.com/guogeer/quasar/v2/cmd" 8 | "github.com/guogeer/quasar/v2/config" 9 | "github.com/guogeer/quasar/v2/log" 10 | ) 11 | 12 | type lotteryArgs struct { 13 | Uid int `json:"uid,omitempty"` 14 | Type int `json:"type,omitempty"` 15 | Message string `json:"message,omitempty"` 16 | Token string `json:"token,omitempty"` 17 | Area int `json:"area,omitempty"` 18 | Gold int64 `json:"gold,omitempty"` 19 | PageNum int `json:"pageNum,omitempty"` 20 | SeatIndex int `json:"seatIndex,omitempty"` 21 | } 22 | 23 | type Config struct { 24 | } 25 | 26 | func AddHandlers(name string) { 27 | cmd.BindFunc(Bet, (*lotteryArgs)(nil), cmd.WithServer(name)) 28 | cmd.BindFunc(GetLastHistory, (*lotteryArgs)(nil), cmd.WithServer(name)) 29 | cmd.BindFunc(ApplyDealer, (*lotteryArgs)(nil), cmd.WithServer(name)) 30 | cmd.BindFunc(CancelDealer, (*lotteryArgs)(nil), cmd.WithServer(name)) 31 | cmd.BindFunc(GetDealerQueue, (*lotteryArgs)(nil), cmd.WithServer(name)) 32 | cmd.BindFunc(ChangeDealerGold, (*lotteryArgs)(nil), cmd.WithServer(name)) 33 | 34 | cmd.BindFunc(Console_WhosYourDaddy, (*lotteryArgs)(nil), cmd.WithServer(name)) 35 | } 36 | 37 | func GetPlayerByContext(ctx *cmd.Context) *lotteryPlayer { 38 | if ply := service.GetGatewayPlayer(ctx.Ssid); ply != nil { 39 | return ply.GameAction.(*lotteryPlayer) 40 | } 41 | return nil 42 | } 43 | 44 | func Bet(ctx *cmd.Context, iArgs interface{}) { 45 | args := iArgs.(*lotteryArgs) 46 | ply := GetPlayerByContext(ctx) 47 | if ply == nil { 48 | return 49 | } 50 | ply.Bet(args.Area, args.Gold) 51 | } 52 | 53 | func GetLastHistory(ctx *cmd.Context, iArgs interface{}) { 54 | args := iArgs.(*lotteryArgs) 55 | ply := GetPlayerByContext(ctx) 56 | if ply == nil { 57 | return 58 | } 59 | ply.GetLastHistory(args.PageNum) 60 | } 61 | 62 | func Chat(ctx *cmd.Context, iArgs interface{}) { 63 | args := iArgs.(*lotteryArgs) 64 | ply := GetPlayerByContext(ctx) 65 | if ply == nil { 66 | return 67 | } 68 | ply.Chat(args.Type, args.Message) 69 | } 70 | 71 | func ApplyDealer(ctx *cmd.Context, iArgs interface{}) { 72 | // args := iArgs.(*Args) 73 | ply := GetPlayerByContext(ctx) 74 | if ply == nil { 75 | return 76 | } 77 | ply.ApplyDealer() 78 | } 79 | 80 | func CancelDealer(ctx *cmd.Context, iArgs interface{}) { 81 | // args := iArgs.(*Args) 82 | ply := GetPlayerByContext(ctx) 83 | if ply == nil { 84 | return 85 | } 86 | ply.CancelDealer() 87 | } 88 | 89 | func GetDealerQueue(ctx *cmd.Context, iArgs interface{}) { 90 | // args := iArgs.(*Args) 91 | ply := GetPlayerByContext(ctx) 92 | if ply == nil { 93 | return 94 | } 95 | 96 | ply.WriteJSON("getDealerQueue", ply.dealerQueue()) 97 | } 98 | 99 | func Console_WhosYourDaddy(ctx *cmd.Context, iArgs interface{}) { 100 | log.Debug("console whos you daddy") 101 | isNextTurnSystemControl = true 102 | } 103 | 104 | // 修改上庄金币 105 | func ChangeDealerGold(ctx *cmd.Context, iArgs interface{}) { 106 | args := iArgs.(*lotteryArgs) 107 | ply := GetPlayerByContext(ctx) 108 | if ply == nil { 109 | return 110 | } 111 | 112 | var e errcode.Error 113 | room := ply.Room() 114 | minDealerGold, _ := config.Int("lottery", room.SubId, "minDealerGold") 115 | if args.Gold < minDealerGold { 116 | e = errcode.Retry 117 | } 118 | ply.WriteErr("changeDealerGold", e, map[string]any{"gold": args.Gold}) 119 | if e != nil { 120 | return 121 | } 122 | ply.dealerLimitGold = args.Gold 123 | } 124 | -------------------------------------------------------------------------------- /games/migrate/niuniu/handle.go: -------------------------------------------------------------------------------- 1 | package niuniu 2 | 3 | import ( 4 | "gofishing-game/service" 5 | 6 | "github.com/guogeer/quasar/v2/cmd" 7 | "github.com/guogeer/quasar/v2/utils" 8 | ) 9 | 10 | type niuniuArgs struct { 11 | Ans bool 12 | Times int 13 | TriCards [3]int 14 | } 15 | 16 | func init() { 17 | cmd.BindFunc(ChooseTriCards, (*niuniuArgs)(nil), cmd.WithServer((*NiuNiuWorld)(nil).GetName())) 18 | cmd.BindFunc(Bet, (*niuniuArgs)(nil), cmd.WithServer((*NiuNiuWorld)(nil).GetName())) 19 | cmd.BindFunc(ChooseDealer, (*niuniuArgs)(nil), cmd.WithServer((*NiuNiuWorld)(nil).GetName())) 20 | cmd.BindFunc(DoubleAndRob, (*niuniuArgs)(nil), cmd.WithServer((*NiuNiuWorld)(nil).GetName())) 21 | cmd.BindFunc(SitDown, (*niuniuArgs)(nil), cmd.WithServer((*NiuNiuWorld)(nil).GetName())) 22 | cmd.BindFunc(EndGame, (*niuniuArgs)(nil), cmd.WithServer((*NiuNiuWorld)(nil).GetName())) 23 | cmd.BindFunc(SetAutoPlay, (*niuniuArgs)(nil), cmd.WithServer((*NiuNiuWorld)(nil).GetName())) 24 | } 25 | 26 | func GetPlayerByContext(ctx *cmd.Context) *NiuNiuPlayer { 27 | if p := service.GetGatewayPlayer(ctx.Ssid); p != nil { 28 | return p.GameAction.(*NiuNiuPlayer) 29 | } 30 | return nil 31 | } 32 | 33 | func ChooseTriCards(ctx *cmd.Context, iArgs interface{}) { 34 | args := iArgs.(*niuniuArgs) 35 | ply := GetPlayerByContext(ctx) 36 | if ply == nil { 37 | 38 | return 39 | } 40 | ply.ChooseTriCards(args.TriCards) 41 | } 42 | 43 | func Bet(ctx *cmd.Context, iArgs interface{}) { 44 | args := iArgs.(*niuniuArgs) 45 | ply := GetPlayerByContext(ctx) 46 | if ply == nil { 47 | 48 | return 49 | } 50 | ply.Bet(args.Times) 51 | } 52 | 53 | func ChooseDealer(ctx *cmd.Context, iArgs interface{}) { 54 | args := iArgs.(*niuniuArgs) 55 | ply := GetPlayerByContext(ctx) 56 | if ply == nil { 57 | 58 | return 59 | } 60 | ply.ChooseDealer(args.Ans) 61 | } 62 | 63 | func DoubleAndRob(ctx *cmd.Context, iArgs interface{}) { 64 | args := iArgs.(*niuniuArgs) 65 | ply := GetPlayerByContext(ctx) 66 | if ply == nil { 67 | 68 | return 69 | } 70 | ply.DoubleAndRob(args.Times) 71 | } 72 | 73 | func SitDown(ctx *cmd.Context, iArgs interface{}) { 74 | // args := iArgs.(*Args) 75 | ply := GetPlayerByContext(ctx) 76 | if ply == nil { 77 | 78 | return 79 | } 80 | room := ply.Room() 81 | seatId := room.GetEmptySeat() 82 | ply.SitDown(seatId) 83 | } 84 | 85 | func EndGame(ctx *cmd.Context, iArgs interface{}) { 86 | // args := iArgs.(*Args) 87 | ply := GetPlayerByContext(ctx) 88 | if ply == nil { 89 | 90 | return 91 | } 92 | ply.EndGame() 93 | } 94 | 95 | func SetAutoPlay(ctx *cmd.Context, iArgs interface{}) { 96 | args := iArgs.(*niuniuArgs) 97 | ply := GetPlayerByContext(ctx) 98 | if ply == nil { 99 | 100 | return 101 | } 102 | chips := ply.Chips() 103 | if args.Times != 0 && utils.InArray(chips, args.Times) == 0 { 104 | return 105 | } 106 | ply.autoTimes = args.Times 107 | ply.WriteJSON("setAutoPlay", map[string]any{"times": ply.autoTimes}) 108 | } 109 | -------------------------------------------------------------------------------- /games/migrate/niuniu/world.go: -------------------------------------------------------------------------------- 1 | package niuniu 2 | 3 | import ( 4 | "gofishing-game/games/migrate/internal/cardrule" 5 | "gofishing-game/internal/cardutils" 6 | "gofishing-game/service" 7 | "gofishing-game/service/roomutils" 8 | "strings" 9 | "time" 10 | 11 | "github.com/guogeer/quasar/v2/config" 12 | ) 13 | 14 | type NiuNiuWorld struct{} 15 | 16 | func init() { 17 | w := &NiuNiuWorld{} 18 | service.AddWorld(w) 19 | 20 | var cards []int 21 | for color := 0; color < 4; color++ { 22 | for value := 2; value <= 14; value++ { 23 | c := (color << 4) | value 24 | cards = append(cards, c) 25 | } 26 | } 27 | 28 | cardutils.AddCardSystem(w.GetName(), cards) 29 | } 30 | 31 | func (w *NiuNiuWorld) NewRoom(subId int) *roomutils.Room { 32 | r := &NiuNiuRoom{ 33 | helper: cardrule.NewNiuNiuHelper(w.GetName()), 34 | } 35 | r.Room = roomutils.NewRoom(subId, r) 36 | 37 | r.SetFreeDuration(8 * time.Second) 38 | 39 | r.SetPlay(OptNiuNiuShangZhuang) 40 | r.SetNoPlay(OptGuDingShangZhuang) 41 | r.SetNoPlay(OptZiYouShangZhuang) 42 | r.SetNoPlay(OptMingPaiShangZhuang) 43 | r.SetNoPlay(OptTongBiNiuNiu) 44 | 45 | r.SetNoPlay(OptWuXiaoNiu) 46 | r.SetNoPlay(OptZhaDanNiu) 47 | r.SetNoPlay(OptWuHuaNiu) 48 | 49 | r.SetPlay(OptFanBeiGuiZe1) 50 | r.SetNoPlay(OptFanBeiGuiZe2) 51 | 52 | r.SetNoPlay(OptDiZhu1_2) 53 | r.SetNoPlay(OptDiZhu2_4) 54 | r.SetNoPlay(OptDiZhu4_8) 55 | r.SetPlay(OptDiZhu1_2_3_4_5) 56 | 57 | r.SetPlay(OptDiZhu1) 58 | r.SetNoPlay(OptDiZhu2) 59 | r.SetNoPlay(OptDiZhu4) 60 | 61 | r.SetPlay(OptZuiDaQiangZhuang1) 62 | r.SetNoPlay(OptZuiDaQiangZhuang2) 63 | r.SetNoPlay(OptZuiDaQiangZhuang3) 64 | r.SetNoPlay(OptZuiDaQiangZhuang4) 65 | 66 | r.SetPlay(roomutils.OptAutoPlay) // 自动代打 67 | r.SetNoPlay(roomutils.OptForbidEnterAfterGameStart) // 游戏开始后禁止进入游戏 68 | 69 | roomName, _ := config.String("room", subId, "roomName") 70 | if strings.Contains(roomName, "耒阳") { 71 | r.SetNoPlay(OptSiHuaNiu) 72 | r.SetMainPlay(OptFanBeiGuiZe3) 73 | } 74 | // 明牌场 75 | if strings.Contains(roomName, "明牌") { 76 | r.SetMainPlay(OptMingPaiShangZhuang) 77 | } 78 | 79 | return r.Room 80 | } 81 | 82 | func (w *NiuNiuWorld) GetName() string { 83 | return "niuniu" 84 | } 85 | 86 | func (w *NiuNiuWorld) NewPlayer() *service.Player { 87 | p := &NiuNiuPlayer{} 88 | p.cards = make([]int, 5) 89 | p.expectCards = make([]int, 5) 90 | p.doneCards = make([]int, 5) 91 | 92 | p.Player = service.NewPlayer(p) 93 | p.initGame() 94 | return p.Player 95 | } 96 | 97 | func GetPlayer(id int) *NiuNiuPlayer { 98 | if p := service.GetPlayer(id); p != nil { 99 | return p.GameAction.(*NiuNiuPlayer) 100 | } 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /games/migrate/paodekuai/handle.go: -------------------------------------------------------------------------------- 1 | package paodekuai 2 | 3 | import ( 4 | "gofishing-game/service" 5 | 6 | "github.com/guogeer/quasar/v2/cmd" 7 | ) 8 | 9 | type paodekuaiArgs struct { 10 | Cards []int 11 | } 12 | 13 | func init() { 14 | cmd.BindFunc(Discard, (*paodekuaiArgs)(nil), cmd.WithServer((*PaodekuaiWorld)(nil).GetName())) 15 | cmd.BindFunc(Pass, (*paodekuaiArgs)(nil), cmd.WithServer((*PaodekuaiWorld)(nil).GetName())) 16 | } 17 | 18 | func GetPlayerByContext(ctx *cmd.Context) *PaodekuaiPlayer { 19 | if p := service.GetGatewayPlayer(ctx.Ssid); p != nil { 20 | return p.GameAction.(*PaodekuaiPlayer) 21 | } 22 | return nil 23 | } 24 | 25 | func Discard(ctx *cmd.Context, data interface{}) { 26 | args := data.(*paodekuaiArgs) 27 | ply := GetPlayerByContext(ctx) 28 | ply.Discard(args.Cards) 29 | } 30 | 31 | func Pass(ctx *cmd.Context, data interface{}) { 32 | // args := iArgs.(*Args) 33 | ply := GetPlayerByContext(ctx) 34 | ply.Pass() 35 | } 36 | -------------------------------------------------------------------------------- /games/migrate/paodekuai/world.go: -------------------------------------------------------------------------------- 1 | package paodekuai 2 | 3 | import ( 4 | "fmt" 5 | "gofishing-game/games/migrate/internal/cardrule" 6 | "gofishing-game/internal/cardutils" 7 | "gofishing-game/service" 8 | "gofishing-game/service/roomutils" 9 | "strings" 10 | 11 | "github.com/guogeer/quasar/v2/config" 12 | ) 13 | 14 | type PaodekuaiWorld struct{} 15 | 16 | func init() { 17 | w := &PaodekuaiWorld{} 18 | service.AddWorld(w) 19 | 20 | var cards = []int{0xf0, 0xf1} 21 | for color := 0; color < 4; color++ { 22 | for value := 0x02; value <= 0x0e; value++ { 23 | c := (color << 4) | value 24 | cards = append(cards, c) 25 | } 26 | } 27 | 28 | cardutils.AddCardSystem(w.GetName(), cards) 29 | } 30 | 31 | func (w *PaodekuaiWorld) NewRoom(subId int) *roomutils.Room { 32 | r := &PaodekuaiRoom{ 33 | helper: cardrule.NewPaodekuaiHelper(), 34 | } 35 | r.Room = roomutils.NewRoom(subId, r) 36 | r.SetPlay(OptCard16) 37 | r.SetNoPlay(OptCard15) 38 | 39 | r.SetPlay(OptXianshipai) 40 | r.SetNoPlay(OptBuxianshipai) 41 | // r.SetNoPlay(OptZhuangJiaXianChu) 42 | // r.SetPlay(OptHeiTaoSanXianChu) 43 | // r.SetPlay(OptHeiTaoSanBiChu) 44 | r.SetPlay(OptBixuguan) 45 | r.SetNoPlay(OptKebuguan) 46 | r.SetNoPlay(OptHongtaoshizhaniao) 47 | r.SetPlay(OptShoulunheitaosanxianchu) 48 | r.SetNoPlay(OptSidaisan) 49 | 50 | roomName, _ := config.String("room", subId, "roomName") 51 | if strings.Contains(roomName, "郑州") { 52 | r.SetMainPlay(OptSandaidui) 53 | r.SetNoPlay(OptMeilunheitaosanbichu) 54 | } 55 | 56 | r.SetPlay(fmt.Sprintf(roomutils.OptSeat, 3)) 57 | r.SetNoPlay(fmt.Sprintf(roomutils.OptSeat, 2)) 58 | 59 | return r.Room 60 | } 61 | 62 | func (w *PaodekuaiWorld) GetName() string { 63 | return "pdk" 64 | } 65 | 66 | func (w *PaodekuaiWorld) NewPlayer() *service.Player { 67 | p := &PaodekuaiPlayer{} 68 | p.cards = make([]int, 1024) 69 | 70 | p.Player = service.NewPlayer(p) 71 | p.initGame() 72 | return p.Player 73 | } 74 | 75 | func GetPlayer(id int) *PaodekuaiPlayer { 76 | if p := service.GetPlayer(id); p != nil { 77 | return p.GameAction.(*PaodekuaiPlayer) 78 | } 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /games/migrate/sangong/README: -------------------------------------------------------------------------------- 1 | 耒阳地区的三公玩法 2 | -------------------------------------------------------------------------------- /games/migrate/sangong/handle.go: -------------------------------------------------------------------------------- 1 | package sangong 2 | 3 | import ( 4 | "gofishing-game/service" 5 | 6 | "github.com/guogeer/quasar/v2/cmd" 7 | ) 8 | 9 | func init() { 10 | cmd.BindFunc(Finish, (*sangongArgs)(nil), cmd.WithServer((*SangongWorld)(nil).GetName())) 11 | cmd.BindFunc(Bet, (*sangongArgs)(nil), cmd.WithServer((*SangongWorld)(nil).GetName())) 12 | cmd.BindFunc(ChooseDealer, (*sangongArgs)(nil), cmd.WithServer((*SangongWorld)(nil).GetName())) 13 | } 14 | 15 | type sangongArgs struct { 16 | Ans bool 17 | Chip int 18 | } 19 | 20 | func GetPlayerByContext(ctx *cmd.Context) *SangongPlayer { 21 | if p := service.GetGatewayPlayer(ctx.Ssid); p != nil { 22 | return p.GameAction.(*SangongPlayer) 23 | } 24 | return nil 25 | } 26 | 27 | func Finish(ctx *cmd.Context, iArgs interface{}) { 28 | ply := GetPlayerByContext(ctx) 29 | if ply == nil { 30 | 31 | return 32 | } 33 | ply.Finish() 34 | } 35 | 36 | func Bet(ctx *cmd.Context, iArgs interface{}) { 37 | args := iArgs.(*sangongArgs) 38 | ply := GetPlayerByContext(ctx) 39 | if ply == nil { 40 | 41 | return 42 | } 43 | ply.Bet(args.Chip) 44 | } 45 | 46 | func ChooseDealer(ctx *cmd.Context, iArgs interface{}) { 47 | args := iArgs.(*sangongArgs) 48 | ply := GetPlayerByContext(ctx) 49 | if ply == nil { 50 | 51 | return 52 | } 53 | ply.ChooseDealer(args.Ans) 54 | } 55 | -------------------------------------------------------------------------------- /games/migrate/sangong/player.go: -------------------------------------------------------------------------------- 1 | // 耒阳地区的三公玩法 2 | // 2017-8-9 3 | package sangong 4 | 5 | import ( 6 | "gofishing-game/internal/errcode" 7 | "gofishing-game/internal/gameutils" 8 | "gofishing-game/service" 9 | "gofishing-game/service/roomutils" 10 | 11 | "github.com/guogeer/quasar/v2/log" 12 | ) 13 | 14 | // 玩家信息 15 | type SangongPlayerInfo struct { 16 | service.UserInfo 17 | SeatIndex int `json:"seatIndex,omitempty"` 18 | Cards []int `json:"cards,omitempty"` 19 | // 准备、房主开始游戏,亮牌、看牌 20 | IsReady bool `json:"isReady,omitempty"` 21 | StartGameOrNot bool `json:"startGameOrNot,omitempty"` 22 | IsDone bool `json:"isDone,omitempty"` 23 | 24 | Chip int `json:"chip,omitempty"` 25 | RobOrNot int `json:"robOrNot,omitempty"` 26 | CardType int `json:"cardType,omitempty"` 27 | } 28 | 29 | type SangongPlayer struct { 30 | cards []int // 手牌 31 | robOrNot int // 自由抢庄 32 | chip int // 押注 33 | cardType int 34 | 35 | isDone bool 36 | winGold int64 37 | *service.Player 38 | } 39 | 40 | // 已亮牌 41 | func (ply *SangongPlayer) IsDone() bool { 42 | return ply.isDone 43 | } 44 | 45 | func (ply *SangongPlayer) TryEnter() errcode.Error { 46 | return nil 47 | } 48 | 49 | func (ply *SangongPlayer) BeforeEnter() { 50 | } 51 | 52 | func (ply *SangongPlayer) AfterEnter() { 53 | } 54 | 55 | func (ply *SangongPlayer) BeforeLeave() { 56 | } 57 | 58 | func (ply *SangongPlayer) TryLeave() errcode.Error { 59 | room := ply.Room() 60 | if room.Status != 0 { 61 | return errcode.Retry 62 | } 63 | return nil 64 | } 65 | 66 | func (ply *SangongPlayer) initGame() { 67 | for i := 0; i < len(ply.cards); i++ { 68 | ply.cards[i] = 0 69 | } 70 | 71 | ply.chip = -1 72 | ply.robOrNot = -1 73 | ply.isDone = false 74 | } 75 | 76 | // 算牌 77 | func (ply *SangongPlayer) Finish() { 78 | if !roomutils.GetRoomObj(ply.Player).IsReady() { 79 | return 80 | } 81 | if ply.IsDone() { 82 | return 83 | } 84 | 85 | room := ply.Room() 86 | log.Debug("finish", ply.cards, room.Status) 87 | if room.Status != RoomStatusLook { 88 | return 89 | } 90 | 91 | // 庄家最后显示 92 | typ, _ := room.helper.GetType(ply.cards) 93 | room.Broadcast("finish", map[string]any{ 94 | "uid": ply.Id, 95 | "type": typ, 96 | "cards": ply.cards, 97 | }) 98 | 99 | ply.isDone = true 100 | room.OnFinish() 101 | } 102 | 103 | func (ply *SangongPlayer) GameOver() { 104 | ply.initGame() 105 | } 106 | 107 | func (ply *SangongPlayer) Bet(chip int) { 108 | if !roomutils.GetRoomObj(ply.Player).IsReady() { 109 | return 110 | } 111 | 112 | room := ply.Room() 113 | log.Debugf("player %d bet %d %d status %d step %d", ply.Id, ply.chip, chip, room.Status, ply.Step()) 114 | if ply.chip != -1 { 115 | return 116 | } 117 | if room.Status != RoomStatusBet { 118 | return 119 | } 120 | 121 | unit := ply.Step() 122 | if chip%unit != 0 || chip < 1 || chip/unit > 10 { 123 | return 124 | } 125 | 126 | // OK 127 | ply.chip = chip 128 | room.Broadcast("bet", map[string]any{"uid": ply.Id, "chip": chip}) 129 | room.OnBet() 130 | } 131 | 132 | func (ply *SangongPlayer) ChooseDealer(b bool) { 133 | if !roomutils.GetRoomObj(ply.Player).IsReady() { 134 | return 135 | } 136 | if ply.robOrNot != -1 { 137 | return 138 | } 139 | 140 | // OK 141 | room := ply.Room() 142 | 143 | ply.robOrNot = 0 144 | if b { 145 | ply.robOrNot = 1 146 | } 147 | room.Broadcast("chooseDealer", gameutils.MergeError(nil, map[string]any{"uid": ply.Id, "ans": b})) 148 | room.OnChooseDealer() 149 | } 150 | 151 | func (ply *SangongPlayer) GetUserInfo(self bool) *SangongPlayerInfo { 152 | info := &SangongPlayerInfo{} 153 | info.UserInfo = ply.UserInfo 154 | // info.UId = ply.GetCharObj().Id 155 | info.SeatIndex = ply.GetSeatIndex() 156 | info.IsReady = roomutils.GetRoomObj(ply.Player).IsReady() 157 | info.RobOrNot = ply.robOrNot 158 | info.Chip = ply.chip 159 | 160 | room := ply.Room() 161 | if room.Status == RoomStatusLook && roomutils.GetRoomObj(ply.Player).IsReady() { 162 | info.IsDone = ply.IsDone() 163 | info.Cards = make([]int, len(ply.cards)) 164 | copy(info.Cards, ply.cards) 165 | 166 | // 已亮牌 167 | if ply.IsDone() { 168 | info.IsDone = true 169 | info.CardType = ply.cardType 170 | } 171 | } 172 | return info 173 | } 174 | 175 | func (ply *SangongPlayer) Room() *SangongRoom { 176 | if room := roomutils.GetRoomObj(ply.Player).CustomRoom(); room != nil { 177 | return room.(*SangongRoom) 178 | } 179 | return nil 180 | } 181 | 182 | func (ply *SangongPlayer) Replay(messageId string, i interface{}) { 183 | switch messageId { 184 | case "sitDown": 185 | return 186 | } 187 | ply.Player.Replay(messageId, i) 188 | } 189 | 190 | func (ply *SangongPlayer) Step() int { 191 | room := ply.Room() 192 | 193 | unit := room.GetPlayValue(OptChouma) 194 | return unit 195 | } 196 | 197 | func (ply *SangongPlayer) GetSeatIndex() int { 198 | return roomutils.GetRoomObj(ply.Player).GetSeatIndex() 199 | } 200 | -------------------------------------------------------------------------------- /games/migrate/sangong/world.go: -------------------------------------------------------------------------------- 1 | package sangong 2 | 3 | import ( 4 | "gofishing-game/games/migrate/internal/cardrule" 5 | "gofishing-game/internal/cardutils" 6 | "gofishing-game/service" 7 | "gofishing-game/service/roomutils" 8 | "time" 9 | ) 10 | 11 | type SangongWorld struct{} 12 | 13 | func init() { 14 | w := &SangongWorld{} 15 | service.AddWorld(w) 16 | 17 | var cards []int 18 | for color := 0; color < 4; color++ { 19 | for value := 2; value <= 14; value++ { 20 | c := (color << 4) | value 21 | cards = append(cards, c) 22 | } 23 | } 24 | 25 | cardutils.AddCardSystem(w.GetName(), cards) 26 | } 27 | 28 | func (w *SangongWorld) NewRoom(subId int) *roomutils.Room { 29 | r := &SangongRoom{ 30 | helper: cardrule.NewSangongHelper(), 31 | } 32 | r.Room = roomutils.NewRoom(subId, r) 33 | r.SetFreeDuration(18 * time.Second) 34 | 35 | r.SetPlay(OptFangzhudangzhuang) 36 | r.SetNoPlay(OptWuzhuang) 37 | r.SetNoPlay(OptZiyouqiangzhuang) 38 | 39 | r.SetPlay(OptChouma, 1) 40 | r.SetNoPlay(OptChouma, 2) 41 | r.SetNoPlay(OptChouma, 3) 42 | r.SetNoPlay(OptChouma, 5) 43 | r.SetNoPlay(OptChouma, 8) 44 | r.SetNoPlay(OptChouma, 10) 45 | r.SetNoPlay(OptChouma, 20) 46 | 47 | r.SetPlay(roomutils.OptAutoPlay) // 自动代打 48 | r.SetNoPlay(roomutils.OptForbidEnterAfterGameStart) // 游戏开始后禁止进入游戏 49 | return r.Room 50 | } 51 | 52 | func (w *SangongWorld) GetName() string { 53 | return "sangong" 54 | } 55 | 56 | func (w *SangongWorld) NewPlayer() *service.Player { 57 | p := &SangongPlayer{} 58 | p.cards = make([]int, 3) 59 | 60 | p.Player = service.NewPlayer(p) 61 | p.initGame() 62 | return p.Player 63 | } 64 | 65 | func GetPlayer(id int) *SangongPlayer { 66 | if p := service.GetPlayer(id); p != nil { 67 | return p.GameAction.(*SangongPlayer) 68 | } 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /games/migrate/texas/README: -------------------------------------------------------------------------------- 1 | 德州扑克 2 | -------------------------------------------------------------------------------- /games/migrate/texas/handle.go: -------------------------------------------------------------------------------- 1 | package texas 2 | 3 | import ( 4 | "gofishing-game/service" 5 | 6 | "github.com/guogeer/quasar/v2/cmd" 7 | "github.com/guogeer/quasar/v2/config" 8 | ) 9 | 10 | type texasArgs struct { 11 | SeatIndex int `json:"seatIndex,omitempty"` 12 | Gold int64 `json:"gold,omitempty"` 13 | Auto int `json:"auto,omitempty"` 14 | IsShow bool `json:"isShow,omitempty"` 15 | Level int `json:"level,omitempty"` // 房间等级 16 | } 17 | 18 | func init() { 19 | cmd.BindFunc(TakeAction, (*texasArgs)(nil), cmd.WithServer((*TexasWorld)(nil).GetName())) 20 | cmd.BindFunc(SitDown, (*texasArgs)(nil), cmd.WithServer((*TexasWorld)(nil).GetName())) 21 | cmd.BindFunc(ChooseBankroll, (*texasArgs)(nil), cmd.WithServer((*TexasWorld)(nil).GetName())) 22 | cmd.BindFunc(SitUp, (*texasArgs)(nil), cmd.WithServer((*TexasWorld)(nil).GetName())) // 站起 23 | cmd.BindFunc(SetAutoPlay, (*texasArgs)(nil), cmd.WithServer((*TexasWorld)(nil).GetName())) // 托管 24 | cmd.BindFunc(ShowCard, (*texasArgs)(nil), cmd.WithServer((*TexasWorld)(nil).GetName())) // 亮牌 25 | cmd.BindFunc(Rebuy, (*texasArgs)(nil), cmd.WithServer((*TexasWorld)(nil).GetName())) // 重购 26 | cmd.BindFunc(Addon, (*texasArgs)(nil), cmd.WithServer((*TexasWorld)(nil).GetName())) // 增购 27 | 28 | cmd.BindFunc(SetWallet, (*texasArgs)(nil), cmd.WithServer((*TexasWorld)(nil).GetName())) 29 | cmd.BindFunc(funcRecommendRooms, (*texasArgs)(nil), cmd.WithServer((*TexasWorld)(nil).GetName())) 30 | } 31 | 32 | func GetPlayerByContext(ctx *cmd.Context) *TexasPlayer { 33 | if p := service.GetGatewayPlayer(ctx.Ssid); p != nil { 34 | return p.GameAction.(*TexasPlayer) 35 | } 36 | return nil 37 | } 38 | 39 | func TakeAction(ctx *cmd.Context, iArgs interface{}) { 40 | args := iArgs.(*texasArgs) 41 | ply := GetPlayerByContext(ctx) 42 | if ply == nil { 43 | 44 | return 45 | } 46 | ply.TakeAction(args.Gold) 47 | } 48 | 49 | func SitDown(ctx *cmd.Context, iArgs interface{}) { 50 | args := iArgs.(*texasArgs) 51 | ply := GetPlayerByContext(ctx) 52 | if ply == nil { 53 | 54 | return 55 | } 56 | ply.SitDown(args.SeatIndex) 57 | } 58 | 59 | func ChooseBankroll(ctx *cmd.Context, iArgs interface{}) { 60 | args := iArgs.(*texasArgs) 61 | ply := GetPlayerByContext(ctx) 62 | if ply == nil { 63 | 64 | return 65 | } 66 | ply.ChooseBankroll(args.Gold) 67 | } 68 | 69 | func SitUp(ctx *cmd.Context, iArgs interface{}) { 70 | // args := iArgs.(*Args) 71 | ply := GetPlayerByContext(ctx) 72 | if ply == nil { 73 | 74 | return 75 | } 76 | ply.SitUp() 77 | } 78 | 79 | func SetAutoPlay(ctx *cmd.Context, iArgs interface{}) { 80 | args := iArgs.(*texasArgs) 81 | ply := GetPlayerByContext(ctx) 82 | if ply == nil { 83 | 84 | return 85 | } 86 | ply.SetAutoPlay(args.Auto) 87 | } 88 | 89 | func ShowCard(ctx *cmd.Context, iArgs interface{}) { 90 | args := iArgs.(*texasArgs) 91 | ply := GetPlayerByContext(ctx) 92 | if ply == nil { 93 | 94 | return 95 | } 96 | ply.ShowCard(args.IsShow) 97 | } 98 | 99 | func Rebuy(ctx *cmd.Context, iArgs interface{}) { 100 | // args := iArgs.(*Args) 101 | ply := GetPlayerByContext(ctx) 102 | if ply == nil { 103 | 104 | return 105 | } 106 | ply.Rebuy() 107 | } 108 | 109 | func Addon(ctx *cmd.Context, iArgs interface{}) { 110 | // args := iArgs.(*Args) 111 | ply := GetPlayerByContext(ctx) 112 | if ply == nil { 113 | 114 | return 115 | } 116 | ply.Addon() 117 | } 118 | 119 | func SetWallet(ctx *cmd.Context, data interface{}) { 120 | args := data.(*texasArgs) 121 | ply := GetPlayerByContext(ctx) 122 | if ply == nil { 123 | return 124 | } 125 | ply.SetWallet(args.Gold) 126 | } 127 | 128 | // 推荐房间 129 | func funcRecommendRooms(ctx *cmd.Context, data interface{}) { 130 | args := data.(*texasArgs) 131 | ss := &cmd.Session{Id: ctx.Ssid, Out: ctx.Out} 132 | rooms := RecommendRooms(args.Level) 133 | 134 | var roomList []TexasRoomInfo 135 | for _, room := range rooms { 136 | texasRoom := room.CustomRoom().(*TexasRoom) 137 | minBankroll, _ := config.Int("texasroom", room.SubId, "minBankroll") 138 | maxBankroll, _ := config.Int("texasroom", room.SubId, "maxBankroll") 139 | info := TexasRoomInfo{ 140 | Id: texasRoom.Id, 141 | SubId: texasRoom.SubId, 142 | FrontBlind: texasRoom.frontBlind, 143 | SmallBlind: texasRoom.smallBlind, 144 | BigBlind: texasRoom.bigBlind, 145 | MinBankroll: minBankroll, 146 | MaxBankroll: maxBankroll, 147 | ActiveUsers: texasRoom.NumSeatPlayer(), 148 | } 149 | roomList = append(roomList, info) 150 | } 151 | service.WriteMessage((*TexasWorld)(nil).GetName(), ss, "recommendRooms", map[string]any{"level": args.Level, "rooms": roomList}) 152 | } 153 | -------------------------------------------------------------------------------- /games/migrate/texas/world.go: -------------------------------------------------------------------------------- 1 | package texas 2 | 3 | import ( 4 | "gofishing-game/games/migrate/internal/cardrule" 5 | "gofishing-game/internal/cardutils" 6 | "gofishing-game/service" 7 | "gofishing-game/service/roomutils" 8 | "time" 9 | ) 10 | 11 | type TexasWorld struct{} 12 | 13 | func init() { 14 | w := &TexasWorld{} 15 | service.AddWorld(w) 16 | 17 | var cards []int 18 | for color := 0; color < 4; color++ { 19 | for value := 2; value <= 14; value++ { 20 | c := (color << 4) | value 21 | cards = append(cards, c) 22 | } 23 | } 24 | 25 | cardutils.AddCardSystem(w.GetName(), cards) 26 | } 27 | 28 | func (w *TexasWorld) NewRoom(subId int) *roomutils.Room { 29 | helper := cardrule.NewTexasHelper() 30 | room := &TexasRoom{ 31 | helper: helper, 32 | cards: make([]int, 0, helper.Size()), 33 | 34 | tempDealerSeat: -1, 35 | dealerSeat: -1, 36 | smallBlindSeat: -1, 37 | bigBlindSeat: -1, 38 | } 39 | room.Room = roomutils.NewRoom(subId, room) 40 | room.AutoStart() 41 | room.SetFreeDuration(18 * time.Second) 42 | 43 | room.SetPlay(roomutils.OptAutoPlay) // 自动代打 44 | room.SetNoPlay(roomutils.OptForbidEnterAfterGameStart) // 游戏开始后禁止进入游戏 45 | return room.Room 46 | } 47 | 48 | func (w *TexasWorld) GetName() string { 49 | return "texas" 50 | } 51 | 52 | func (w *TexasWorld) NewPlayer() *service.Player { 53 | p := &TexasPlayer{} 54 | 55 | p.Player = service.NewPlayer(p) 56 | p.initGame() 57 | return p.Player 58 | } 59 | -------------------------------------------------------------------------------- /games/migrate/xiaojiu/README: -------------------------------------------------------------------------------- 1 | 耒阳地区的三公玩法 2 | -------------------------------------------------------------------------------- /games/migrate/xiaojiu/handle.go: -------------------------------------------------------------------------------- 1 | package xiaojiu 2 | 3 | import ( 4 | "gofishing-game/service" 5 | 6 | "github.com/guogeer/quasar/v2/cmd" 7 | ) 8 | 9 | func init() { 10 | cmd.BindFunc(Bet, (*xiaojiuArgs)(nil), cmd.WithServer((*XiaojiuWorld)(nil).GetName())) 11 | } 12 | 13 | type xiaojiuArgs struct { 14 | AreaId int `json:"areaId,omitempty"` 15 | SeatIndex int `json:"seatIndex,omitempty"` 16 | Gold int64 `json:"gold,omitempty"` 17 | } 18 | 19 | func GetPlayerByContext(ctx *cmd.Context) *XiaojiuPlayer { 20 | if p := service.GetGatewayPlayer(ctx.Ssid); p != nil { 21 | return p.GameAction.(*XiaojiuPlayer) 22 | } 23 | return nil 24 | } 25 | 26 | func Bet(ctx *cmd.Context, iArgs interface{}) { 27 | args := iArgs.(*xiaojiuArgs) 28 | ply := GetPlayerByContext(ctx) 29 | if ply == nil { 30 | 31 | return 32 | } 33 | ply.Bet(args.AreaId, args.Gold) 34 | } 35 | -------------------------------------------------------------------------------- /games/migrate/xiaojiu/player.go: -------------------------------------------------------------------------------- 1 | package xiaojiu 2 | 3 | // 小九 4 | // Guogeer 2018-02-08 5 | 6 | import ( 7 | "gofishing-game/internal/errcode" 8 | "gofishing-game/internal/gameutils" 9 | "gofishing-game/service" 10 | "gofishing-game/service/roomutils" 11 | 12 | "github.com/guogeer/quasar/v2/log" 13 | ) 14 | 15 | // 玩家信息 16 | type XiaojiuUserInfo struct { 17 | service.UserInfo 18 | SeatIndex int 19 | Areas [3]int64 20 | } 21 | 22 | type XiaojiuPlayer struct { 23 | *service.Player 24 | 25 | areas [3]int64 26 | winGold int64 27 | } 28 | 29 | func (ply *XiaojiuPlayer) BeforeEnter() { 30 | } 31 | 32 | func (ply *XiaojiuPlayer) AfterEnter() { 33 | } 34 | 35 | func (ply *XiaojiuPlayer) TryEnter() errcode.Error { 36 | room := ply.Room() 37 | if room.Status != 0 || room.ExistTimes != 0 { 38 | return roomutils.ErrPlaying 39 | } 40 | return nil 41 | } 42 | 43 | func (ply *XiaojiuPlayer) TryLeave() errcode.Error { 44 | room := ply.Room() 45 | if room.IsTypeScore() && room.Status != 0 { 46 | return errcode.Retry 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (ply *XiaojiuPlayer) BeforeLeave() { 53 | } 54 | 55 | func (ply *XiaojiuPlayer) initGame() { 56 | for i := range ply.areas { 57 | ply.areas[i] = 0 58 | } 59 | ply.winGold = 0 60 | } 61 | 62 | func (ply *XiaojiuPlayer) GameOver() { 63 | ply.initGame() 64 | } 65 | 66 | func (ply *XiaojiuPlayer) Bet(area int, gold int64) { 67 | room := ply.Room() 68 | log.Debugf("player %d bet %d %d status %d", ply.Id, area, gold, room.Status) 69 | if ply == room.dealer { 70 | return 71 | } 72 | if gold < 0 || area < 0 || area >= len(ply.areas) { 73 | return 74 | } 75 | var sum int64 76 | for _, n := range ply.areas { 77 | sum += n 78 | } 79 | 80 | limit := room.betLimitPerUser() // 单人限注 81 | if sum+gold > limit { 82 | return 83 | } 84 | if room.Status != roomutils.RoomStatusPlaying { 85 | return 86 | } 87 | 88 | // OK 89 | ply.areas[area] += gold 90 | ply.AddGold(-gold, "xiaojiu_bet", service.WithNoItemLog()) 91 | 92 | room.areas[area] += gold 93 | room.Broadcast("bet", gameutils.MergeError(nil, map[string]any{"uid": ply.Id, "areaId": area, "gold": gold})) 94 | } 95 | 96 | func (ply *XiaojiuPlayer) GetUserInfo(self bool) *XiaojiuUserInfo { 97 | info := &XiaojiuUserInfo{} 98 | info.UserInfo = ply.UserInfo 99 | info.SeatIndex = ply.GetSeatIndex() 100 | info.Areas = ply.areas 101 | return info 102 | } 103 | 104 | func (ply *XiaojiuPlayer) Room() *XiaojiuRoom { 105 | if room := roomutils.GetRoomObj(ply.Player).CustomRoom(); room != nil { 106 | return room.(*XiaojiuRoom) 107 | } 108 | return nil 109 | } 110 | 111 | func (ply *XiaojiuPlayer) GetSeatIndex() int { 112 | return roomutils.GetRoomObj(ply.Player).GetSeatIndex() 113 | } 114 | -------------------------------------------------------------------------------- /games/migrate/xiaojiu/world.go: -------------------------------------------------------------------------------- 1 | package xiaojiu 2 | 3 | import ( 4 | "gofishing-game/internal/cardutils" 5 | "gofishing-game/service" 6 | "gofishing-game/service/roomutils" 7 | "time" 8 | ) 9 | 10 | func init() { 11 | w := &XiaojiuWorld{} 12 | service.AddWorld(w) 13 | 14 | var cards []int 15 | for color := 0; color < 4; color++ { 16 | for value := 2; value <= 0xa; value++ { 17 | c := (color << 4) | value 18 | cards = append(cards, c) 19 | } 20 | } 21 | cards = append(cards, 0x0e, 0x1e, 0x2e, 0x3e) 22 | cardutils.AddCardSystem(w.GetName(), cards) 23 | } 24 | 25 | type XiaojiuWorld struct{} 26 | 27 | func (w *XiaojiuWorld) NewRoom(subId int) *roomutils.Room { 28 | room := &XiaojiuRoom{} 29 | room.Room = roomutils.NewRoom(subId, room) 30 | room.SetFreeDuration(18 * time.Second) 31 | 32 | room.SetNoPlay(roomOptMingjiu) // 明九 33 | room.SetPlay(roomOptAnjiu) // 暗九 34 | 35 | room.SetNoPlay(roomOptLunzhuang) // 轮庄 36 | room.SetNoPlay(roomOptSuijizhuang) // 随机庄 37 | room.SetPlay(roomOptFangzhuzhuang) // 房主庄 38 | 39 | room.SetPlay(roomOptDanrenxianzhu10) // 单人限注10 40 | room.SetNoPlay(roomOptDanrenxianzhu20) // 单人限注20 41 | room.SetNoPlay(roomOptDanrenxianzhu30) // 单人限注30 42 | room.SetNoPlay(roomOptDanrenxianzhu50) // 单人限注50 43 | 44 | room.SetNoPlay(roomOptZhuangjiabie10) // 蹩十 45 | room.SetNoPlay("必压_1") // 必压1 46 | room.SetNoPlay("必压_2") // 必压2 47 | room.SetNoPlay("必压_5") // 必压5 48 | room.SetNoPlay("必压_10") // 必压10 49 | room.SetNoPlay("必压_20") // 必压20 50 | 51 | return room.Room 52 | } 53 | 54 | func (w *XiaojiuWorld) GetName() string { 55 | return "xiao9" 56 | } 57 | 58 | func (w *XiaojiuWorld) NewPlayer() *service.Player { 59 | p := &XiaojiuPlayer{} 60 | 61 | p.Player = service.NewPlayer(p) 62 | p.initGame() 63 | return p.Player 64 | } 65 | 66 | func GetPlayer(id int) *XiaojiuPlayer { 67 | if p := service.GetPlayer(id); p != nil { 68 | return p.GameAction.(*XiaojiuPlayer) 69 | } 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /games/migrate/zhajinhua/handle.go: -------------------------------------------------------------------------------- 1 | package zhajinhua 2 | 3 | import ( 4 | "gofishing-game/service" 5 | 6 | "github.com/guogeer/quasar/v2/cmd" 7 | ) 8 | 9 | func init() { 10 | cmd.BindFunc(TakeAction, (*zhajinhuaArgs)(nil), cmd.WithServer((*ZhajinhuaWorld)(nil).GetName())) 11 | cmd.BindFunc(SitUp, (*zhajinhuaArgs)(nil), cmd.WithServer((*ZhajinhuaWorld)(nil).GetName())) // 站起 12 | cmd.BindFunc(LookCard, (*zhajinhuaArgs)(nil), cmd.WithServer((*ZhajinhuaWorld)(nil).GetName())) // 亮牌 13 | cmd.BindFunc(ShowCard, (*zhajinhuaArgs)(nil), cmd.WithServer((*ZhajinhuaWorld)(nil).GetName())) // 亮牌 14 | cmd.BindFunc(CompareCard, (*zhajinhuaArgs)(nil), cmd.WithServer((*ZhajinhuaWorld)(nil).GetName())) // 比牌 15 | cmd.BindFunc(SetAutoPlay, (*zhajinhuaArgs)(nil), cmd.WithServer((*ZhajinhuaWorld)(nil).GetName())) // 托管 16 | } 17 | 18 | type zhajinhuaArgs struct { 19 | SeatIndex int `json:"seatIndex,omitempty"` 20 | Gold int64 `json:"gold,omitempty"` 21 | Auto int `json:"auto,omitempty"` 22 | } 23 | 24 | func GetPlayerByContext(ctx *cmd.Context) *ZhajinhuaPlayer { 25 | if p := service.GetGatewayPlayer(ctx.Ssid); p != nil { 26 | return p.GameAction.(*ZhajinhuaPlayer) 27 | } 28 | return nil 29 | } 30 | 31 | func TakeAction(ctx *cmd.Context, iArgs interface{}) { 32 | args := iArgs.(*zhajinhuaArgs) 33 | ply := GetPlayerByContext(ctx) 34 | if ply == nil { 35 | return 36 | } 37 | ply.TakeAction(args.Gold) 38 | } 39 | 40 | func SitUp(ctx *cmd.Context, iArgs interface{}) { 41 | // args := iArgs.(*Args) 42 | ply := GetPlayerByContext(ctx) 43 | if ply == nil { 44 | return 45 | } 46 | ply.SitUp() 47 | } 48 | 49 | func LookCard(ctx *cmd.Context, iArgs interface{}) { 50 | // args := iArgs.(*Args) 51 | ply := GetPlayerByContext(ctx) 52 | if ply == nil { 53 | return 54 | } 55 | ply.LookCard() 56 | } 57 | 58 | func ShowCard(ctx *cmd.Context, iArgs interface{}) { 59 | // args := iArgs.(*Args) 60 | ply := GetPlayerByContext(ctx) 61 | if ply == nil { 62 | return 63 | } 64 | ply.ShowCard() 65 | } 66 | 67 | func CompareCard(ctx *cmd.Context, iArgs interface{}) { 68 | args := iArgs.(*zhajinhuaArgs) 69 | ply := GetPlayerByContext(ctx) 70 | if ply == nil { 71 | return 72 | } 73 | ply.CompareCard(args.SeatIndex) 74 | } 75 | 76 | func SetAutoPlay(ctx *cmd.Context, iArgs interface{}) { 77 | args := iArgs.(*zhajinhuaArgs) 78 | ply := GetPlayerByContext(ctx) 79 | if ply == nil { 80 | return 81 | } 82 | ply.auto = args.Auto 83 | ply.AutoPlay() 84 | ply.WriteJSON("setAutoPlay", map[string]any{"uid": ply.Id, "auto": args.Auto}) 85 | } 86 | -------------------------------------------------------------------------------- /games/migrate/zhajinhua/world.go: -------------------------------------------------------------------------------- 1 | package zhajinhua 2 | 3 | import ( 4 | "gofishing-game/games/migrate/internal/cardrule" 5 | "gofishing-game/internal/cardutils" 6 | "gofishing-game/service" 7 | "gofishing-game/service/roomutils" 8 | "time" 9 | ) 10 | 11 | type ZhajinhuaWorld struct{} 12 | 13 | func init() { 14 | w := &ZhajinhuaWorld{} 15 | service.AddWorld(w) 16 | 17 | var cards []int 18 | for color := 0; color < 4; color++ { 19 | for value := 2; value <= 14; value++ { 20 | c := (color << 4) | value 21 | cards = append(cards, c) 22 | } 23 | } 24 | 25 | cardutils.AddCardSystem(w.GetName(), cards) 26 | } 27 | 28 | func (w *ZhajinhuaWorld) NewRoom(subId int) *roomutils.Room { 29 | room := &ZhajinhuaRoom{ 30 | helper: cardrule.NewZhajinhuaHelper(w.GetName()), 31 | 32 | dealerSeatIndex: -1, 33 | } 34 | room.Room = roomutils.NewRoom(subId, room) 35 | room.AutoStart() 36 | room.SetFreeDuration(8 * time.Second) 37 | 38 | room.SetPlay(OptLunshu10) 39 | room.SetNoPlay(OptLunshu20) 40 | 41 | room.SetNoPlay(OptMengpailunshu1) 42 | room.SetNoPlay(OptMengpailunshu2) 43 | room.SetNoPlay(OptMengpailunshu3) 44 | 45 | room.SetPlay(roomutils.OptAutoPlay) // 自动代打 46 | room.SetNoPlay(roomutils.OptForbidEnterAfterGameStart) // 游戏开始后禁止进入游戏 47 | return room.Room 48 | } 49 | 50 | func (w *ZhajinhuaWorld) GetName() string { 51 | return "zjh" 52 | } 53 | 54 | func (w *ZhajinhuaWorld) NewPlayer() *service.Player { 55 | p := &ZhajinhuaPlayer{} 56 | 57 | p.Player = service.NewPlayer(p) 58 | p.initGame() 59 | return p.Player 60 | } 61 | 62 | func GetPlayer(id int) *ZhajinhuaPlayer { 63 | if p := service.GetPlayer(id); p != nil { 64 | return p.GameAction.(*ZhajinhuaPlayer) 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gofishing-game 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.8.1 // indirect 7 | github.com/yuin/gopher-lua v1.1.1 8 | google.golang.org/grpc v1.67.1 9 | google.golang.org/protobuf v1.35.1 10 | layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf 11 | layeh.com/gopher-luar v1.0.11 12 | ) 13 | 14 | require ( 15 | github.com/golang-jwt/jwt/v4 v4.5.0 16 | github.com/guogeer/quasar/v2 v2.0.6 17 | ) 18 | 19 | require ( 20 | filippo.io/edwards25519 v1.1.0 // indirect 21 | github.com/jinzhu/inflection v1.0.0 // indirect 22 | github.com/jinzhu/now v1.1.5 // indirect 23 | ) 24 | 25 | require ( 26 | github.com/buger/jsonparser v1.1.1 // indirect 27 | github.com/bytedance/sonic v1.12.3 // indirect 28 | github.com/bytedance/sonic/loader v0.2.0 // indirect 29 | github.com/cloudwego/base64x v0.1.4 // indirect 30 | github.com/cloudwego/iasm v0.2.0 // indirect 31 | github.com/gabriel-vasile/mimetype v1.4.5 // indirect 32 | github.com/gin-contrib/cors v1.7.2 33 | github.com/gin-contrib/sse v0.1.0 // indirect 34 | github.com/gin-gonic/gin v1.10.0 35 | github.com/go-playground/locales v0.14.1 // indirect 36 | github.com/go-playground/universal-translator v0.18.1 // indirect 37 | github.com/go-playground/validator/v10 v10.22.1 // indirect 38 | github.com/goccy/go-json v0.10.3 // indirect 39 | github.com/google/uuid v1.6.0 // indirect 40 | github.com/json-iterator/go v1.1.12 // indirect 41 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 42 | github.com/kr/text v0.2.0 // indirect 43 | github.com/leodido/go-urn v1.4.0 // indirect 44 | github.com/mattn/go-isatty v0.0.20 // indirect 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 46 | github.com/modern-go/reflect2 v1.0.2 // indirect 47 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 48 | github.com/rogpeppe/go-internal v1.13.1 // indirect 49 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 50 | github.com/ugorji/go/codec v1.2.12 // indirect 51 | golang.org/x/arch v0.11.0 // indirect 52 | golang.org/x/crypto v0.28.0 // indirect 53 | golang.org/x/net v0.30.0 // indirect 54 | golang.org/x/sys v0.26.0 // indirect 55 | golang.org/x/text v0.19.0 // indirect 56 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect 57 | gopkg.in/yaml.v3 v3.0.1 // indirect 58 | gorm.io/driver/mysql v1.5.7 59 | gorm.io/gorm v1.25.12 60 | ) 61 | -------------------------------------------------------------------------------- /hall/gm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/guogeer/quasar/v2/utils" 5 | 6 | "github.com/guogeer/quasar/v2/cmd" 7 | "github.com/guogeer/quasar/v2/log" 8 | ) 9 | 10 | type gmArgs struct { 11 | Content string `json:"content,omitempty"` 12 | 13 | Users []int `json:"users,omitempty"` 14 | Mail *Mail `json:"mail,omitempty"` 15 | } 16 | 17 | func init() { 18 | cmd.Bind("sendMail", funcSendMail, (*gmArgs)(nil)) 19 | } 20 | 21 | func funcSendMail(ctx *cmd.Context, data any) { 22 | args := data.(*gmArgs) 23 | 24 | mail := &Mail{} 25 | utils.DeepCopy(mail, args.Mail) 26 | log.Info("send mail", args.Users, mail.EffectTime, mail.RegTime) 27 | if mail.Type == MailTypeMass { 28 | args.Users = []int{0} 29 | } 30 | 31 | for _, uid := range args.Users { 32 | mail.RecvId = uid 33 | SendMail(mail) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /hall/handle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "gofishing-game/internal/pb" 7 | "gofishing-game/internal/rpc" 8 | "gofishing-game/service" 9 | 10 | "github.com/guogeer/quasar/v2/cmd" 11 | "github.com/guogeer/quasar/v2/log" 12 | ) 13 | 14 | type hallArgs struct { 15 | Id int `json:"id,omitempty"` 16 | Plate string `json:"plate,omitempty"` 17 | Uid int `json:"uid,omitempty"` 18 | Address string `json:"address,omitempty"` 19 | Version string `json:"version,omitempty"` 20 | Segments []service.GameOnlineSegment `json:"segments,omitempty"` 21 | } 22 | 23 | func GetPlayerByContext(ctx *cmd.Context) *hallPlayer { 24 | if p := service.GetGatewayPlayer(ctx.Ssid); p != nil { 25 | return p.GameAction.(*hallPlayer) 26 | } 27 | return nil 28 | } 29 | 30 | type updateOnlineArgs struct { 31 | Games []service.ServerOnline `json:"games,omitempty"` 32 | } 33 | 34 | func init() { 35 | // internal call 36 | cmd.BindFunc(FUNC_UpdateOnline, (*updateOnlineArgs)(nil), cmd.WithPrivate()) 37 | cmd.BindFunc(FUNC_SyncOnline, (*hallArgs)(nil), cmd.WithPrivate()) 38 | cmd.BindFunc(S2C_GetBestGateway, (*hallArgs)(nil), cmd.WithPrivate()) 39 | 40 | cmd.BindFunc(FUNC_DeleteAccount, (*hallArgs)(nil), cmd.WithPrivate(), cmd.WithoutQueue()) 41 | cmd.BindFunc(FUNC_UpdateMaintain, (*hallArgs)(nil), cmd.WithPrivate()) 42 | cmd.BindFunc(FUNC_UpdateFakeOnline, (*hallArgs)(nil), cmd.WithPrivate()) 43 | } 44 | 45 | // 更新在线人数 46 | func FUNC_UpdateOnline(ctx *cmd.Context, data any) { 47 | w := GetWorld() 48 | args := data.(*updateOnlineArgs) 49 | for _, g := range args.Games { 50 | w.onlines[g.Id] = g 51 | } 52 | } 53 | 54 | // 后台获取用户实时的在线数据 55 | func FUNC_SyncOnline(ctx *cmd.Context, data any) { 56 | // req := data.(*service.Args) 57 | online := GetWorld().GetCurrentOnline() 58 | ctx.Out.WriteJSON("func_syncOnline", online) 59 | } 60 | 61 | func S2C_GetBestGateway(ctx *cmd.Context, data any) { 62 | args := data.(*hallArgs) 63 | w := GetWorld() 64 | w.currentBestGateway = args.Address 65 | } 66 | 67 | // 同步删除账号 68 | func FUNC_DeleteAccount(ctx *cmd.Context, data any) { 69 | args := data.(*hallArgs) 70 | log.Debug("gm delete account", args.Uid) 71 | rpc.CacheClient().ClearAccount(context.Background(), &pb.ClearAccountReq{ 72 | Uid: int32(args.Uid), 73 | }) 74 | ctx.Out.WriteJSON("func_deleteAccount", struct{}{}) 75 | } 76 | 77 | func FUNC_UpdateMaintain(ctx *cmd.Context, data any) { 78 | GetWorld().updateMaintain() 79 | } 80 | 81 | func FUNC_UpdateFakeOnline(ctx *cmd.Context, data any) { 82 | args := data.(*hallArgs) 83 | GetWorld().segments = args.Segments 84 | } 85 | -------------------------------------------------------------------------------- /hall/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | // "fmt" 5 | "gofishing-game/service" 6 | _ "gofishing-game/service/system" 7 | ) 8 | 9 | func main() { 10 | service.Start() 11 | } 12 | -------------------------------------------------------------------------------- /hall/player.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "gofishing-game/internal/errcode" 5 | "gofishing-game/internal/gameutils" 6 | "gofishing-game/internal/pb" 7 | "gofishing-game/service" 8 | ) 9 | 10 | type hallPlayer struct { 11 | *service.Player 12 | 13 | mailObj *mailObj 14 | loginClientVersion string 15 | } 16 | 17 | func (ply *hallPlayer) TryEnter() errcode.Error { 18 | return nil 19 | } 20 | 21 | func (ply *hallPlayer) TryLeave() errcode.Error { 22 | return nil 23 | } 24 | 25 | func (ply *hallPlayer) BeforeEnter() { 26 | ply.mailObj.BeforeEnter() 27 | } 28 | 29 | func (ply *hallPlayer) AfterEnter() { 30 | 31 | } 32 | 33 | func (ply *hallPlayer) BeforeLeave() { 34 | } 35 | 36 | func (ply *hallPlayer) OnClose() { 37 | ply.Player.OnClose() 38 | } 39 | 40 | func (ply *hallPlayer) Load(pdata any) { 41 | bin := pdata.(*pb.UserBin) 42 | gameutils.InitNilFields(bin.Hall) 43 | ply.loginClientVersion = bin.Hall.LoginClientVersion 44 | 45 | ply.mailObj.Load(pdata) 46 | } 47 | 48 | func (ply *hallPlayer) Save(pdata any) { 49 | bin := pdata.(*pb.UserBin) 50 | bin.Hall = &pb.HallBin{ 51 | LoginClientVersion: ply.loginClientVersion, 52 | } 53 | ply.mailObj.Save(pdata) 54 | } 55 | -------------------------------------------------------------------------------- /hall/tool.go: -------------------------------------------------------------------------------- 1 | // 测试工具 2 | package main 3 | 4 | import ( 5 | "gofishing-game/service" 6 | ) 7 | 8 | func init() { 9 | service.AddTestTool(&hallTestTool{}) 10 | } 11 | 12 | type hallTestTool struct { 13 | } 14 | -------------------------------------------------------------------------------- /hall/world.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/guogeer/quasar/v2/utils" 9 | 10 | "gofishing-game/internal/pb" 11 | "gofishing-game/internal/rpc" 12 | "gofishing-game/service" 13 | 14 | "github.com/guogeer/quasar/v2/cmd" 15 | "github.com/guogeer/quasar/v2/config" 16 | "github.com/guogeer/quasar/v2/log" 17 | ) 18 | 19 | // 版本更新奖励 20 | type ClientVersion struct { 21 | // 版本 1.12.123.1234_r,1.12.123.1234_d,1.12.123.1234 22 | Version string `json:"version"` 23 | // 奖励 [[1102,1000],[1104,2000]] 24 | Reward string `json:"reward"` 25 | ChanId string `json:"chan_id"` 26 | Title string `json:"title"` 27 | Body string `json:"change_log"` 28 | } 29 | 30 | type buildRankItem struct { 31 | Uid int 32 | Nickname string 33 | Icon string 34 | BuildLevel int 35 | BuildExp int 36 | } 37 | 38 | type hallWorld struct { 39 | currentBestGateway string 40 | 41 | onlines map[string]service.ServerOnline 42 | buildRank []buildRankItem 43 | massMails []*Mail // 系统群发的邮件 44 | 45 | maintain struct { 46 | StartTime, EndTime time.Time 47 | notifyTimer *utils.Timer 48 | } 49 | segments []service.GameOnlineSegment 50 | } 51 | 52 | func GetWorld() *hallWorld { 53 | return service.GetWorld((*hallWorld)(nil).GetName()).(*hallWorld) 54 | } 55 | 56 | func init() { 57 | w := &hallWorld{ 58 | onlines: make(map[string]service.ServerOnline), 59 | } 60 | 61 | service.AddWorld(w) 62 | 63 | startTime, _ := config.ParseTime("2010-01-01") 64 | utils.NewPeriodTimer(w.UpdateOnline, startTime, 5*time.Minute) 65 | //utils.NewPeriodTimer(hall.tick1m, startTime, 1*time.Minute) 66 | //utils.NewPeriodTimer(hall.tick1d, startTime, 24*time.Hour) 67 | 68 | w.updateBuildRank() 69 | w.updateMaintain() 70 | 71 | // 加载群发邮件 72 | resp, _ := rpc.CacheClient().QuerySomeMail(context.Background(), 73 | &pb.QuerySomeMailReq{Type: MailTypeMass, Num: 999}) 74 | if resp == nil { 75 | log.Fatal("load mass mail fail") 76 | } 77 | for _, pbMail := range resp.Mails { 78 | mail := &Mail{} 79 | utils.DeepCopy(mail, pbMail) 80 | } 81 | } 82 | 83 | func (w *hallWorld) GetName() string { 84 | return "hall" 85 | } 86 | 87 | func (w *hallWorld) NewPlayer() *service.Player { 88 | p := &hallPlayer{} 89 | p.Player = service.NewPlayer(p) 90 | p.DataObj().Push(p) 91 | 92 | p.mailObj = newMailObj(p) 93 | return p.Player 94 | } 95 | 96 | func (w *hallWorld) GetCurrentOnline() []service.ServerOnline { 97 | data := make([]service.ServerOnline, 0, 16) 98 | for _, g := range w.onlines { 99 | data = append(data, g) 100 | } 101 | return data 102 | } 103 | 104 | func (w *hallWorld) UpdateOnline() { 105 | data := w.GetCurrentOnline() 106 | if len(data) == 0 { 107 | return 108 | } 109 | cmd.Forward("plate", "reportOnline", cmd.M{"servers": data}) 110 | } 111 | 112 | func GetPlayer(uid int) *hallPlayer { 113 | if p := service.GetPlayer(uid); p != nil { 114 | return p.GameAction.(*hallPlayer) 115 | } 116 | return nil 117 | } 118 | 119 | func (w *hallWorld) GetBestGateway() string { 120 | return w.currentBestGateway 121 | } 122 | 123 | func (w *hallWorld) updateBuildRank() { 124 | go func() { 125 | dict, _ := rpc.CacheClient().QueryDict(context.Background(), &pb.QueryDictReq{Key: "build_rank"}) 126 | rpc.OnResponse(func() { 127 | json.Unmarshal(dict.Value, &w.buildRank) 128 | }) 129 | }() 130 | } 131 | 132 | func (w *hallWorld) notifyMaintain() { 133 | secs := time.Until(w.maintain.StartTime).Seconds() 134 | msg := cmd.M{ 135 | "content": "The game will be temporarily closed after {seconds} s.", 136 | "startTs": w.maintain.StartTime.Unix(), 137 | "endTs": w.maintain.EndTime.Unix(), 138 | } 139 | if secs > 60 { 140 | msg["content"] = "The game will be temporarily closed after {clock}." 141 | 142 | utils.StopTimer(w.maintain.notifyTimer) 143 | w.maintain.notifyTimer = utils.NewTimer(w.notifyMaintain, time.Until(w.maintain.StartTime)-time.Minute) 144 | } 145 | log.Infof("maintain broadcast msg %s", msg["content"]) 146 | service.Broadcast2Gateway("maintain", msg) 147 | } 148 | 149 | func (w *hallWorld) updateMaintain() { 150 | go func() { 151 | dict, err := rpc.CacheClient().QueryDict(context.Background(), &pb.QueryDictReq{Key: "maintain"}) 152 | if err != nil { 153 | return 154 | } 155 | 156 | pbData := &pb.Maintain{} 157 | json.Unmarshal([]byte(dict.Value), pbData) 158 | startTime, _ := config.ParseTime(pbData.StartTime) 159 | endTime, _ := config.ParseTime(pbData.EndTime) 160 | rpc.OnResponse(func() { 161 | utils.StopTimer(w.maintain.notifyTimer) 162 | w.maintain.StartTime = startTime 163 | w.maintain.EndTime = endTime 164 | log.Infof("maintain game at %s-%s content %s allow list %s", pbData.StartTime, pbData.EndTime, pbData.Content, pbData.AllowList) 165 | 166 | if d := time.Until(startTime); d > 5*time.Minute { 167 | w.maintain.notifyTimer = utils.NewTimer(w.notifyMaintain, d-5*time.Minute) 168 | } else if d > time.Minute { 169 | w.maintain.notifyTimer = utils.NewTimer(w.notifyMaintain, -time.Minute) 170 | } 171 | }) 172 | }() 173 | } 174 | -------------------------------------------------------------------------------- /init.sql: -------------------------------------------------------------------------------- 1 | -- MySQL 8 2 | 3 | CREATE DATABASE IF NOT EXISTS game; 4 | USE game; 5 | 6 | DROP TABLE IF EXISTS user_info; 7 | CREATE TABLE user_info ( 8 | id INT AUTO_INCREMENT PRIMARY KEY, 9 | nickname VARCHAR(50) NOT NULL, 10 | sex int not null, 11 | icon varchar(255) not null default '', 12 | plate_icon varchar(255) not null default '', 13 | time_zone float not null default 0, 14 | email varchar(64) not null, 15 | ip varchar(32) not null, 16 | client_version varchar(32) not null, 17 | mac varchar(24) not null, 18 | imei varchar(24) not null, 19 | imsi varchar(24) not null, 20 | chan_id varchar(32) not null, 21 | server_location varchar(32) not null default '', 22 | create_time TIMESTAMP not null default current_TIMESTAMP 23 | ); 24 | 25 | DROP TABLE IF EXISTS item_log; 26 | CREATE TABLE item_log ( 27 | id INT AUTO_INCREMENT PRIMARY KEY, 28 | `uid` INT NOT NULL, 29 | item_id INT NOT NULL, 30 | way varchar(64) not null, 31 | num INT NOT NULL, 32 | balance INT NOT NULL, 33 | uuid varchar(64) NOT NULL, 34 | create_time TIMESTAMP NOT NULL 35 | ); 36 | 37 | DROP TABLE IF EXISTS online_log; 38 | CREATE TABLE online_log ( 39 | id INT AUTO_INCREMENT PRIMARY KEY, 40 | `uid` INT NOT NULL, 41 | ip varchar(48) NOT NULL, 42 | imei varchar(18) NOT NULL, 43 | imsi varchar(16) NOT NULL, 44 | chan_id varchar(32) NOT NULL, 45 | client_version varchar(32) NOT NULL, 46 | login_time TIMESTAMP NOT NULL, 47 | offline_time TIMESTAMP 48 | ); 49 | 50 | DROP TABLE IF EXISTS user_plate; 51 | CREATE TABLE user_plate ( 52 | id INT AUTO_INCREMENT PRIMARY KEY, 53 | `uid` INT NOT NULL, 54 | plate varchar(16) not null, 55 | open_id varchar(48) NOT NULL, 56 | create_time TIMESTAMP NOT NULL, 57 | index idx_uid(`uid`), 58 | unique index idx_open_id(open_id) 59 | ); 60 | 61 | DROP TABLE IF EXISTS user_bin; 62 | CREATE TABLE user_bin ( 63 | id INT AUTO_INCREMENT PRIMARY KEY, 64 | `uid` INT NOT NULL, 65 | `class` varchar(16) not null, 66 | bin blob not null, 67 | update_time TIMESTAMP NOT NULL, 68 | unique index idx_uid_class(`uid`,`class`) 69 | ); 70 | 71 | DROP TABLE IF EXISTS mail; 72 | CREATE TABLE mail ( 73 | id INT AUTO_INCREMENT PRIMARY KEY, 74 | `type` INT NOT NULL, 75 | send_uid int not null, 76 | recv_uid int not null, 77 | `status` int not null, 78 | `data` text not null, 79 | send_time TIMESTAMP NOT NULL default current_TIMESTAMP, 80 | index idx_recv_uid(recv_uid) 81 | ); 82 | 83 | DROP TABLE IF EXISTS dict; 84 | CREATE TABLE dict ( 85 | id INT AUTO_INCREMENT PRIMARY KEY, 86 | `key` varchar(32) not null, 87 | `value` JSON not null, 88 | update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 89 | index idx_key(`key`) 90 | ); 91 | 92 | CREATE DATABASE IF NOT EXISTS manage; 93 | USE manage; 94 | 95 | DROP TABLE IF EXISTS gm_table; 96 | CREATE TABLE gm_table ( 97 | id INT AUTO_INCREMENT PRIMARY KEY, 98 | `name` varchar(32) not null, 99 | `version` int NOT NULL, 100 | content text not null, 101 | update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 102 | index idx_name(`name`) 103 | ); 104 | 105 | DROP TABLE IF EXISTS gm_script; 106 | CREATE TABLE gm_script ( 107 | id INT AUTO_INCREMENT PRIMARY KEY, 108 | `name` varchar(32) not null, 109 | body text not null, 110 | update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 111 | index idx_name(`name`) 112 | ); -------------------------------------------------------------------------------- /internal/cardutils/card_test.go: -------------------------------------------------------------------------------- 1 | package cardutils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCheat(t *testing.T) { 8 | AddCardSystem("test", []int{1, 1, 2, 2, 3, 3, 1, 2, 3}) 9 | cs := NewCardSet("test") 10 | 11 | samples := [][]int{ 12 | {3, 2, 1}, 13 | {1, 1, 2}, 14 | {3, 2, 1, 1, 1, 2}, 15 | {1, 1, 1}, 16 | {1, 1, 2}, 17 | {1, 1, 1}, 18 | {1, 1, 1}, 19 | } 20 | 21 | for _, sample := range samples { 22 | cs.Shuffle() 23 | cs.Cheat(sample[:2]...) 24 | cs.Cheat(sample[2:]...) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/dbo/pool.go: -------------------------------------------------------------------------------- 1 | // database pool 2 | package dbo 3 | 4 | import ( 5 | "database/sql/driver" 6 | "encoding/json" 7 | "fmt" 8 | "sync" 9 | "time" 10 | 11 | "google.golang.org/protobuf/proto" 12 | "gorm.io/driver/mysql" 13 | "gorm.io/gorm" 14 | "gorm.io/gorm/schema" 15 | ) 16 | 17 | const ( 18 | defaultIdleConns = 100 19 | defaultOpenConns = 200 20 | defaultConnLifeTime = 1800 * time.Second // MySQL默认8小时 21 | ) 22 | 23 | type Pool struct { 24 | db *gorm.DB 25 | User string 26 | Password string 27 | Addr string 28 | SchemaName string 29 | 30 | mu sync.RWMutex 31 | } 32 | 33 | func (dbPool *Pool) SetSource(user, password, addr, dbname string) { 34 | dbPool.User = user 35 | dbPool.Password = password 36 | dbPool.Addr = addr 37 | dbPool.SchemaName = dbname 38 | 39 | } 40 | func (p *Pool) createDatabase() { 41 | dsn := fmt.Sprintf("%s:%s@tcp(%s)/?charset=utf8mb4&parseTime=True&loc=Local", p.User, p.Password, p.Addr) 42 | ormDB, err := gorm.Open(mysql.Open(dsn)) 43 | if err != nil { 44 | panic(err) 45 | } 46 | if err := ormDB.Exec("create database if not exists " + p.SchemaName).Error; err != nil { 47 | panic(err) 48 | } 49 | if err := ormDB.Exec("use " + p.SchemaName).Error; err != nil { 50 | panic(err) 51 | } 52 | db, _ := ormDB.DB() 53 | db.Close() 54 | } 55 | 56 | func (p *Pool) Get() *gorm.DB { 57 | p.mu.RLock() 58 | ormDB := p.db 59 | p.mu.RUnlock() 60 | 61 | if ormDB != nil { 62 | return ormDB 63 | } 64 | 65 | p.mu.Lock() 66 | defer p.mu.Unlock() 67 | p.createDatabase() 68 | 69 | dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", p.User, p.Password, p.Addr, p.SchemaName) 70 | ormDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{NamingStrategy: schema.NamingStrategy{SingularTable: true}}) 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | db, _ := ormDB.DB() 76 | db.SetMaxIdleConns(defaultIdleConns) 77 | db.SetMaxOpenConns(defaultOpenConns) 78 | db.SetConnMaxLifetime(defaultConnLifeTime) 79 | p.db = ormDB 80 | return p.db 81 | } 82 | 83 | func NewPool() *Pool { 84 | p := &Pool{} 85 | return p 86 | } 87 | 88 | var dbPool = NewPool() 89 | 90 | func SetSource(user, password, addr, dbname string) { 91 | dbPool.SetSource(user, password, addr, dbname) 92 | } 93 | 94 | func Get() *gorm.DB { 95 | return dbPool.Get() 96 | } 97 | 98 | type jsonValue struct { 99 | ptr any 100 | } 101 | 102 | func (jv *jsonValue) Scan(value any) error { 103 | if value != nil { 104 | if buf, ok := value.([]byte); ok { 105 | return json.Unmarshal(buf, jv.ptr) 106 | } 107 | } 108 | return nil 109 | } 110 | 111 | func (jv *jsonValue) Value() (driver.Value, error) { 112 | return json.Marshal(jv.ptr) 113 | } 114 | 115 | func JSON(ptr any) *jsonValue { 116 | return &jsonValue{ptr: ptr} 117 | } 118 | 119 | type pbValue struct { 120 | ptr proto.Message 121 | } 122 | 123 | func (pv *pbValue) Scan(value any) error { 124 | if value != nil { 125 | if buf, ok := value.([]byte); ok { 126 | return proto.Unmarshal(buf, pv.ptr) 127 | } 128 | } 129 | return nil 130 | } 131 | 132 | func (pv *pbValue) Value() (driver.Value, error) { 133 | return proto.Marshal(pv.ptr) 134 | } 135 | 136 | func PB(ptr proto.Message) *pbValue { 137 | return &pbValue{ptr: ptr} 138 | } 139 | -------------------------------------------------------------------------------- /internal/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "github.com/guogeer/quasar/v2/config" 5 | ) 6 | 7 | type DataSource struct { 8 | User string `xml:"User" yaml:"user"` 9 | Password string `xml:"Password" yaml:"password"` 10 | Addr string `xml:"Address" yaml:"address"` 11 | Name string `xml:"Name" yaml:"name"` 12 | MaxIdleConns int `yaml:"maxIdleConns"` 13 | MaxOpenConns int `yaml:"maxOpenConns"` 14 | } 15 | 16 | type Env struct { 17 | Version int `yaml:"version"` 18 | Environment string `yaml:"environment"` 19 | DB struct { 20 | Game DataSource `yaml:"game"` 21 | Manage DataSource `yaml:"manage"` 22 | } `yaml:"db"` 23 | ProductName string `yaml:"productName"` 24 | ScriptPath string `yaml:"scriptPath"` 25 | TablePath string `yaml:"tablePath"` 26 | } 27 | 28 | var defaultConfig Env 29 | 30 | func init() { 31 | config.LoadFile(config.Config().Path(), &defaultConfig) 32 | } 33 | 34 | func Config() *Env { 35 | return &defaultConfig 36 | } 37 | -------------------------------------------------------------------------------- /internal/errcode/errcode.go: -------------------------------------------------------------------------------- 1 | package errcode 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/guogeer/quasar/v2/config" 7 | ) 8 | 9 | type Error interface { 10 | GetCode() string 11 | Error() string 12 | } 13 | 14 | type BaseError struct { 15 | Code string `json:"code,omitempty"` 16 | Msg string `json:"msg,omitempty"` 17 | } 18 | 19 | func (e BaseError) GetCode() string { 20 | return e.Code 21 | } 22 | 23 | func (e BaseError) Error() string { 24 | return e.Msg 25 | } 26 | 27 | func New(code, msg string) *BaseError { 28 | e := &BaseError{Code: code, Msg: msg} 29 | return e 30 | } 31 | 32 | type itemError struct { 33 | BaseError 34 | ItemId int `json:"itemId,omitempty"` 35 | } 36 | 37 | func MoreItem(itemId int) Error { 38 | itemName, _ := config.String("item", itemId, "name") 39 | e := *moreItem 40 | e.Msg = strings.ReplaceAll(e.Msg, "{itemName}", itemName) 41 | return &itemError{ItemId: itemId, BaseError: e} 42 | } 43 | 44 | func TooMuchItem(itemId int) Error { 45 | itemName, _ := config.String("item", itemId, "name") 46 | e := *tooMuchItem 47 | e.Msg = strings.ReplaceAll(e.Msg, "{itemName}", itemName) 48 | return &itemError{ItemId: itemId, BaseError: e} 49 | } 50 | 51 | const CodeOk = "ok" 52 | 53 | var Retry = New("retry", "catch error, please retry") 54 | var moreItem = New("more_item", "more item {itemName}") 55 | var tooMuchItem = New("too_much_item", "too much item {itemName}") 56 | -------------------------------------------------------------------------------- /internal/gameutils/item.go: -------------------------------------------------------------------------------- 1 | package gameutils 2 | 3 | import ( 4 | "encoding/json" 5 | "sort" 6 | ) 7 | 8 | const ( 9 | ItemIdGold = 1001 10 | ItemIdExp = 1002 11 | ) 12 | 13 | type Item interface { 14 | GetId() int 15 | GetNum() int64 16 | Merge(Item) bool 17 | Multi(n int) 18 | } 19 | 20 | type NumericItem struct { 21 | Id int `json:"id,omitempty"` 22 | Num int64 `json:"num,omitempty"` 23 | } 24 | 25 | func (item *NumericItem) Merge(mergeItem Item) bool { 26 | if item.Id != mergeItem.GetId() { 27 | return false 28 | } 29 | item.Num += mergeItem.GetNum() 30 | return true 31 | } 32 | 33 | func (item *NumericItem) GetId() int { 34 | return item.Id 35 | } 36 | 37 | func (item *NumericItem) GetNum() int64 { 38 | return item.Num 39 | } 40 | 41 | func (item *NumericItem) Multi(n int) { 42 | item.Num *= int64(n) 43 | } 44 | 45 | // 格式:[[1000,1],[1001,2]] 46 | func ParseNumbericItems(s string) []Item { 47 | var items []Item 48 | var a [][2]int64 49 | json.Unmarshal([]byte(s), &a) 50 | for _, v := range a { 51 | items = append(items, &NumericItem{Id: int(v[0]), Num: v[1]}) 52 | } 53 | return items 54 | } 55 | 56 | func MergeItems(items []Item) []Item { 57 | sort.Slice(items, func(i, j int) bool { 58 | return items[i].GetId() < items[j].GetId() 59 | }) 60 | 61 | var mergeItems []Item 62 | for i, j := 0, 0; i < len(items); i = j { 63 | for j = i + 1; j < len(items) && items[i].GetId() == items[j].GetId(); j++ { 64 | if items[j].GetNum() != 0 && !items[i].Merge(items[j]) { 65 | mergeItems = append(mergeItems, items[j]) 66 | } 67 | } 68 | if items[i].GetNum() != 0 { 69 | mergeItems = append(mergeItems, items[i]) 70 | } 71 | } 72 | 73 | return mergeItems 74 | } 75 | 76 | func CountItems(items []Item, itemId int) int64 { 77 | var num int64 78 | for _, item := range items { 79 | if item.GetId() == itemId { 80 | num += item.GetNum() 81 | } 82 | } 83 | return num 84 | } 85 | -------------------------------------------------------------------------------- /internal/gameutils/jwt.go: -------------------------------------------------------------------------------- 1 | package gameutils 2 | 3 | import ( 4 | "gofishing-game/internal/errcode" 5 | 6 | "github.com/guogeer/quasar/v2/config" 7 | 8 | "github.com/golang-jwt/jwt/v4" 9 | ) 10 | 11 | var defaultJWT *JWT 12 | 13 | var errMalformedToken = errcode.New("malformed_jwt_token", "malformed jwt token") 14 | var errInvalidToken = errcode.New("invalid_jwt_token", "invalid jwt token") 15 | var errExpiredToken = errcode.New("expired_jwt_token", "expired jwt token") 16 | var errUnavailableToken = errcode.New("unavailable_jwt_token", "unavailable jwt token") 17 | var errUnknowToken = errcode.New("unknow_jwt_token", "unknow jwt token") 18 | 19 | type JWT struct { 20 | key []byte 21 | } 22 | 23 | func init() { 24 | defaultJWT = &JWT{key: []byte(config.Config().ServerKey)} 25 | } 26 | 27 | type CustomClaims struct { 28 | Uid int `json:"uid"` 29 | jwt.RegisteredClaims 30 | } 31 | 32 | func (j *JWT) CreateToken(claims CustomClaims) (string, error) { 33 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 34 | return token.SignedString(j.key) 35 | } 36 | 37 | func (j *JWT) ParserToken(tokenString string) (*CustomClaims, errcode.Error) { 38 | token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (any, error) { 39 | return j.key, nil 40 | }) 41 | 42 | if err != nil { 43 | if ve, ok := err.(*jwt.ValidationError); ok { 44 | if ve.Errors&jwt.ValidationErrorMalformed != 0 { 45 | return nil, errMalformedToken 46 | } else if ve.Errors&jwt.ValidationErrorExpired != 0 { 47 | return nil, errExpiredToken 48 | } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 { 49 | return nil, errInvalidToken 50 | } else { 51 | return nil, errUnavailableToken 52 | } 53 | } 54 | } 55 | if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { 56 | return claims, nil 57 | } 58 | return nil, errUnknowToken 59 | } 60 | 61 | func ValidateToken(token string) (int, errcode.Error) { 62 | if token == "" { 63 | return 0, errcode.New("empty_token", "empty token") 64 | } 65 | claims, err := defaultJWT.ParserToken(token) 66 | if err != nil { 67 | return 0, err 68 | } 69 | return claims.Uid, nil 70 | } 71 | 72 | func CreateToken(uid int) string { 73 | token, _ := defaultJWT.CreateToken(CustomClaims{Uid: uid}) 74 | return token 75 | } 76 | -------------------------------------------------------------------------------- /internal/gameutils/time.go: -------------------------------------------------------------------------------- 1 | package gameutils 2 | 3 | import ( 4 | "encoding/json" 5 | // "github.com/guogeer/quasar/v2/log" 6 | "time" 7 | ) 8 | 9 | const MaxDelayDuration = 1200 * time.Millisecond 10 | 11 | type CD struct { 12 | ExpireMs int64 13 | PeriodMs int64 14 | unit time.Duration // 单位,默认ms 15 | } 16 | 17 | func NewCD(d time.Duration, unit ...time.Duration) *CD { 18 | if d < 0 { 19 | d = 0 20 | } 21 | 22 | unit2 := time.Millisecond 23 | for _, u := range unit { 24 | unit2 = u 25 | } 26 | return &CD{ 27 | ExpireMs: time.Now().Add(d).UnixNano() / 1e6, 28 | PeriodMs: d.Milliseconds(), 29 | unit: unit2, 30 | } 31 | } 32 | 33 | func (cd *CD) IsValid() bool { 34 | process := cd.process() 35 | return process[0] > 0 36 | } 37 | 38 | // 当前进度[剩余时间,总时间] 39 | func (cd *CD) process() []int { 40 | expireTime := time.Unix(cd.ExpireMs/1000, cd.ExpireMs%1000*1e6) 41 | d := time.Until(expireTime) 42 | if d < 0 { 43 | d = 0 44 | } 45 | unit := cd.unit 46 | if unit == 0 { 47 | unit = time.Millisecond 48 | } 49 | period := int(time.Duration(cd.PeriodMs) * time.Millisecond / unit) 50 | offset := int(float64(d+unit-1) / float64(unit)) 51 | if offset > period { 52 | offset = period 53 | } 54 | return []int{offset, period} 55 | } 56 | 57 | func (cd *CD) MarshalJSON() ([]byte, error) { 58 | return json.Marshal(cd.process()) 59 | } 60 | 61 | type Clock struct { 62 | t time.Time 63 | unit time.Duration 64 | } 65 | 66 | func NewClock(d time.Duration, unit ...time.Duration) *Clock { 67 | unit2 := time.Millisecond 68 | for _, u := range unit { 69 | unit2 = u 70 | } 71 | return &Clock{t: time.Now().Add(d), unit: unit2} 72 | } 73 | 74 | func (c Clock) IsValid() bool { 75 | return !time.Now().After(c.t) 76 | } 77 | 78 | func (c Clock) MarshalJSON() ([]byte, error) { 79 | unit := c.unit 80 | d := time.Until(c.t) 81 | offset := int(float64(d+unit-1) / float64(unit)) 82 | if offset < 0 { 83 | offset = 0 84 | } 85 | // log.Debug(d, unit, int(d/unit), float64(d)/float64(unit), offset) 86 | return json.Marshal(offset) 87 | } 88 | -------------------------------------------------------------------------------- /internal/gameutils/time_test.go: -------------------------------------------------------------------------------- 1 | package gameutils 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/guogeer/quasar/v2/utils" 9 | ) 10 | 11 | func TestCD(t *testing.T) { 12 | cd1 := NewCD(10000 * time.Millisecond) 13 | if !utils.EqualJSON(cd1, []int{10000, 10000}) { 14 | t.Error("cd wrong") 15 | } 16 | } 17 | 18 | func TestClock(t *testing.T) { 19 | c := NewClock(1500 * time.Millisecond) 20 | if !utils.EqualJSON(c, 1500) { 21 | t.Error(json.Marshal(c)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/gameutils/util.go: -------------------------------------------------------------------------------- 1 | package gameutils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "gofishing-game/internal/errcode" 7 | "maps" 8 | "reflect" 9 | ) 10 | 11 | func InitNilFields(obj any) { 12 | if obj == nil { 13 | return 14 | } 15 | 16 | objv := reflect.Indirect(reflect.ValueOf(obj)) 17 | if objv.Kind() != reflect.Struct { 18 | return 19 | } 20 | 21 | for i := 0; i < objv.NumField(); i++ { 22 | objfield := objv.Field(i) 23 | if objfield.CanSet() && objfield.Kind() == reflect.Map && objfield.IsNil() { 24 | objfield.Set(reflect.MakeMap(objfield.Type())) 25 | } 26 | if objfield.CanSet() && objfield.Kind() == reflect.Ptr && objfield.IsNil() { 27 | objfield.Set(reflect.New(objfield.Type().Elem())) 28 | } 29 | } 30 | } 31 | 32 | func marshalJSONObjects(objs ...any) ([]byte, error) { 33 | result := map[string]json.RawMessage{} 34 | for _, obj := range objs { 35 | buf, _ := json.Marshal(obj) 36 | 37 | m := map[string]json.RawMessage{} 38 | if bytes.HasPrefix(buf, []byte("{")) { 39 | json.Unmarshal(buf, &m) 40 | maps.Copy(result, m) 41 | } 42 | } 43 | return json.Marshal(result) 44 | } 45 | 46 | func MergeError(e errcode.Error, objs ...any) []byte { 47 | if e == nil { 48 | e = errcode.New(errcode.CodeOk, "success") 49 | } 50 | mergeData := append([]any{e}, objs...) 51 | buf, _ := marshalJSONObjects(mergeData...) 52 | return buf 53 | } 54 | -------------------------------------------------------------------------------- /internal/gameutils/util_test.go: -------------------------------------------------------------------------------- 1 | package gameutils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/guogeer/quasar/v2/utils" 7 | ) 8 | 9 | func TestInitNilFields(t *testing.T) { 10 | type AA struct { 11 | NA int 12 | } 13 | type A struct { 14 | M map[string]int32 15 | AA *AA 16 | AA2 AA 17 | MA map[string]*AA 18 | N *int 19 | } 20 | a := &A{} 21 | n := 0 22 | ans := &A{ 23 | M: map[string]int32{}, 24 | AA: &AA{}, 25 | AA2: AA{}, 26 | MA: map[string]*AA{}, 27 | N: &n, 28 | } 29 | InitNilFields(a) 30 | if !utils.EqualJSON(a, ans) { 31 | t.Error(a, ans) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/internal.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "reflect" 4 | 5 | const ( 6 | LongDateFmt = "2006-01-02 15:04:05" 7 | ShortDateFmt = "2006-01-02" 8 | ) 9 | 10 | func IndexArrayFunc(array any, equal func(i int) bool) int { 11 | arrayValues := reflect.ValueOf(array) 12 | for i := 0; i < arrayValues.Len(); i++ { 13 | if equal(i) { 14 | return i 15 | } 16 | } 17 | return -1 18 | } 19 | -------------------------------------------------------------------------------- /internal/pb/login.proto: -------------------------------------------------------------------------------- 1 | // 登陆 2 | 3 | syntax = "proto3"; 4 | 5 | package pb; 6 | 7 | option go_package=".;pb"; 8 | 9 | message AccountInfo { 10 | int32 uid = 1; 11 | string address = 3; 12 | string account = 4; 13 | string password = 5; 14 | string plate = 6; 15 | string openId = 7; 16 | string chanId = 9; 17 | string nickname = 10; 18 | int32 sex = 11; 19 | int32 vip = 12; 20 | string icon = 13; 21 | string imsi = 14; 22 | string imei = 15; 23 | string mac = 16; 24 | string phone = 17; 25 | string osVersion = 18; 26 | string netMode = 19; 27 | string clientVersion = 20; 28 | string phoneBrand = 21; 29 | string iosIDFA = 24; // IOS设备标识 30 | int32 subId = 25; 31 | string ip = 26; 32 | string email = 36; 33 | string plateIcon = 37; 34 | } 35 | 36 | message CreateAccountReq { 37 | AccountInfo info = 1; 38 | } 39 | 40 | message CreateAccountResp { 41 | int32 uid = 1; 42 | int32 newUserId = 2; 43 | string chanId = 3; 44 | } 45 | 46 | message LoginParams { 47 | double timeZone = 1; 48 | } 49 | 50 | message LoginParamsReq { 51 | int32 uid = 1; 52 | LoginParams params = 2; 53 | } 54 | 55 | message BindAccountReq { 56 | string reserveOpenId = 1; 57 | string addPlate = 2; 58 | string addOpenId = 3; 59 | } 60 | 61 | message BindAccountResp { 62 | int32 uid = 1; 63 | string reserveOpenId = 2; 64 | repeated string plates = 3; 65 | } 66 | 67 | 68 | message BindPlateReq { 69 | int32 uid = 1; 70 | string plate = 2; 71 | } 72 | 73 | message BindPlateResp { 74 | repeated string plates = 1; 75 | } 76 | 77 | message AddLoginLogReq { 78 | int32 uid = 1; 79 | string loginTime = 2; 80 | string ip = 3; 81 | string mac = 4; 82 | string imei = 5; 83 | string imsi = 6; 84 | string chanId = 7; 85 | string clientVersion = 8; 86 | } 87 | 88 | message AddLeaveLogReq { 89 | int32 uid = 1; 90 | string leaveTime = 2; 91 | } 92 | 93 | message ClearAccountReq { 94 | int32 uid = 1; 95 | } 96 | 97 | message QueryLoginParamsReq { 98 | int32 uid = 1; 99 | } 100 | 101 | message QueryLoginParamsResp { 102 | LoginParams params = 1; 103 | } 104 | 105 | message UpdateLoginParamsReq { 106 | int32 uid = 1; 107 | LoginParams params = 2; 108 | } 109 | 110 | message AuthReq { 111 | int32 uid = 1; 112 | string ip = 2; 113 | } 114 | 115 | message AuthResp { 116 | string token = 1; 117 | int32 reason = 2; 118 | string clientVersion = 5; // 版本 119 | string loginTime = 6; // 登陆时间 120 | repeated string loginPlates = 7; 121 | string serverLocation = 8; 122 | } -------------------------------------------------------------------------------- /internal/pb/userdata.proto: -------------------------------------------------------------------------------- 1 | 2 | 3 | syntax = "proto3"; 4 | 5 | package pb; 6 | 7 | option go_package = ".;pb"; 8 | 9 | message CD { 10 | int64 expireMs = 1; // 过期时间,单位ms 11 | int64 periodMs = 2; // 周期单位ms 12 | } 13 | 14 | message SimpleUserInfo { 15 | int32 uid = 1; 16 | string nickname = 2; 17 | string icon = 3; 18 | int32 vip = 4; 19 | int32 sex = 5; 20 | int32 level = 6; 21 | } 22 | 23 | message UserInfo { 24 | int32 uid = 1; 25 | string serverLocation = 3; 26 | string createTime = 4; 27 | string chanId = 5; 28 | string openId = 6; 29 | int32 sex = 8; 30 | string icon = 9; 31 | string plateIcon = 10; 32 | string nickname = 11; 33 | } 34 | 35 | message Task { 36 | int32 id = 1; 37 | int64 num = 2; // 当前进度 38 | int64 total = 3; // 总进度 39 | int32 status = 4; 40 | } 41 | 42 | message NumericItem { 43 | int32 id = 1; 44 | int64 num = 2; 45 | int64 balance = 3; 46 | } 47 | 48 | message Bag { 49 | repeated NumericItem numericItems = 1; 50 | } 51 | 52 | message GlobalBin { 53 | int64 lastDayUpdateTs = 1; // 每日数据上次更新时间 54 | Bag bag = 2; // 背包 55 | int32 level = 3; // 等级 56 | repeated Task tasks = 5; 57 | DailySignIn signIn = 6; // 签到数据 58 | } 59 | 60 | // 离线数据 61 | message OfflineBin { 62 | repeated NumericItem items = 2; 63 | } 64 | 65 | message UserBin { 66 | HallBin hall = 1; // 大厅数据 67 | GlobalBin global = 2; // 全局数据 68 | OfflineBin offline = 3; // 离线数据 69 | RoomBin room = 4; // 房间数据 70 | } 71 | 72 | message RoomBin { 73 | FingerGuessingRoom fingerGuessing = 1; 74 | } 75 | 76 | // 大厅的数据 77 | message HallBin { 78 | string loginClientVersion = 4; // 上次登陆的版本 79 | int64 lastMassMail = 10; // 上次群发邮件的ID 80 | } 81 | 82 | message FingerGuessingRoom { 83 | int32 winPlay = 1; 84 | int32 losePlay = 2; 85 | int32 totalPlay = 3; 86 | } 87 | 88 | // 签到 89 | message DailySignIn { 90 | int64 drawTs = 1; 91 | int64 startTs = 2; 92 | int32 drawState = 3; 93 | } -------------------------------------------------------------------------------- /internal/rpc/cache.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "time" 7 | 8 | "gofishing-game/internal/env" 9 | "gofishing-game/internal/pb" 10 | 11 | "github.com/guogeer/quasar/v2/cmd" 12 | "github.com/guogeer/quasar/v2/config" 13 | "github.com/guogeer/quasar/v2/log" 14 | "google.golang.org/grpc" 15 | "google.golang.org/grpc/credentials/insecure" 16 | ) 17 | 18 | var defaultCacheClient pb.CacheClient 19 | 20 | func init() { 21 | opts := []grpc.DialOption{ 22 | grpc.WithTransportCredentials(insecure.NewCredentials()), 23 | } 24 | 25 | addr, err := cmd.RequestServerAddr("cache") 26 | if err != nil { 27 | log.Fatalf("cache or router service is unavailable %v", err) 28 | } 29 | log.Infof("request server cache addr: %s", addr) 30 | 31 | for { 32 | conn, err := grpc.Dial(addr, opts...) 33 | if err == nil { 34 | defaultCacheClient = pb.NewCacheClient(conn) 35 | break 36 | } 37 | time.Sleep(5 * time.Second) 38 | } 39 | log.Debugf("connect rpc server successfully.") 40 | // 优先加载本地配置 41 | config.LoadLocalTables(env.Config().TablePath) 42 | // 如果DB存在相同的配置表,将覆盖替换本地的拷贝 43 | loadRemoteTables() 44 | 45 | cmd.Bind("func_effectConfigTable", funcEffectConfigTable, (*tableArgs)(nil), cmd.WithoutQueue()) 46 | } 47 | 48 | func CacheClient() pb.CacheClient { 49 | return defaultCacheClient 50 | } 51 | 52 | /*func LoadRemoteScripts() error { 53 | stream, err := CacheClient().LoadAllScript(context.Background(), &pb.Request{}) 54 | if err != nil { 55 | return err 56 | } 57 | for { 58 | f, err := stream.Recv() 59 | if err == io.EOF { 60 | break 61 | } 62 | if err != nil { 63 | return err 64 | } 65 | // 移除脚本远程加载 66 | if err = script.LoadString(f.Name, f.Body); err != nil { 67 | return err 68 | } 69 | } 70 | return nil 71 | } 72 | */ 73 | 74 | func loadRemoteTables() { 75 | stream, err := CacheClient().LoadAllTable(context.Background(), &pb.EmptyReq{}) 76 | if err != nil { 77 | log.Fatalf("load all table config %v", err) 78 | } 79 | for { 80 | table, err := stream.Recv() 81 | if err == io.EOF { 82 | break 83 | } 84 | if err != nil { 85 | log.Fatalf("pull table config %v", err) 86 | } 87 | err = config.LoadTable(table.Name, []byte(table.Content)) 88 | if err != nil { 89 | log.Fatalf("load table config %v", err) 90 | } 91 | } 92 | } 93 | 94 | type tableArgs struct { 95 | Name string `json:"name,omitempty"` 96 | Tables []string `json:"tables,omitempty"` 97 | } 98 | 99 | // 直接使用TCP协议发送,会有64K大小限制 100 | func funcEffectConfigTable(ctx *cmd.Context, data any) { 101 | args := data.(*tableArgs) 102 | for _, name := range args.Tables { 103 | resp, err := CacheClient().LoadTable(context.Background(), &pb.LoadTableReq{Name: name}) 104 | if err != nil { 105 | log.Errorf("load table %s error: %v", name, err) 106 | return 107 | } 108 | 109 | log.Info("effect config table", name) 110 | log.Info(resp.File.Content) 111 | config.LoadTable(name, []byte(resp.File.Content)) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /internal/rpc/call.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | type rpcHandler func() 4 | 5 | type rpcQueue struct { 6 | q chan rpcHandler 7 | } 8 | 9 | var mainQueue = &rpcQueue{ 10 | q: make(chan rpcHandler, 8<<10), 11 | } 12 | 13 | func (rq *rpcQueue) RunOnce() { 14 | for i := 0; i < 3; i++ { 15 | select { 16 | case h := <-rq.q: 17 | h() 18 | default: 19 | return 20 | } 21 | } 22 | } 23 | 24 | func (rq *rpcQueue) Push(h func()) { 25 | rq.q <- (rpcHandler)(h) 26 | } 27 | 28 | func OnResponse(h func()) { 29 | mainQueue.Push(h) 30 | } 31 | 32 | func RunOnce() { 33 | mainQueue.RunOnce() 34 | } 35 | -------------------------------------------------------------------------------- /login/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/guogeer/quasar/v2/cmd" 9 | "github.com/guogeer/quasar/v2/log" 10 | ) 11 | 12 | var port = flag.Int("port", 9501, "the server port") 13 | var rootpath = flag.String("rootpath", "", "root path") 14 | 15 | func main() { 16 | flag.Parse() 17 | 18 | addr := fmt.Sprintf(":%d", *port) 19 | log.Infof("start login server, listen %s", addr) 20 | cmd.RegisterService(&cmd.ServiceConfig{ 21 | Name: "login", Addr: addr, 22 | }) 23 | log.Debugf("register service login addr %s", addr) 24 | 25 | r := gin.Default() 26 | if *rootpath != "" { 27 | r.Static("/", *rootpath) 28 | } 29 | RegisterHandlers(r) 30 | if err := r.Run(addr); err != nil { 31 | log.Fatal(err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /login/user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/json" 7 | "io" 8 | "net/http" 9 | "strings" 10 | 11 | _ "gofishing-game/internal/rpc" 12 | 13 | "github.com/guogeer/quasar/v2/cmd" 14 | "github.com/guogeer/quasar/v2/log" 15 | 16 | "github.com/guogeer/quasar/v2/config" 17 | ) 18 | 19 | var gRandNames []string 20 | var table = [...]byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'} 21 | 22 | func init() { 23 | for _, rowId := range config.Rows("robot") { 24 | var nickname, icon string 25 | config.Scan("robot", rowId, "nickname,icon", &nickname, &icon) 26 | if icon == "" { 27 | gRandNames = append(gRandNames, nickname) 28 | } 29 | } 30 | } 31 | 32 | func RandStringNum(max int) string { 33 | nums := make([]byte, max) 34 | n, err := io.ReadAtLeast(rand.Reader, nums, max) 35 | if n != max { 36 | log.Error(err) 37 | } 38 | for i := 0; i < len(nums); i++ { 39 | nums[i] = table[int(nums[i])%len(table)] 40 | } 41 | return string(nums) 42 | } 43 | 44 | func GetRandName(sex int) string { 45 | return strings.Join([]string{"guest", RandStringNum(6)}, "_") 46 | } 47 | 48 | func saveFacebookIcon(iconName string, path string) (string, error) { 49 | var response struct{ Path string } 50 | 51 | err := requestPlate("/plate/save_icon", cmd.M{"iconName": iconName, "path": path}, &response) 52 | if err != nil { 53 | return "", err 54 | } 55 | return response.Path, nil 56 | } 57 | 58 | func requestPlate(uri string, in, out any) error { 59 | addr, err := cmd.RequestServerAddr("plate") 60 | if err != nil { 61 | return err 62 | } 63 | 64 | data, _ := cmd.Encode("", in) 65 | resp, err := http.Post("http://"+addr+uri, "application/json", bytes.NewBuffer(data)) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | buf, _ := io.ReadAll(resp.Body) 71 | pkg, _ := cmd.Decode(buf) 72 | return json.Unmarshal(pkg.Data, &out) 73 | } 74 | -------------------------------------------------------------------------------- /service/action.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "strings" 5 | 6 | "gofishing-game/internal/gameutils" 7 | ) 8 | 9 | type ActionConstructor func(player *Player) EnterAction 10 | 11 | var ( 12 | actionKeys []string // AddAction调用顺序 13 | actionConstructors = map[string]ActionConstructor{} 14 | ) 15 | 16 | // BeforeEnter在GameAction前调用 17 | func AddAction(key string, h ActionConstructor) { 18 | key = strings.ToLower(key) 19 | actionKeys = append(actionKeys, key) 20 | actionConstructors[key] = h 21 | } 22 | 23 | type EnterAction interface { 24 | BeforeEnter() 25 | } 26 | 27 | func (player *Player) GetAction(key string) EnterAction { 28 | key = strings.ToLower(key) 29 | return player.enterActions[key] 30 | } 31 | 32 | type actionLevelUp interface { 33 | OnLevelUp(reason string) 34 | } 35 | 36 | type actionAddItems interface { 37 | OnAddItems(items []gameutils.Item, way string) 38 | } 39 | 40 | type actionClose interface { 41 | OnClose() 42 | } 43 | 44 | type actionLeave interface { 45 | OnLeave() 46 | } 47 | -------------------------------------------------------------------------------- /service/bag.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | // 背包逻辑更新 4 | 5 | import ( 6 | "gofishing-game/internal/gameutils" 7 | "gofishing-game/internal/pb" 8 | "strings" 9 | 10 | "github.com/guogeer/quasar/v2/config" 11 | ) 12 | 13 | // 在item物品表出现 14 | func isItemValid(id int) bool { 15 | if _, ok := config.Int("item", id, "id"); !ok { 16 | return false 17 | } 18 | s, _ := config.String("item", id, "visible") 19 | return strings.ToLower(s) != "no" 20 | } 21 | 22 | type itemOption struct { 23 | nolog bool 24 | } 25 | 26 | type itemOptionFunc func(*itemOption) 27 | 28 | func WithNoItemLog() itemOptionFunc { 29 | return func(opt *itemOption) { 30 | opt.nolog = true 31 | } 32 | } 33 | 34 | type bagObj struct { 35 | player *Player 36 | 37 | items []gameutils.Item 38 | offlineItems []*pb.NumericItem 39 | } 40 | 41 | func newBagObj(player *Player) *bagObj { 42 | obj := &bagObj{ 43 | player: player, 44 | } 45 | obj.player.DataObj().Push(obj) 46 | return obj 47 | } 48 | 49 | func (obj *bagObj) BeforeEnter() { 50 | obj.clearEmptyItems() 51 | } 52 | 53 | // 移除空物品 54 | func (obj *bagObj) clearEmptyItems() { 55 | obj.items = gameutils.MergeItems(obj.items) 56 | } 57 | 58 | func (obj *bagObj) GetItems() []gameutils.Item { 59 | items := make([]gameutils.Item, 0, 4) 60 | for _, item := range obj.items { 61 | if item.GetNum() != 0 { 62 | items = append(items, item) 63 | } 64 | } 65 | return items 66 | } 67 | 68 | func (obj *bagObj) IsEnough(id int, num int64) bool { 69 | return obj.NumItem(id) >= num 70 | } 71 | 72 | func (obj *bagObj) Add(id int, num int64, way string, fns ...itemOptionFunc) { 73 | obj.AddSomeItems([]gameutils.Item{&gameutils.NumericItem{Id: id, Num: num}}, way, fns...) 74 | } 75 | 76 | func (obj *bagObj) NumItem(id int) int64 { 77 | for _, item := range obj.items { 78 | if item.GetId() == id { 79 | return item.GetNum() 80 | } 81 | } 82 | return 0 83 | } 84 | 85 | func (obj *bagObj) CostSomeItems(items []gameutils.Item, way string) { 86 | for _, item := range items { 87 | item.Multi(-1) 88 | } 89 | obj.AddSomeItems(items, way) 90 | } 91 | 92 | func (obj *bagObj) AddSomeItems(items []gameutils.Item, way string, fns ...itemOptionFunc) { 93 | for _, item := range items { 94 | obj.addItem(item) 95 | } 96 | obj.items = gameutils.MergeItems(obj.items) 97 | obj.player.GameAction.OnAddItems(items, way) 98 | 99 | opt := &itemOption{} 100 | for _, fn := range fns { 101 | fn(opt) 102 | } 103 | if !opt.nolog { 104 | AddSomeItemLog(obj.player.Id, items, way) 105 | } 106 | } 107 | 108 | func (obj *bagObj) GetItem(id int) gameutils.Item { 109 | for _, item := range obj.items { 110 | if item.GetId() == id { 111 | return item 112 | } 113 | } 114 | return nil 115 | } 116 | 117 | func (obj *bagObj) Load(data any) { 118 | bin := data.(*pb.UserBin) 119 | 120 | obj.items = make([]gameutils.Item, 0, 8) 121 | for _, item := range bin.Global.Bag.NumericItems { 122 | newItem := &gameutils.NumericItem{Id: int(item.Id), Num: item.Num} 123 | obj.addItem(newItem) 124 | } 125 | 126 | obj.offlineItems = nil 127 | // 对离线数据进行合并 128 | for _, item := range bin.Offline.Items { 129 | obj.addItem(&gameutils.NumericItem{Id: int(item.Id), Num: item.Num}) 130 | obj.offlineItems = append(obj.offlineItems, &pb.NumericItem{Id: item.Id, Num: -item.Num}) 131 | } 132 | } 133 | 134 | func (obj *bagObj) Save(data any) { 135 | bin := data.(*pb.UserBin) 136 | 137 | var numericItems []*pb.NumericItem 138 | for _, item := range obj.items { 139 | newItem := &pb.NumericItem{Id: int32(item.GetId()), Num: item.GetNum()} 140 | numericItems = append(numericItems, newItem) 141 | } 142 | bin.Global.Bag = &pb.Bag{ 143 | NumericItems: numericItems, 144 | } 145 | if bin.Offline == nil { 146 | bin.Offline = &pb.OfflineBin{} 147 | } 148 | bin.Offline.Items, obj.offlineItems = obj.offlineItems, nil 149 | } 150 | 151 | func (obj *bagObj) addItem(newItem gameutils.Item) { 152 | if isItemValid(newItem.GetId()) { 153 | obj.items = append(obj.items, newItem) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /service/data.go: -------------------------------------------------------------------------------- 1 | // 玩家数据表 2 | 3 | package service 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "time" 9 | 10 | "gofishing-game/internal/errcode" 11 | "gofishing-game/internal/gameutils" 12 | "gofishing-game/internal/pb" 13 | "gofishing-game/internal/rpc" 14 | 15 | "github.com/guogeer/quasar/v2/log" 16 | "github.com/guogeer/quasar/v2/utils" 17 | "google.golang.org/protobuf/proto" 18 | ) 19 | 20 | type EnumDataReset int 21 | 22 | const ( 23 | _ = EnumDataReset(iota) 24 | DataResetPerDay 25 | DataResetPerWeek 26 | ) 27 | 28 | type reseter interface { 29 | Reset(EnumDataReset) 30 | } 31 | 32 | type loadSaver interface { 33 | Save(any) 34 | Load(any) 35 | } 36 | 37 | // 保存游戏全局(大厅/房间)数据 38 | type dataObj struct { 39 | player *Player 40 | 41 | loadSavers []loadSaver 42 | period time.Duration 43 | saveTimer *utils.Timer 44 | offline *pb.OfflineBin 45 | lastDayUpdateTs int64 46 | } 47 | 48 | func newDataObj(player *Player) *dataObj { 49 | obj := &dataObj{ 50 | player: player, 51 | period: 109 * time.Second, 52 | } 53 | obj.Push(obj) 54 | return obj 55 | } 56 | 57 | func (obj *dataObj) TryEnter() errcode.Error { 58 | request := GetEnterQueue().GetRequest(obj.player.Id) 59 | obj.loadAll(request.EnterGameResp.Bin) 60 | obj.updateNewDay() 61 | return nil 62 | } 63 | 64 | func (obj *dataObj) BeforeEnter() { 65 | p := obj.player 66 | now := time.Now() 67 | 68 | if !obj.saveTimer.IsValid() { 69 | obj.saveTimer = p.TimerGroup.NewPeriodTimer(obj.onTime, now, obj.period) 70 | } 71 | } 72 | 73 | func (obj *dataObj) OnLeave() { 74 | } 75 | 76 | func (obj *dataObj) onTime() { 77 | uid := obj.player.Id 78 | bin := &pb.UserBin{} 79 | obj.saveAll(bin) 80 | 81 | bin = proto.Clone(bin).(*pb.UserBin) 82 | go func() { 83 | req := &pb.SaveBinReq{Uid: int32(uid), Bin: bin} 84 | rpc.CacheClient().SaveBin(context.Background(), req) 85 | }() 86 | } 87 | 88 | func (obj *dataObj) saveAll(data any) { 89 | p := obj.player 90 | bin := data.(*pb.UserBin) 91 | 92 | for _, h := range obj.loadSavers { 93 | h.Save(bin) 94 | } 95 | 96 | log.Debugf("player %d save all data", p.Id) 97 | } 98 | 99 | func (obj *dataObj) loadAll(data any) { 100 | for _, h := range obj.loadSavers { 101 | h.Load(data) 102 | } 103 | } 104 | 105 | func (obj *dataObj) Push(h loadSaver) { 106 | obj.loadSavers = append(obj.loadSavers, h) 107 | } 108 | 109 | func (obj *dataObj) Load(data any) { 110 | p := obj.player 111 | bin := data.(*pb.UserBin) 112 | gameutils.InitNilFields(bin.Global) 113 | 114 | p.Level = int(bin.Global.Level) 115 | obj.lastDayUpdateTs = bin.Global.LastDayUpdateTs 116 | } 117 | 118 | func (obj *dataObj) updateNewDay() { 119 | if time.Now().Truncate(24*time.Hour) == time.Unix(obj.lastDayUpdateTs, 0).Truncate(24*time.Hour) { 120 | return 121 | } 122 | obj.lastDayUpdateTs = time.Now().Unix() 123 | for _, loadSaver := range obj.loadSavers { 124 | if h, ok := loadSaver.(reseter); ok { 125 | h.Reset(DataResetPerDay) 126 | } 127 | } 128 | } 129 | 130 | func (obj *dataObj) Save(data any) { 131 | p := obj.player 132 | bin := data.(*pb.UserBin) 133 | bin.Global = &pb.GlobalBin{} 134 | 135 | bin.Global.Level = int32(p.Level) 136 | bin.Offline, obj.offline = obj.offline, &pb.OfflineBin{} 137 | bin.Global.LastDayUpdateTs = obj.lastDayUpdateTs 138 | } 139 | 140 | type globalDictItem struct { 141 | Data any `json:"data,omitempty"` 142 | LastDayUpdateTs int64 `json:"lastDayUpdateTs,omitempty"` 143 | } 144 | 145 | // 全局数据 146 | type GlobalDict struct { 147 | values map[string]*globalDictItem 148 | } 149 | 150 | var globalData GlobalDict 151 | 152 | func (gdata *GlobalDict) load() { 153 | for key, value := range gdata.values { 154 | resp, err := rpc.CacheClient().QueryDict(context.Background(), &pb.QueryDictReq{Key: key}) 155 | if err != nil { 156 | log.Fatalf("load service dict %s error: %v", key, err) 157 | } 158 | if len(resp.Value) == 0 { 159 | continue 160 | } 161 | if err := json.Unmarshal([]byte(resp.Value), value); err != nil { 162 | log.Fatalf("parse service dict %s error: %v", key, err) 163 | } 164 | } 165 | gdata.updateNewDay() 166 | } 167 | 168 | func (gdata *GlobalDict) save() { 169 | var reqs []*pb.UpdateDictReq 170 | for key, value := range gdata.values { 171 | buf, _ := json.Marshal(value) 172 | reqs = append(reqs, &pb.UpdateDictReq{Key: key, Value: buf}) 173 | } 174 | go func() { 175 | for _, req := range reqs { 176 | rpc.CacheClient().UpdateDict(context.Background(), req) 177 | } 178 | }() 179 | } 180 | 181 | func (gdata *GlobalDict) Add(key string, value any) { 182 | if gdata.values == nil { 183 | gdata.values = map[string]*globalDictItem{} 184 | } 185 | gdata.values[key] = &globalDictItem{ 186 | Data: value, 187 | } 188 | } 189 | 190 | func (gdata *GlobalDict) updateNewDay() { 191 | for _, value := range gdata.values { 192 | if time.Now().Truncate(24*time.Hour) != time.Unix(value.LastDayUpdateTs, 0).Truncate(24*time.Hour) { 193 | value.LastDayUpdateTs = time.Now().Unix() 194 | if h, ok := value.Data.(reseter); ok { 195 | h.Reset(DataResetPerDay) 196 | } 197 | } 198 | } 199 | } 200 | 201 | func UpdateDict(key string, value any) { 202 | globalData.Add(key, value) 203 | } 204 | -------------------------------------------------------------------------------- /service/handle.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "encoding/json" 5 | "gofishing-game/internal/gameutils" 6 | 7 | "github.com/guogeer/quasar/v2/cmd" 8 | "github.com/guogeer/quasar/v2/log" 9 | ) 10 | 11 | type addItemsArgs struct { 12 | Uid int `json:"uid,omitempty"` 13 | Items []*gameutils.NumericItem `json:"items,omitempty"` 14 | Way string `json:"way,omitempty"` 15 | } 16 | 17 | type leaveArgs struct { 18 | Uid int `json:"uid,omitempty"` 19 | } 20 | 21 | func init() { 22 | cmd.Hook(hook) 23 | 24 | cmd.Bind("func_effectTableOk", funcEffectTableOk, nil) 25 | cmd.Bind("func_addItems", funcAddItems, (*addItemsArgs)(nil)) 26 | 27 | cmd.Bind("leave", funcLeave, nil) 28 | cmd.Bind("close", funcClose, nil) 29 | 30 | cmd.Bind("func_leave", funcSysLeave, (*leaveArgs)(nil), cmd.WithPrivate()) 31 | cmd.Bind("enter", funcEnter, (*json.RawMessage)(nil)) 32 | } 33 | 34 | type msgHandler interface { 35 | OnRecvMsg() 36 | } 37 | 38 | func hook(ctx *cmd.Context, data any) { 39 | // 非客户端发过来的消息或进入游戏 40 | if ctx.ClientAddr == "" || ctx.MsgId == "enter" { 41 | return 42 | } 43 | 44 | ply := GetPlayerByContext(ctx) 45 | if ply == nil { 46 | ss := &cmd.Session{Id: ctx.Ssid, Out: ctx.Out} 47 | WriteMessage(ctx.ServerName, ss, "serverClose", cmd.M{"serverName": ctx.ServerName, "cause": "not found player"}) 48 | 49 | ctx.Fail() 50 | } else { 51 | if mh, ok := ply.GameAction.(msgHandler); ok { 52 | mh.OnRecvMsg() 53 | } 54 | } 55 | } 56 | 57 | func funcClose(ctx *cmd.Context, iArgs any) { 58 | ply := GetPlayerByContext(ctx) 59 | if ply == nil { 60 | return 61 | } 62 | ply.GameAction.OnClose() 63 | } 64 | 65 | func funcLeave(ctx *cmd.Context, iArgs any) { 66 | ply := GetGatewayPlayer(ctx.Ssid) 67 | if ply == nil { 68 | return 69 | } 70 | log.Debugf("player %d leave room", ply.Id) 71 | ply.Leave() 72 | } 73 | 74 | func funcEffectTableOk(ctx *cmd.Context, data any) { 75 | } 76 | 77 | func funcAddItems(ctx *cmd.Context, data any) { 78 | args := data.(*addItemsArgs) 79 | 80 | var items []gameutils.Item 81 | for _, item := range args.Items { 82 | items = append(items, item) 83 | } 84 | AddItems(args.Uid, items, args.Way) 85 | } 86 | 87 | func funcEnter(ctx *cmd.Context, data any) { 88 | rawData := *(data.(*json.RawMessage)) 89 | 90 | args := &enterArgs{} 91 | json.Unmarshal(rawData, args) 92 | 93 | if args.Token == "" { 94 | return 95 | } 96 | if args.LeaveServer == GetServerId() { 97 | args.LeaveServer = "" 98 | } 99 | 100 | ss, e := GetEnterQueue().PushBack(ctx, args.Token, args.LeaveServer, rawData) 101 | if e != nil && ss != nil { 102 | WriteMessage(ctx.ServerName, ss, "enter", e) 103 | } 104 | } 105 | 106 | type enterArgs struct { 107 | Token string `json:"token,omitempty"` 108 | LeaveServer string `json:"leaveServer,omitempty"` 109 | } 110 | 111 | func funcSysLeave(ctx *cmd.Context, data any) { 112 | args := data.(*leaveArgs) 113 | 114 | uid := args.Uid 115 | ply := GetPlayer(uid) 116 | log.Debugf("player %d auto leave", uid) 117 | 118 | if ply == nil { 119 | ctx.Out.WriteJSON("func_leave", cmd.M{"uid": uid}) 120 | } else { 121 | ply.Leave2(ctx, nil) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /service/roomutils/room.go: -------------------------------------------------------------------------------- 1 | package roomutils 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "gofishing-game/internal/errcode" 10 | "gofishing-game/service" 11 | 12 | "github.com/guogeer/quasar/v2/cmd" 13 | "github.com/guogeer/quasar/v2/config" 14 | "github.com/guogeer/quasar/v2/log" 15 | "github.com/guogeer/quasar/v2/utils" 16 | ) 17 | 18 | var gSubGames map[int]*subGame // 所有的场次 19 | 20 | var ErrPlaying = errcode.New("playing", "wait game ending") 21 | 22 | const NoSeat = -1 23 | 24 | const ( 25 | _ = iota 26 | RoomStatusPlaying // 游戏中 27 | ) 28 | 29 | var ( 30 | OptAutoPlay = "autoPlay" 31 | OptForbidEnterAfterGameStart = "forbidEnterAfterGameStart" 32 | OptSeat = "seat_%d" 33 | ) 34 | 35 | type subGame struct { 36 | Id int 37 | MaxSeatNum int 38 | UserNum int // 机器人+玩家数量 39 | serverName string // 房间的场次名称 40 | Online int 41 | 42 | rooms []*Room 43 | } 44 | 45 | // 虚假的在线人数 46 | func (sub *subGame) updateOnline() { 47 | sub.Online = 0 48 | for _, player := range service.GetAllPlayers() { 49 | if GetRoomObj(player).room.SubId == sub.Id { 50 | if !player.IsRobot && !player.IsSessionClose { 51 | sub.Online++ 52 | } 53 | } 54 | } 55 | // log.Infof("current %s online users %d:%d", GetName(), sub.Online, len(gAllPlayers)) 56 | } 57 | 58 | func GetServerName(subId int) string { 59 | if sub, ok := gSubGames[subId]; ok { 60 | return sub.serverName 61 | } 62 | return "" 63 | } 64 | 65 | func updateOnline() { 66 | var onlines []service.ServerOnline 67 | for _, sub := range gSubGames { 68 | sub.updateOnline() 69 | one := service.ServerOnline{ 70 | Online: sub.Online, 71 | Id: service.GetServerId() + ":" + sub.serverName + ":" + strconv.Itoa(sub.Id), 72 | } 73 | onlines = append(onlines, one) 74 | } 75 | cmd.Forward("hall", "func_updateOnline", cmd.M{"games": onlines}) 76 | } 77 | 78 | type RoomWorld interface { 79 | service.World 80 | NewRoom(subId int) *Room 81 | } 82 | 83 | func LoadGames() { 84 | servers := service.GetAllServers() 85 | 86 | games := map[int]*subGame{} 87 | for _, rowId := range config.Rows("room") { 88 | tagStr, _ := config.String("room", rowId, "tags") 89 | tags := strings.Split(tagStr, ",") 90 | 91 | var name string 92 | for _, tag := range tags { 93 | for server := range servers { 94 | if tag == server { 95 | name = tag 96 | } 97 | } 98 | } 99 | 100 | var subId, seatNum int 101 | config.Scan("room", rowId, "id,seatNum", &subId, &seatNum) 102 | 103 | log.Infof("load game:%d name:%s", subId, name) 104 | games[subId] = &subGame{ 105 | Id: subId, 106 | MaxSeatNum: seatNum, 107 | serverName: name, 108 | } 109 | } 110 | gSubGames = games 111 | } 112 | 113 | func init() { 114 | service.GetEnterQueue().SetLocationFunc(func(uid int) string { 115 | args := &roomEnterArgs{} 116 | enterReq := service.GetEnterQueue().GetRequest(uid) 117 | json.Unmarshal(enterReq.RawData, args) 118 | return service.GetServerId() + ":" + strconv.Itoa(args.SubId) 119 | }) 120 | utils.GetTimerSet().NewPeriodTimer(tick10s, time.Now(), 10*time.Second) 121 | } 122 | 123 | func tick10s() { 124 | updateOnline() 125 | } 126 | -------------------------------------------------------------------------------- /service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "os" 9 | "os/signal" 10 | "runtime" 11 | "slices" 12 | "strings" 13 | "time" 14 | 15 | "gofishing-game/internal/pb" 16 | "gofishing-game/internal/rpc" 17 | 18 | "github.com/guogeer/quasar/v2/cmd" 19 | "github.com/guogeer/quasar/v2/log" 20 | "github.com/guogeer/quasar/v2/utils" 21 | ) 22 | 23 | var port = flag.Int("port", 9510, "server port") 24 | var serverId = flag.String("server_id", "", "server id") 25 | 26 | // 异常退出时保存玩家的数据 27 | func saveAllPlayers() { 28 | startTime := time.Now() 29 | for _, player := range gAllPlayers { 30 | bin := &pb.UserBin{} 31 | player.dataObj.saveAll(bin) 32 | rpc.CacheClient().SaveBin(context.Background(), &pb.SaveBinReq{Uid: int32(player.Id), Bin: bin}) 33 | log.Infof("player %d force save data", player.Id) 34 | } 35 | log.Infof("server %s quit and save data cost %v", GetServerId(), time.Since(startTime)) 36 | } 37 | 38 | func Start() { 39 | // 正常关闭进程 40 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) 41 | defer func() { 42 | stop() 43 | if err := recover(); err != nil { 44 | const size = 64 << 10 45 | buf := make([]byte, size) 46 | buf = buf[:runtime.Stack(buf, false)] 47 | log.Error(err) 48 | log.Errorf("%s", buf) 49 | } 50 | saveAllPlayers() 51 | }() 52 | 53 | flag.Parse() 54 | loadAllScripts() // 加载脚本 55 | 56 | if *port == 0 { 57 | panic("server port is zero") 58 | } 59 | // 向路由注册服务 60 | addr := fmt.Sprintf(":%d", *port) 61 | l, err := net.Listen("tcp", addr) 62 | if err != nil { 63 | log.Fatalf("listen %v", err) 64 | } 65 | 66 | srv := &cmd.Server{Addr: addr} 67 | go func() { srv.Serve(l) }() 68 | 69 | allServers := slices.Collect(GetAllServers()) 70 | if *serverId == "" { 71 | *serverId = allServers[0] 72 | } 73 | cmd.RegisterService(&cmd.ServiceConfig{ 74 | Id: GetServerId(), 75 | Name: strings.Join(allServers, ","), 76 | Addr: addr, 77 | }) 78 | globalData.load() 79 | log.Infof("server id %s name %v start ok.", GetServerId(), strings.Join(allServers, ",")) 80 | 81 | for { 82 | select { 83 | default: 84 | case <-ctx.Done(): 85 | log.Infof("server %s recv signal SIGINT and quit", GetServerId()) 86 | return 87 | } 88 | utils.GetTimerSet().RunOnce() 89 | rpc.RunOnce() // 无等待 90 | // handle network message 91 | cmd.RunOnce() // 无消息时会等待 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /service/system/daily_signin.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "gofishing-game/internal/errcode" 5 | "gofishing-game/internal/gameutils" 6 | "gofishing-game/internal/pb" 7 | "gofishing-game/service" 8 | "time" 9 | 10 | "github.com/guogeer/quasar/v2/cmd" 11 | "github.com/guogeer/quasar/v2/config" 12 | ) 13 | 14 | const maxSignInDay = 7 15 | 16 | var ( 17 | errSignInAlready = errcode.New("sign_in_already", "sign in already") 18 | ) 19 | 20 | type dailySignInObj struct { 21 | player *service.Player 22 | 23 | drawTime time.Time 24 | startTime time.Time 25 | drawState int 26 | } 27 | 28 | var _ service.EnterAction = (*dailySignInObj)(nil) 29 | 30 | func newDailySignInObj(player *service.Player) service.EnterAction { 31 | obj := &dailySignInObj{ 32 | player: player, 33 | } 34 | player.DataObj().Push(obj) 35 | return obj 36 | } 37 | 38 | func (obj *dailySignInObj) BeforeEnter() { 39 | obj.update(time.Now()) 40 | } 41 | 42 | func (obj *dailySignInObj) update(current time.Time) { 43 | dayNum := countIntervalDay(current, obj.startTime) 44 | if dayNum >= maxSignInDay { 45 | obj.drawState = 0 46 | obj.startTime = current 47 | } 48 | } 49 | 50 | type signInState struct { 51 | IsDraw bool `json:"isDraw,omitempty"` 52 | DrawState int `json:"drawState,omitempty"` 53 | DayIndex int `json:"dayIndex,omitempty"` 54 | } 55 | 56 | func (obj *dailySignInObj) currentState(current time.Time) signInState { 57 | obj.update(current) 58 | 59 | dayIndex := countIntervalDay(current, obj.startTime) 60 | dayIndex = dayIndex % maxSignInDay 61 | return signInState{ 62 | IsDraw: countIntervalDay(obj.drawTime, current) == 0, 63 | DrawState: obj.drawState, 64 | DayIndex: dayIndex, 65 | } 66 | } 67 | 68 | func (obj *dailySignInObj) Draw() errcode.Error { 69 | now := time.Now() 70 | 71 | state := obj.currentState(now) 72 | if state.IsDraw { 73 | return errSignInAlready 74 | } 75 | 76 | obj.drawState |= 1 << state.DayIndex 77 | obj.drawTime = now 78 | 79 | reward, _ := config.String("signin", config.RowId(state.DayIndex+1), "reward") 80 | obj.player.BagObj().AddSomeItems(gameutils.ParseNumbericItems(reward), "sign_in") 81 | return nil 82 | } 83 | 84 | func (obj *dailySignInObj) Load(data any) { 85 | bin := data.(*pb.UserBin) 86 | 87 | obj.drawTime = time.Unix(bin.Global.SignIn.DrawTs, 0) 88 | obj.startTime = time.Unix(bin.Global.SignIn.StartTs, 0) 89 | obj.drawState = int(bin.Global.SignIn.DrawState) 90 | } 91 | 92 | func (obj *dailySignInObj) Save(data any) { 93 | bin := data.(*pb.UserBin) 94 | 95 | bin.Global.SignIn = &pb.DailySignIn{ 96 | StartTs: obj.startTime.Unix(), 97 | DrawTs: obj.drawTime.Unix(), 98 | DrawState: int32(obj.drawState), 99 | } 100 | } 101 | 102 | func (obj *dailySignInObj) Look() { 103 | obj.player.WriteJSON("lookSignIn", obj.currentState(time.Now())) 104 | } 105 | 106 | // 计算间隔天数 107 | func countIntervalDay(t1, t2 time.Time) int { 108 | if t1.After(t2) { 109 | t2, t1 = t1, t2 110 | } 111 | y, m, d := t1.Date() 112 | date1 := time.Date(y, m, d, 0, 0, 0, 0, t1.Location()) 113 | duration := t2.Sub(date1) 114 | 115 | return int(duration.Hours() / 24) 116 | } 117 | 118 | const enterActionDailySignIn = "dailySignIn" 119 | 120 | func init() { 121 | service.AddAction(enterActionDailySignIn, newDailySignInObj) 122 | cmd.Bind("drawSignIn", funcDrawSignIn, (*signInArgs)(nil)) 123 | cmd.Bind("lookSignIn", funcLookSignIn, (*signInArgs)(nil)) 124 | } 125 | 126 | type signInArgs struct{} 127 | 128 | func getSignInObj(player *service.Player) *dailySignInObj { 129 | return player.GetAction(enterActionDailySignIn).(*dailySignInObj) 130 | } 131 | 132 | func funcDrawSignIn(ctx *cmd.Context, data any) { 133 | ply := service.GetPlayerByContext(ctx) 134 | if ply == nil { 135 | return 136 | } 137 | 138 | e := getSignInObj(ply).Draw() 139 | ply.WriteErr("drawSignIn", e) 140 | } 141 | 142 | func funcLookSignIn(ctx *cmd.Context, data any) { 143 | ply := service.GetPlayerByContext(ctx) 144 | if ply == nil { 145 | return 146 | } 147 | 148 | getSignInObj(ply).Look() 149 | } 150 | -------------------------------------------------------------------------------- /service/system/login.go: -------------------------------------------------------------------------------- 1 | // 各种平台的参数 2 | 3 | package system 4 | 5 | import ( 6 | "gofishing-game/service" 7 | "slices" 8 | ) 9 | 10 | const actionKeyLogin = "login" 11 | 12 | type loginObj struct { 13 | player *service.Player 14 | 15 | TimeZone float32 16 | LoginPlates []string 17 | ClientVersion string 18 | } 19 | 20 | func (obj *loginObj) BeforeEnter() { 21 | } 22 | 23 | func (obj *loginObj) Load(data any) { 24 | req := service.GetEnterQueue().GetRequest(obj.player.Id) 25 | if req != nil { 26 | obj.TimeZone = float32(req.LoginParamsResp.Params.TimeZone) 27 | obj.LoginPlates = append([]string{}, req.AuthResp.LoginPlates...) 28 | } 29 | } 30 | 31 | func (obj *loginObj) Save(data any) { 32 | } 33 | 34 | var sysPlates = []string{"robot", "guest"} 35 | 36 | func (obj *loginObj) IsBindPlate() bool { 37 | for _, plate := range obj.LoginPlates { 38 | if slices.Index(sysPlates, plate) < 0 { 39 | return true 40 | } 41 | } 42 | return false 43 | } 44 | 45 | func (obj *loginObj) IsRobot() bool { 46 | return slices.Index(obj.LoginPlates, "robot") >= 0 47 | } 48 | 49 | func GetLoginObj(player *service.Player) *loginObj { 50 | return player.GetAction(actionKeyLogin).(*loginObj) 51 | } 52 | 53 | func newloginObj(player *service.Player) service.EnterAction { 54 | obj := &loginObj{player: player} 55 | player.DataObj().Push(obj) 56 | return obj 57 | } 58 | 59 | func init() { 60 | service.AddAction(actionKeyLogin, newloginObj) 61 | } 62 | -------------------------------------------------------------------------------- /service/system/shop.go: -------------------------------------------------------------------------------- 1 | // 商城 2 | package system 3 | 4 | import ( 5 | "gofishing-game/internal/gameutils" 6 | "gofishing-game/service" 7 | 8 | "github.com/guogeer/quasar/v2/cmd" 9 | "github.com/guogeer/quasar/v2/config" 10 | "github.com/guogeer/quasar/v2/log" 11 | ) 12 | 13 | const ( 14 | actionKeyShop = "shop" 15 | shopGroupFirstPay = "firstPay" 16 | shopGroupSubscription = "subscribe" 17 | ) 18 | 19 | type shopObj struct { 20 | player *service.Player 21 | } 22 | 23 | type payArgs struct { 24 | OrderId string `json:"orderId,omitempty"` 25 | Uid int `json:"uid,omitempty"` 26 | Price float64 `json:"price,omitempty"` 27 | GoodsId int `json:"goodsId,omitempty"` 28 | IsTest bool `json:"isTest,omitempty"` 29 | PaySDK string `json:"paySDK,omitempty"` 30 | Group string `json:"group,omitempty"` 31 | ExpireTs int64 `json:"expireTs,omitempty"` 32 | IsFirstPay bool `json:"isFirstPay,omitempty"` 33 | } 34 | 35 | func init() { 36 | service.AddAction(actionKeyShop, newShopObj) 37 | 38 | cmd.Bind("func_pay", funcPay, (*payArgs)(nil), cmd.WithPrivate()) 39 | } 40 | 41 | func newShopObj(p *service.Player) service.EnterAction { 42 | obj := &shopObj{player: p} 43 | return obj 44 | } 45 | 46 | func (obj *shopObj) BeforeEnter() { 47 | } 48 | 49 | func (obj *shopObj) onPayOk(paySDK, orderId string, goodsId int) { 50 | var group string 51 | var price float64 52 | config.Scan("shop", goodsId, "group,price", &group, &price) 53 | 54 | obj.player.WriteJSON("payOk", cmd.M{ 55 | "shopId": goodsId, 56 | "orderId": orderId, 57 | "paySDK": paySDK, 58 | }) 59 | } 60 | 61 | func GetShopObj(player *service.Player) *shopObj { 62 | action := player.GetAction(actionKeyShop) 63 | return action.(*shopObj) 64 | } 65 | 66 | // 支付成功 67 | func funcPay(ctx *cmd.Context, data any) { 68 | args := data.(*payArgs) 69 | 70 | var price float64 71 | var reward string 72 | config.Scan("shop", args.GoodsId, "price,reward", &price, &reward) 73 | log.Infof("player %d pay id %d rmb %v test %v", args.Uid, args.GoodsId, args.Price, args.IsTest) 74 | 75 | items := gameutils.ParseNumbericItems(reward) 76 | //log.Debugf("ply %v buy items %v", uid, reward) 77 | service.AddItems(args.Uid, items, "pay") 78 | 79 | if p := service.GetPlayer(args.Uid); p != nil { 80 | GetShopObj(p).onPayOk(args.PaySDK, args.OrderId, args.GoodsId) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /service/system/tool.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "time" 5 | 6 | "gofishing-game/internal/gameutils" 7 | "gofishing-game/service" 8 | 9 | "github.com/guogeer/quasar/v2/cmd" 10 | "github.com/guogeer/quasar/v2/config" 11 | ) 12 | 13 | type testTool struct { 14 | Id int 15 | ItemId int 16 | } 17 | 18 | func init() { 19 | service.AddTestTool(&testTool{}) 20 | } 21 | 22 | func (tool *testTool) Test_Q清理物品(ctx *cmd.Context, params string) { 23 | ply := service.GetPlayerByContext(ctx) 24 | 25 | addItems := []gameutils.Item{} 26 | for _, rowId := range config.Rows("item") { 27 | itemId, _ := config.Int("item", rowId, "shopid") 28 | num := ply.BagObj().NumItem(int(itemId)) 29 | 30 | addItems = append(addItems, &gameutils.NumericItem{Id: int(itemId), Num: -num}) 31 | } 32 | ply.BagObj().AddSomeItems(addItems, "tool") 33 | } 34 | 35 | func (tool *testTool) Test_Q签到可领(ctx *cmd.Context, params string) { 36 | ply := service.GetPlayerByContext(ctx) 37 | 38 | signInObj := getSignInObj(ply) 39 | signInObj.drawTime = time.Now().Add(-24 * time.Hour) 40 | signInObj.Look() 41 | } 42 | -------------------------------------------------------------------------------- /service/tool.go: -------------------------------------------------------------------------------- 1 | // 测试工具 2 | package service 3 | 4 | import ( 5 | "reflect" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/guogeer/quasar/v2/utils" 11 | 12 | "gofishing-game/internal" 13 | "gofishing-game/internal/env" 14 | "gofishing-game/internal/gameutils" 15 | 16 | "github.com/guogeer/quasar/v2/cmd" 17 | "github.com/guogeer/quasar/v2/config" 18 | "github.com/guogeer/quasar/v2/log" 19 | ) 20 | 21 | const toolFuncPrefix = "Test_" 22 | 23 | var ( 24 | testToolNames []string 25 | testToolHandlers []any 26 | ) 27 | 28 | type toolArgs struct { 29 | Id int `json:"id,omitempty"` 30 | Params string `json:"params,omitempty"` 31 | } 32 | 33 | func init() { 34 | cmd.Bind("getTestTools", funcGetTestTools, (*toolArgs)(nil)) 35 | cmd.Bind("useTestTool", funcUseTestTool, (*toolArgs)(nil)) 36 | 37 | AddTestTool(&testTool{}) 38 | } 39 | 40 | func AddTestTool(toolFuncs any) { 41 | handlers := reflect.ValueOf(toolFuncs) 42 | for i := 0; i < handlers.NumMethod(); i++ { 43 | fn := handlers.Type().Method(i) 44 | if strings.Index(fn.Name, toolFuncPrefix) == 0 { 45 | testToolNames = append(testToolNames, fn.Name[len(toolFuncPrefix):]) 46 | } 47 | } 48 | testToolHandlers = append(testToolHandlers, toolFuncs) 49 | sort.Strings(testToolNames) 50 | } 51 | 52 | func getTestTools(uid int) []string { 53 | if env.Config().Environment == "test" { 54 | return testToolNames 55 | } 56 | 57 | uidStr := strconv.Itoa(uid) 58 | godStr, _ := config.String("config", "god", "value") 59 | gods := strings.Split(godStr, ",") 60 | if internal.IndexArrayFunc(gods, func(i int) bool { return gods[i] == uidStr }) >= 0 { 61 | return testToolNames 62 | } 63 | return nil 64 | } 65 | 66 | func funcGetTestTools(ctx *cmd.Context, data any) { 67 | // args := data.(*ToolArgs) 68 | ply := GetPlayerByContext(ctx) 69 | if ply == nil { 70 | return 71 | } 72 | log.Debugf("player %d get test tools", ply.Id) 73 | tools := getTestTools(ply.Id) 74 | ply.WriteJSON("getTestTools", map[string]any{"tools": tools}) 75 | } 76 | 77 | func funcUseTestTool(ctx *cmd.Context, data any) { 78 | args := data.(*toolArgs) 79 | ply := GetPlayerByContext(ctx) 80 | if ply == nil { 81 | return 82 | } 83 | log.Debugf("player %d use test tool %d", ply.Id, args.Id) 84 | 85 | tools := getTestTools(ply.Id) 86 | if args.Id < 0 || args.Id >= len(tools) { 87 | return 88 | } 89 | for _, tool := range testToolHandlers { 90 | fn := reflect.ValueOf(tool).MethodByName(toolFuncPrefix + tools[args.Id]) 91 | if fn.IsValid() { 92 | fn.Call([]reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(args.Params)}) 93 | } 94 | } 95 | } 96 | 97 | type testTool struct{} 98 | 99 | func (tool *testTool) Test_Q强制断线(ctx *cmd.Context, params string) { 100 | ply := GetPlayerByContext(ctx) 101 | WriteMessage(ply.serverName, ply.session, "serverClose", cmd.M{ 102 | "serverName": GetServerId(), 103 | "cause": "test tool", 104 | }) 105 | } 106 | 107 | func (tool *testTool) Test_Z增加各种数值(ctx *cmd.Context, params string) { 108 | ply := GetPlayerByContext(ctx) 109 | 110 | var items []gameutils.Item 111 | for _, rowId := range config.Rows("item") { 112 | id, _ := config.Int("item", rowId, "id") 113 | items = append(items, &gameutils.NumericItem{Id: int(id), Num: 9999}) 114 | } 115 | 116 | ply.BagObj().AddSomeItems(items, "tool") 117 | } 118 | 119 | func (tool *testTool) Test_S升级到X(ctx *cmd.Context, params string) { 120 | ply := GetPlayerByContext(ctx) 121 | 122 | addExp := int64(0) 123 | toLevel, _ := strconv.Atoi(params) 124 | for i := ply.Level + 1; i < config.NumRow("level") && i <= toLevel; i++ { 125 | exp, _ := config.Int("level", i, "exp") 126 | addExp += exp 127 | } 128 | 129 | ply.BagObj().Add(1110, addExp, utils.GUID()) 130 | } 131 | 132 | func (tool *testTool) Test_L离开游戏(ctx *cmd.Context, params string) { 133 | ply := GetPlayerByContext(ctx) 134 | ply.Leave() 135 | } 136 | 137 | func (tool *testTool) Test_Z增加指定物品数量(ctx *cmd.Context, params string) { 138 | ply := GetPlayerByContext(ctx) 139 | 140 | ss := strings.Split(params+",,", ",") 141 | itemId, _ := strconv.Atoi(ss[0]) 142 | num, _ := strconv.Atoi(ss[1]) 143 | if itemId == 0 { 144 | ply.WriteJSON("prompt", cmd.M{"msg": "格式:物品ID,物品数量"}) 145 | } 146 | ply.BagObj().Add(itemId, int64(num), utils.GUID()) 147 | } 148 | 149 | type testReseter interface { 150 | TestReset() 151 | } 152 | 153 | func (tool *testTool) Test_Y一键重置(ctx *cmd.Context, params string) { 154 | ply := GetPlayerByContext(ctx) 155 | for _, action := range ply.enterActions { 156 | if h, ok := action.(testReseter); ok { 157 | h.TestReset() 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /service/world.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "iter" 7 | "maps" 8 | "time" 9 | 10 | "github.com/guogeer/quasar/v2/utils" 11 | 12 | "gofishing-game/internal/gameutils" 13 | "gofishing-game/internal/pb" 14 | "gofishing-game/internal/rpc" 15 | 16 | "github.com/guogeer/quasar/v2/cmd" 17 | "github.com/guogeer/quasar/v2/config" 18 | "github.com/guogeer/quasar/v2/log" 19 | "github.com/guogeer/quasar/v2/script" 20 | ) 21 | 22 | var ( 23 | gGatewayPlayers map[string]*Player // 关联网络连接 24 | gAllPlayers map[int]*Player // 所有玩家 25 | gAllWorlds map[string]World // 游戏玩法 26 | 27 | isRobotNoLog bool 28 | ) 29 | 30 | // 在线人数 31 | type ServerOnline struct { 32 | Id string `json:"id,omitempty"` // serverId:serverName{:subId} 33 | Online int `json:"online,omitempty"` 34 | } 35 | 36 | // 移除对象缓存,开发中容易出现未初始化,数据继承问题 37 | func createPlayer(serverName string, uid int) *Player { 38 | p := GetWorld(serverName).NewPlayer() 39 | 40 | for _, key := range actionKeys { 41 | h := actionConstructors[key] 42 | p.enterActions[key] = h(p) 43 | } 44 | 45 | p.Id = uid 46 | return p 47 | } 48 | 49 | // 游戏玩法 50 | type World interface { 51 | NewPlayer() *Player 52 | GetName() string 53 | } 54 | 55 | func init() { 56 | gAllPlayers = map[int]*Player{} 57 | gGatewayPlayers = map[string]*Player{} 58 | gAllWorlds = map[string]World{} 59 | 60 | startTime, _ := config.ParseTime("2018-01-31 00:00:00") 61 | utils.NewPeriodTimer(tick10s, startTime, 10*time.Second) 62 | utils.NewPeriodTimer(tick1d, startTime, 24*time.Hour) 63 | } 64 | 65 | func AddWorld(w World) { 66 | gAllWorlds[w.GetName()] = w 67 | } 68 | 69 | func GetWorld(serverName string) World { 70 | return gAllWorlds[serverName] 71 | } 72 | 73 | func tick10s() { 74 | globalData.save() 75 | } 76 | 77 | func Broadcast2Game(messageId string, data any) { 78 | for _, player := range gAllPlayers { 79 | player.WriteJSON(messageId, data) 80 | } 81 | } 82 | 83 | func Broadcast2Gateway(messageId string, i any) { 84 | pkg := &cmd.Package{Id: messageId, Body: i} 85 | data, err := cmd.EncodePackage(pkg) 86 | if err != nil { 87 | return 88 | } 89 | cmd.Route("router", "c2s_broadcast", data) 90 | } 91 | 92 | func BroadcastNotification(notification string) { 93 | Broadcast2Gateway("broadcast", map[string]any{"notification": notification}) 94 | } 95 | 96 | func GetAllPlayers() iter.Seq2[int, *Player] { 97 | return maps.All(gAllPlayers) 98 | } 99 | 100 | func GetPlayer(id int) *Player { 101 | return gAllPlayers[id] 102 | } 103 | 104 | func GetGatewayPlayer(ssid string) *Player { 105 | p, ok := gGatewayPlayers[ssid] 106 | // 玩家不存在 107 | if !ok { 108 | return nil 109 | } 110 | // 玩家在进入游戏或离开游戏过程中,屏蔽其他消息请求 111 | if p.IsBusy() { 112 | return nil 113 | } 114 | return p 115 | } 116 | 117 | func GetServerId() string { 118 | return *serverId 119 | } 120 | 121 | func GetAllServers() iter.Seq[string] { 122 | return maps.Keys(gAllWorlds) 123 | } 124 | 125 | func WriteMessage(serverName string, ss *cmd.Session, id string, i any) { 126 | if ss == nil { 127 | return 128 | } 129 | if serverName != "" { 130 | id = fmt.Sprintf("%s.%s", serverName, id) 131 | } 132 | if m, ok := i.(map[any]any); ok { 133 | i = (script.GenericMap)(m) 134 | } 135 | 136 | pkg := &cmd.Package{Id: id, Body: i} 137 | buf, err := cmd.EncodePackage(pkg) 138 | 139 | if err != nil { 140 | log.Errorf("route message %s %v %v", id, i, err) 141 | return 142 | } 143 | ss.WriteJSON("func_route", buf) 144 | } 145 | 146 | func AddItems(uid int, items []gameutils.Item, way string) { 147 | // log.Debugf("player %d AddItems way %s", uid, itemLog.Way) 148 | if p := GetPlayer(uid); p != nil && !p.IsBusy() { 149 | p.bagObj.AddSomeItems(items, way) 150 | } else { 151 | // 玩家不在线 152 | bin := &pb.UserBin{Offline: &pb.OfflineBin{}} 153 | for _, item := range items { 154 | bin.Offline.Items = append(bin.Offline.Items, &pb.NumericItem{Id: int32(item.GetId()), Num: item.GetNum()}) 155 | } 156 | AddSomeItemLog(uid, items, way) 157 | go func() { 158 | rpc.CacheClient().SaveBin(context.Background(), &pb.SaveBinReq{Uid: int32(uid), Bin: bin}) 159 | }() 160 | } 161 | } 162 | 163 | func SetRobotNoLog(noLog bool) { 164 | isRobotNoLog = noLog 165 | } 166 | 167 | func AddSomeItemLog(uid int, items []gameutils.Item, way string) { 168 | if len(items) == 0 { 169 | return 170 | } 171 | 172 | pbItems := make([]*pb.NumericItem, 0, 4) 173 | for _, item := range items { 174 | pbItems = append(pbItems, &pb.NumericItem{Id: int32(item.GetId()), Num: item.GetNum()}) 175 | } 176 | 177 | if p := GetPlayer(uid); p != nil { 178 | if p.IsRobot && isRobotNoLog { 179 | return 180 | } 181 | for _, pbItem := range pbItems { 182 | pbItem.Balance = p.BagObj().NumItem(int(pbItem.Id)) 183 | } 184 | } 185 | 186 | // 玩家日志按时序更新 187 | req := &pb.AddSomeItemLogReq{ 188 | Uid: int32(uid), 189 | Items: pbItems, 190 | Uuid: utils.GUID(), 191 | Way: way, 192 | CreateTs: time.Now().Unix(), 193 | } 194 | go func() { 195 | rpc.CacheClient().AddSomeItemLog(context.Background(), req) 196 | }() 197 | } 198 | 199 | func tick1d() { 200 | for _, player := range gAllPlayers { 201 | player.dataObj.updateNewDay() 202 | } 203 | globalData.updateNewDay() 204 | } 205 | 206 | type GameOnlineSegment struct { 207 | Id int 208 | PlayerTotal int 209 | PlayerCure []int 210 | } 211 | 212 | func GetPlayerByContext(ctx *cmd.Context) *Player { 213 | return GetGatewayPlayer(ctx.Ssid) 214 | } 215 | -------------------------------------------------------------------------------- /tests/config.yaml: -------------------------------------------------------------------------------- 1 | environment: release 2 | serverKey: helloworld 3 | clientKey: hellokitty 4 | enableDebug: false 5 | db: 6 | game: 7 | user: root 8 | password: 123456 9 | address: "mariadb:3306" 10 | name: game 11 | maxIdleConns: 10 12 | maxOpenConns: 20 13 | manage: 14 | user: root 15 | password: 123456 16 | address: "mariadb:3306" 17 | name: manage 18 | maxIdleConns: 10 19 | maxOpenConns: 20 20 | serverList: 21 | - name: router 22 | address: "router_server:9010" 23 | tablePath: tests/tables 24 | scriptPath: tests/scripts 25 | -------------------------------------------------------------------------------- /tests/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tests", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /tests/scripts/common.lua: -------------------------------------------------------------------------------- 1 | -- module 2 | -- Guogeer 2019-08-20 通用的功能 3 | 4 | -- module("common",package.seeall) 5 | local common = { _version = "0.0.1" } 6 | 7 | function common.print_table(tb,prefix) 8 | if prefix == nil then 9 | prefix = "" 10 | end 11 | if type(tb) == "table" then 12 | for k,v in pairs(tb) do 13 | if type(v) == "table" then 14 | common.print_table(v,prefix.."\t") 15 | else 16 | print(prefix,k,v) 17 | end 18 | end 19 | end 20 | end 21 | 22 | function common.in_array(tb, target) 23 | for k,v in pairs(tb) do 24 | if v == target then 25 | return true 26 | end 27 | end 28 | return false 29 | end 30 | -- print(in_array({"A","B"},"B")) 31 | 32 | function common.parse_strings(s) 33 | local parts = {} 34 | for part in string.gmatch(s,"[^-,;/~]+") do 35 | table.insert(parts,part) 36 | end 37 | return parts 38 | end 39 | -- print_table(parse_strings("10-11001*1,10-11002*2,10-11003*2")) 40 | 41 | function common.shuffle(tb,n) 42 | if n == nil then 43 | n = #tb 44 | end 45 | if n > #tb then 46 | n = #tb 47 | end 48 | for i=1,n do 49 | local r = math.random(i,#tb) 50 | tb[i],tb[r]= tb[r],tb[i] 51 | end 52 | return tb 53 | end 54 | 55 | --[[ 56 | common.print_table(common.shuffle({"A","B","C","D","E","F","G","H","I","J","K","L"},1)) 57 | common.print_table(common.shuffle({"A","B","C","D","E","F","G","H","I","J","K","L"},1)) 58 | common.print_table(common.shuffle({"A","B","C","D","E","F","G","H","I","J","K","L"})) 59 | common.print_table(common.shuffle({"A","B","C","D","E","F","G","H","I","J","K","L"})) 60 | ]]-- 61 | 62 | function common.rand_sample(samples) 63 | if #samples == 0 then 64 | return -1 65 | end 66 | 67 | local sum = 0 68 | for k,v in pairs(samples) do 69 | samples[k] = tonumber(v) 70 | end 71 | for _,n in pairs(samples) do 72 | sum = sum + n 73 | end 74 | if sum <= 0 then 75 | return -1 76 | end 77 | 78 | local t = math.random(sum) 79 | sum = 0 80 | for i,n in pairs(samples) do 81 | sum = sum + n 82 | if t <= sum then 83 | return i 84 | end 85 | end 86 | return -1 87 | end 88 | --[[ 89 | print(rand_sample({100,1,2,3})) 90 | print(rand_sample({100,1,20000,3})) 91 | ]]-- 92 | 93 | function common.parse_items(s) 94 | s = string.gsub(s,"*","-") 95 | 96 | local items = {} 97 | local values = common.parse_strings(s) 98 | for i=1,#values,2 do 99 | local item = { 100 | ["Id"]=tonumber(values[i]), 101 | ["Num"]=tonumber(values[i+1]), 102 | } 103 | table.insert(items,item) 104 | end 105 | return items 106 | end 107 | -- parse_items("1000*100,1001*123") 108 | 109 | function common.split(s,sep) 110 | local result = {} 111 | local pattern = "[^" .. sep .. "]+" 112 | for part in string.gmatch(s, pattern) do 113 | table.insert(result, part) 114 | end 115 | return result 116 | end 117 | 118 | -- common.print_table(common.split("a;b;c",";")) 119 | 120 | return common 121 | -------------------------------------------------------------------------------- /tests/tables/item.tbl: -------------------------------------------------------------------------------- 1 | 物品ID[INT] 物品名称 图标 注册赠送[INT] 2 | id name icon regNum 3 | 1001 gold icons/item/gold.png 1001 4 | 1002 diamond icons/item/diamond.png 1002 -------------------------------------------------------------------------------- /tests/tables/robot.tbl: -------------------------------------------------------------------------------- 1 | 序号[id] 名称 性别[int] 头像 2 | id nickname sex icon 3 | 1 Gale 0 http://localhost/icons/m1.jpg -------------------------------------------------------------------------------- /tests/tables/room.tbl: -------------------------------------------------------------------------------- 1 | ID[INT] 标签 玩法名称 房间名称 游戏时长[DURATION] 空闲时长[DURATION] 门槛[JSON] 上限[JSON] 台桌费[INT] 最大人数[INT] 座位数[INT] 基础奖励[JSON] 固定奖励[JSON] 2 | id tags gameName roomName playDuration freeDuration needItems limitItems cost maxPlayerNum seatNum baseAward constAward 3 | 1001 fingerGuessing 石头剪刀布 初级场 10 5 [[1001,100]] [[1001,2000]] [[1001,10]] 8 4 [[1001,20]] [[1002,10]] 4 | 1002 fingerGuessing 石头剪刀布 中级场 10 5 [[1001,1000]] [[1001,20000]] [[1001,100]] 8 4 [[1001,200]] [[1002,10]] 5 | 1002 fingerGuessing 石头剪刀布 高级场 10 5 [[1001,10000]] [] [[1001,1000]] 8 4 [[1001,2000]] [[1002,10]] 6 | 1102 dice 色子场 普通场 10 5 [[1001,10]] [] [] 8 4 [] [[1002,1]] -------------------------------------------------------------------------------- /tests/tables/signin.tbl: -------------------------------------------------------------------------------- 1 | 天数[int] 奖励[json] 2 | id reward 3 | 1 [[1001,1]] 4 | 2 [[1001,1]] 5 | 3 [[1001,1]] 6 | 4 [[1001,1]] 7 | 5 [[1001,1]] 8 | 6 [[1001,1]] 9 | 7 [[1001,1],[1002,1]] -------------------------------------------------------------------------------- /tests/temp/assets/game.js: -------------------------------------------------------------------------------- 1 | 2 | function parseQuery() { 3 | const search = location.search.slice(1) 4 | const pairs = search ? search.split('&') : [] 5 | const query = {} 6 | for (let i = 0; i < pairs.length; ++i) { 7 | const [key, value] = pairs[i].split('=') 8 | query[key] = query[key] || decodeURIComponent(value) 9 | } 10 | return query 11 | } 12 | 13 | function inArray(arr, target) { 14 | for (i = 0; i < arr.length; i++) { 15 | if (arr[i] == target) { 16 | return true 17 | } 18 | } 19 | return false 20 | } 21 | 22 | function encode(msg_id, msg_obj) { 23 | const default_sign = '12345678' 24 | const pkg = { 25 | "id": msg_id, 26 | "data": msg_obj, 27 | "sign": default_sign 28 | } 29 | 30 | const raw_body = JSON.stringify(pkg) 31 | const md5_sum = md5("hellokitty" + raw_body) 32 | 33 | let md5_part = '' 34 | const md5_indexs = [0, 3, 4, 8, 10, 11, 13, 14] 35 | md5_indexs.forEach(function (v) { 36 | md5_part = md5_part + md5_sum[v] 37 | }) 38 | 39 | const last_index = raw_body.lastIndexOf(default_sign) 40 | const msg_body = raw_body.substring(0, last_index) + md5_part + raw_body.substring(last_index + md5_part.length) 41 | return msg_body 42 | } -------------------------------------------------------------------------------- /tests/temp/client.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 测试工具 9 | 11 | 12 | 13 | 14 | 15 |

仅内部测试

16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/temp/fingerguessingx4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 石头剪刀布DEMO 6 | 7 | 15 | 16 | 17 | 19 | 21 | 23 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/web/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /tests/web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /tests/web/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /tests/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 石头剪刀布DEMO 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host 0.0.0.0", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "antd": "^5.18.1", 14 | "js-md5": "^0.8.3", 15 | "md5": "^2.3.0", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-use-websocket": "^4.8.1", 19 | "socket.io-client": "^4.7.5", 20 | "websocket": "^1.0.35", 21 | "ws": "^8.17.1" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^18.2.66", 25 | "@types/react-dom": "^18.2.22", 26 | "@types/websocket": "^1.0.10", 27 | "@types/ws": "^8.5.10", 28 | "@typescript-eslint/eslint-plugin": "^7.2.0", 29 | "@typescript-eslint/parser": "^7.2.0", 30 | "@vitejs/plugin-react": "^4.2.1", 31 | "eslint": "^8.57.0", 32 | "eslint-plugin-react-hooks": "^4.6.0", 33 | "eslint-plugin-react-refresh": "^0.4.6", 34 | "typescript": "^5.2.2", 35 | "vite": "^5.4.5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/web/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Flex, Form, Input } from 'antd'; 2 | import FingerGuessing from './fingerguessing'; 3 | import { useState } from 'react'; 4 | import { Account } from './utils/game'; 5 | 6 | 7 | const App = () => { 8 | const [form] = Form.useForm(); 9 | const [accounts, setAccounts] = useState([]) 10 | 11 | const accTable: Account[][] = [] 12 | for (let i = 0; accounts && i * 4 < accounts.length; i++) { 13 | accTable.push(accounts.slice(4 * i, Math.min(4 * i + 4, accounts.length))) 14 | } 15 | 16 | return ( 17 |
18 |
23 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 51 | 52 |
53 | { 54 | accounts.length > 0 && accTable?.map((row, rowindex) => 55 | 56 | { 57 | row?.map((account: Account) => ) 58 | } 59 | 60 | 61 | ) 62 | } 63 | 64 | 65 |
66 | ) 67 | 68 | } 69 | 70 | export default App; -------------------------------------------------------------------------------- /tests/web/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/web/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: top; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | 55 | button:focus, 56 | button:focus-visible { 57 | outline: 4px auto -webkit-focus-ring-color; 58 | } 59 | 60 | @media (prefers-color-scheme: light) { 61 | :root { 62 | color: #213547; 63 | background-color: #ffffff; 64 | } 65 | 66 | a:hover { 67 | color: #747bff; 68 | } 69 | 70 | button { 71 | background-color: #f9f9f9; 72 | } 73 | } -------------------------------------------------------------------------------- /tests/web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | // import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /tests/web/src/utils/game.ts: -------------------------------------------------------------------------------- 1 | import { md5 } from 'js-md5'; 2 | 3 | export function parseQuery() { 4 | const search = location.search.slice(1) 5 | const pairs = search ? search.split('&') : [] 6 | const query: { [key: string]: string } = {} 7 | for (let i = 0; i < pairs.length; ++i) { 8 | const [key, value] = pairs[i].split('=') 9 | query[key] = query[key] || decodeURIComponent(value) 10 | } 11 | return query 12 | } 13 | 14 | export function inArray(arr: Array, target: string) { 15 | for (let i = 0; i < arr.length; i++) { 16 | if (arr[i] == target) { 17 | return true 18 | } 19 | } 20 | return false 21 | } 22 | 23 | export function encode(msg_id: string, msg_obj: any) { 24 | const default_sign = '12345678' 25 | const pkg = { 26 | "id": msg_id, 27 | "data": msg_obj, 28 | "sign": default_sign 29 | } 30 | 31 | const raw_body = JSON.stringify(pkg) 32 | const md5_sum = md5("hellokitty" + raw_body) 33 | 34 | let md5_part = '' 35 | const md5_indexs = [0, 3, 4, 8, 10, 11, 13, 14] 36 | md5_indexs.forEach(function (v) { 37 | md5_part = md5_part + md5_sum[v] 38 | }) 39 | 40 | const last_index = raw_body.lastIndexOf(default_sign) 41 | const msg_body = raw_body.substring(0, last_index) + md5_part + raw_body.substring(last_index + md5_part.length) 42 | return msg_body 43 | } 44 | 45 | export type Account = { 46 | loginAddr: string 47 | openId: string 48 | } -------------------------------------------------------------------------------- /tests/web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tests/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "ES2020", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "module": "ESNext", 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | "include": [ 26 | "src" 27 | ], 28 | "references": [ 29 | { 30 | "path": "./tsconfig.node.json" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /tests/web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tests/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------