From 6e3670aec8bccc8a9aea4f7065a736a49de6ffbf Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Thu, 9 Apr 2026 17:09:41 +0700 Subject: [PATCH] feat: add ratel-online server, core, client, and api sources Import source files from ratel-online organization repos into monorepo structure for gomoku game development. Update README with project structure and credits for original authors. --- README.md | 28 +- api/LICENSE | 21 + api/README.md | 17 + api/pom.xml | 173 +++++ api/ratel.sql | 44 ++ .../main/java/com/isnico/api/Application.java | 15 + .../java/com/isnico/api/annotation/Auth.java | 14 + .../com/isnico/api/component/MailUtil.java | 109 +++ .../com/isnico/api/component/RedisUtil.java | 128 ++++ .../com/isnico/api/config/MybatisConfig.java | 25 + .../com/isnico/api/config/RedisConfig.java | 65 ++ .../com/isnico/api/config/SwaggerConfig.java | 34 + .../java/com/isnico/api/consts/AppConst.java | 21 + .../isnico/api/controller/AuthController.java | 58 ++ .../isnico/api/controller/MailController.java | 53 ++ .../isnico/api/controller/UserController.java | 31 + .../java/com/isnico/api/enums/ResultCode.java | 45 ++ .../api/exception/BusinessException.java | 43 ++ .../api/exception/GlobalExceptionAdvice.java | 36 + .../com/isnico/api/filter/AuthFilter.java | 40 ++ .../com/isnico/api/filter/ContextFilter.java | 59 ++ .../com/isnico/api/mapper/UserMapper.java | 8 + .../java/com/isnico/api/model/Context.java | 26 + .../java/com/isnico/api/model/Result.java | 49 ++ .../java/com/isnico/api/model/po/User.java | 34 + .../com/isnico/api/model/vo/LoginResp.java | 24 + .../com/isnico/api/model/vo/UserResp.java | 29 + .../com/isnico/api/service/AuthService.java | 65 ++ .../com/isnico/api/service/UserService.java | 31 + .../java/com/isnico/api/util/HttpUtil.java | 105 +++ .../java/com/isnico/api/util/StringUtil.java | 40 ++ api/src/main/resources/application.properties | 32 + api/src/main/resources/logback-spring.xml | 172 +++++ client/LICENSE | 21 + client/README.md | 18 + client/api/login.go | 22 + client/ctx/ctx.go | 138 ++++ client/ctx/ctx_test.go | 69 ++ client/go.mod | 15 + client/go.sum | 51 ++ client/main.go | 26 + client/model/model.go | 19 + client/shell/shell.go | 52 ++ client/util/rand.go | 56 ++ client/util/stdin.go | 36 + core/LICENSE | 21 + core/README.md | 1 + core/consts/consts.go | 45 ++ core/go.mod | 20 + core/go.sum | 49 ++ core/log/log.go | 42 ++ core/model/model.go | 71 ++ core/model/poker.go | 183 ++++++ core/model/poker_test.go | 20 + core/model/texas.go | 48 ++ core/network/conn.go | 56 ++ core/pkg/holdem/cards.go | 119 ++++ core/pkg/holdem/holdem.go | 241 +++++++ core/protocol/protocol.go | 105 +++ core/protocol/tcp.go | 30 + core/protocol/websocket.go | 36 + core/util/arrays/arrays.go | 26 + core/util/async/async.go | 31 + core/util/bytes/bytes.go | 2 + core/util/http/http.go | 72 ++ core/util/json/json.go | 26 + core/util/math/math.go | 15 + core/util/poker/consts.go | 133 ++++ core/util/poker/consts_test.go | 11 + core/util/poker/poker.go | 380 +++++++++++ core/util/poker/poker_test.go | 227 +++++++ core/util/poker/runfast_poker.go | 622 ++++++++++++++++++ core/util/poker/runfast_poker_test.go | 132 ++++ core/util/poker/texas.go | 39 ++ core/util/poker/texas_test.go | 44 ++ core/util/rand/rand.go | 11 + core/util/strings/strings.go | 118 ++++ server/Dockerfile | 56 ++ server/LICENSE | 21 + server/Makefile | 80 +++ server/README.md | 134 ++++ server/bot/bot.go | 83 +++ server/build.ps1 | 63 ++ server/build.sh | 59 ++ server/consts/const.go | 144 ++++ server/database/database.go | 501 ++++++++++++++ server/database/liar.go | 27 + server/database/mahjong.go | 209 ++++++ server/database/model.go | 333 ++++++++++ server/database/texas.go | 134 ++++ server/database/uno.go | 184 ++++++ server/demo.gif | Bin 0 -> 324842 bytes server/docker-compose.yaml | 47 ++ server/docs/Docker部署指南.md | 323 +++++++++ server/docs/快速开始.md | 85 +++ server/go.mod | 24 + server/go.sum | 93 +++ server/main.go | 53 ++ server/network/network.go | 66 ++ server/network/tcp.go | 43 ++ server/network/wss.go | 42 ++ server/render/render.go | 131 ++++ server/rule/rule.go | 55 ++ server/rule/texas.go | 24 + server/skill/skill.go | 269 ++++++++ server/skill/skill_test.go | 15 + server/state/create.go | 61 ++ server/state/game/game.go | 492 ++++++++++++++ server/state/game/liar.go | 467 +++++++++++++ server/state/game/mahjong.go | 244 +++++++ server/state/game/runfast_game.go | 396 +++++++++++ server/state/game/texas/bet.go | 119 ++++ server/state/game/texas/init.go | 93 +++ server/state/game/texas/round.go | 173 +++++ server/state/game/texas/texas.go | 65 ++ server/state/game/uno.go | 130 ++++ server/state/home.go | 33 + server/state/join.go | 85 +++ server/state/state.go | 89 +++ server/state/waiting.go | 263 ++++++++ server/state/welcome.go | 24 + 121 files changed, 11204 insertions(+), 1 deletion(-) create mode 100644 api/LICENSE create mode 100644 api/README.md create mode 100644 api/pom.xml create mode 100644 api/ratel.sql create mode 100644 api/src/main/java/com/isnico/api/Application.java create mode 100644 api/src/main/java/com/isnico/api/annotation/Auth.java create mode 100644 api/src/main/java/com/isnico/api/component/MailUtil.java create mode 100644 api/src/main/java/com/isnico/api/component/RedisUtil.java create mode 100644 api/src/main/java/com/isnico/api/config/MybatisConfig.java create mode 100644 api/src/main/java/com/isnico/api/config/RedisConfig.java create mode 100644 api/src/main/java/com/isnico/api/config/SwaggerConfig.java create mode 100644 api/src/main/java/com/isnico/api/consts/AppConst.java create mode 100644 api/src/main/java/com/isnico/api/controller/AuthController.java create mode 100644 api/src/main/java/com/isnico/api/controller/MailController.java create mode 100644 api/src/main/java/com/isnico/api/controller/UserController.java create mode 100644 api/src/main/java/com/isnico/api/enums/ResultCode.java create mode 100644 api/src/main/java/com/isnico/api/exception/BusinessException.java create mode 100644 api/src/main/java/com/isnico/api/exception/GlobalExceptionAdvice.java create mode 100644 api/src/main/java/com/isnico/api/filter/AuthFilter.java create mode 100644 api/src/main/java/com/isnico/api/filter/ContextFilter.java create mode 100644 api/src/main/java/com/isnico/api/mapper/UserMapper.java create mode 100644 api/src/main/java/com/isnico/api/model/Context.java create mode 100644 api/src/main/java/com/isnico/api/model/Result.java create mode 100644 api/src/main/java/com/isnico/api/model/po/User.java create mode 100644 api/src/main/java/com/isnico/api/model/vo/LoginResp.java create mode 100644 api/src/main/java/com/isnico/api/model/vo/UserResp.java create mode 100644 api/src/main/java/com/isnico/api/service/AuthService.java create mode 100644 api/src/main/java/com/isnico/api/service/UserService.java create mode 100644 api/src/main/java/com/isnico/api/util/HttpUtil.java create mode 100644 api/src/main/java/com/isnico/api/util/StringUtil.java create mode 100644 api/src/main/resources/application.properties create mode 100644 api/src/main/resources/logback-spring.xml create mode 100644 client/LICENSE create mode 100644 client/README.md create mode 100644 client/api/login.go create mode 100644 client/ctx/ctx.go create mode 100644 client/ctx/ctx_test.go create mode 100644 client/go.mod create mode 100644 client/go.sum create mode 100644 client/main.go create mode 100644 client/model/model.go create mode 100644 client/shell/shell.go create mode 100644 client/util/rand.go create mode 100644 client/util/stdin.go create mode 100644 core/LICENSE create mode 100644 core/README.md create mode 100644 core/consts/consts.go create mode 100644 core/go.mod create mode 100644 core/go.sum create mode 100644 core/log/log.go create mode 100644 core/model/model.go create mode 100644 core/model/poker.go create mode 100644 core/model/poker_test.go create mode 100644 core/model/texas.go create mode 100644 core/network/conn.go create mode 100644 core/pkg/holdem/cards.go create mode 100644 core/pkg/holdem/holdem.go create mode 100644 core/protocol/protocol.go create mode 100644 core/protocol/tcp.go create mode 100644 core/protocol/websocket.go create mode 100644 core/util/arrays/arrays.go create mode 100644 core/util/async/async.go create mode 100644 core/util/bytes/bytes.go create mode 100644 core/util/http/http.go create mode 100644 core/util/json/json.go create mode 100644 core/util/math/math.go create mode 100644 core/util/poker/consts.go create mode 100644 core/util/poker/consts_test.go create mode 100644 core/util/poker/poker.go create mode 100644 core/util/poker/poker_test.go create mode 100644 core/util/poker/runfast_poker.go create mode 100644 core/util/poker/runfast_poker_test.go create mode 100644 core/util/poker/texas.go create mode 100644 core/util/poker/texas_test.go create mode 100644 core/util/rand/rand.go create mode 100644 core/util/strings/strings.go create mode 100644 server/Dockerfile create mode 100644 server/LICENSE create mode 100644 server/Makefile create mode 100644 server/README.md create mode 100644 server/bot/bot.go create mode 100644 server/build.ps1 create mode 100644 server/build.sh create mode 100644 server/consts/const.go create mode 100644 server/database/database.go create mode 100644 server/database/liar.go create mode 100644 server/database/mahjong.go create mode 100644 server/database/model.go create mode 100644 server/database/texas.go create mode 100644 server/database/uno.go create mode 100644 server/demo.gif create mode 100644 server/docker-compose.yaml create mode 100644 server/docs/Docker部署指南.md create mode 100644 server/docs/快速开始.md create mode 100644 server/go.mod create mode 100644 server/go.sum create mode 100644 server/main.go create mode 100644 server/network/network.go create mode 100644 server/network/tcp.go create mode 100644 server/network/wss.go create mode 100644 server/render/render.go create mode 100644 server/rule/rule.go create mode 100644 server/rule/texas.go create mode 100644 server/skill/skill.go create mode 100644 server/skill/skill_test.go create mode 100644 server/state/create.go create mode 100644 server/state/game/game.go create mode 100644 server/state/game/liar.go create mode 100644 server/state/game/mahjong.go create mode 100644 server/state/game/runfast_game.go create mode 100644 server/state/game/texas/bet.go create mode 100644 server/state/game/texas/init.go create mode 100644 server/state/game/texas/round.go create mode 100644 server/state/game/texas/texas.go create mode 100644 server/state/game/uno.go create mode 100644 server/state/home.go create mode 100644 server/state/join.go create mode 100644 server/state/state.go create mode 100644 server/state/waiting.go create mode 100644 server/state/welcome.go diff --git a/README.md b/README.md index 405874a..cd95c04 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,28 @@ # gomoku -A simple Gomoku game with networking. Base on [Ratel](https://github.com/ratel-online/) + +A simple Gomoku game with networking. Based on [Ratel](https://github.com/ratel-online/). + +## Project Structure + +This is a monorepo containing the following components: + +| Directory | Description | Language | Source | +|-----------|-------------|----------|--------| +| `server/` | Game server with multiplayer support | Go | [ratel-online/server](https://github.com/ratel-online/server) | +| `core/` | Shared core logic, models, and networking | Go | [ratel-online/core](https://github.com/ratel-online/core) | +| `client/` | CLI game client | Go | [ratel-online/client](https://github.com/ratel-online/client) | +| `api/` | API definitions | Java | [ratel-online/api](https://github.com/ratel-online/api) | + +## Credits + +This project is built upon the work of the [Ratel Online](https://github.com/ratel-online) organization. The original repositories are licensed under the **MIT License**. + +### Original Authors & Contributors + +- [ainilili](https://github.com/ainilili) — Primary author +- [feel-easy](https://github.com/feel-easy) +- [mlmdflr](https://github.com/mlmdflr) +- [mikodream](https://github.com/mikodream) +- [EldersJavas](https://github.com/EldersJavas) + +See the `LICENSE` file in each subdirectory for the original MIT license terms. diff --git a/api/LICENSE b/api/LICENSE new file mode 100644 index 0000000..b777b61 --- /dev/null +++ b/api/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 ratel-online + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..8b5afcb --- /dev/null +++ b/api/README.md @@ -0,0 +1,17 @@ +# Api +负责C端及游戏服务器内部调用的接口支持。 +# Todos +## C端 + - [x] 注册 + - [x] 登录 + - [x] 发送邮件 + - [ ] 忘记密码 + - [ ] 修改密码 + - [x] 查询个人信息 + - [ ] 查询战绩 + +## 对内 + - [ ] 校验Token有效性 + - [ ] 新增战绩 + - [ ] 查询玩家信息 + - [ ] 修改积分 diff --git a/api/pom.xml b/api/pom.xml new file mode 100644 index 0000000..fc05206 --- /dev/null +++ b/api/pom.xml @@ -0,0 +1,173 @@ + + 4.0.0 + com.isnico + api + 1.0.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.3.1.RELEASE + + + + 1.8 + 1.8 + UTF-8 + 1.1.10 + 1.3.1 + 1.2.3 + 1.0.6 + 1.1.4 + 2.6.1 + com.isnico.api.Application + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-mail + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis.version} + + + com.alibaba + fastjson + 1.2.56 + + + com.smallnico + ourbatis-spring-boot-starter + ${ourbatis.version} + + + com.gitee.ainilili + noson + + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + com.smallnico + noson + ${noson.version} + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + mysql + mysql-connector-java + + + + com.alibaba + druid + ${druid.version} + + + + io.springfox + springfox-swagger2 + ${swagger.version} + + + + io.springfox + springfox-swagger-ui + ${swagger.version} + + + + org.apache.httpcomponents + httpclient + + + + org.apache.httpcomponents + httpcore + + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pageHelper.version} + + + + org.projectlombok + lombok + 1.18.20 + + + + org.apache.commons + commons-lang3 + 3.7 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + ${main.class} + + + + + repackage + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${java.version.source} + ${java.version.target} + ${java.specification.version} + ${java.specification.version} + + + + + \ No newline at end of file diff --git a/api/ratel.sql b/api/ratel.sql new file mode 100644 index 0000000..225766b --- /dev/null +++ b/api/ratel.sql @@ -0,0 +1,44 @@ +/* + Navicat Premium Data Transfer + + Source Server : localhost + Source Server Type : MySQL + Source Server Version : 80026 + Source Host : localhost:3306 + Source Schema : ratel + + Target Server Type : MySQL + Target Server Version : 80026 + File Encoding : 65001 + + Date: 11/11/2021 20:21:17 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for user +-- ---------------------------- +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id', + `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '姓名', + `avatar` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '头像', + `username` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '账号(邮箱)', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '密码', + `score` bigint NOT NULL DEFAULT 0 COMMENT '积分', + `victory` bigint NOT NULL DEFAULT 0 COMMENT '胜局', + `defeat` bigint NOT NULL DEFAULT 0 COMMENT '败局', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` tinyint NOT NULL DEFAULT 2 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of user +-- ---------------------------- +INSERT INTO `user` VALUES (1, 'nico', '', 'ainililia@163.com', 'a77ef9dd851b4c91b597d5ccba77cf02', 0, 0, 0, '2021-11-11 14:55:10', '2021-11-11 14:55:10', 2); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/api/src/main/java/com/isnico/api/Application.java b/api/src/main/java/com/isnico/api/Application.java new file mode 100644 index 0000000..a80275d --- /dev/null +++ b/api/src/main/java/com/isnico/api/Application.java @@ -0,0 +1,15 @@ +package com.isnico.api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@SpringBootApplication +@EnableTransactionManagement +public class Application { + + public static void main(String[] args) throws Exception { + SpringApplication.run(Application.class, args); + } +} diff --git a/api/src/main/java/com/isnico/api/annotation/Auth.java b/api/src/main/java/com/isnico/api/annotation/Auth.java new file mode 100644 index 0000000..ab5657f --- /dev/null +++ b/api/src/main/java/com/isnico/api/annotation/Auth.java @@ -0,0 +1,14 @@ +package com.isnico.api.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface Auth { + +} diff --git a/api/src/main/java/com/isnico/api/component/MailUtil.java b/api/src/main/java/com/isnico/api/component/MailUtil.java new file mode 100644 index 0000000..a66c618 --- /dev/null +++ b/api/src/main/java/com/isnico/api/component/MailUtil.java @@ -0,0 +1,109 @@ +package com.isnico.api.component; + +import org.nico.noson.Noson; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.FileSystemResource; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import java.io.File; + + +@Component +public class MailUtil { + + Logger logger = LoggerFactory.getLogger(MailUtil.class); + + @Autowired + private JavaMailSender mailSender; + + @Value("${spring.mail.username}") + private String from; + + /** + * 发送纯文本邮件 + * @param to + * @param subject + * @param content + */ + public boolean sendSimpleMail(String to, String subject, String content) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(from); + message.setTo(to); + message.setSubject(subject); + message.setText(content); + try { + mailSender.send(message); + logger.info("文本邮件发送成功: "+ Noson.reversal(message)); + return true; + } catch (Exception e) { + logger.info("文本邮件发送失败: "+ Noson.reversal(message)); + logger.info("文本邮件发送失败: "+e); + return false; + } + } + + /** + * 发送html类型邮件 + * @param to + * @param subject + * @param content + */ + public boolean sendHtmlMail(String to, String subject, String content) { + MimeMessage message = mailSender.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(message, true); //true表示需要创建一个multipart message + helper.setFrom(from); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(content, true); + mailSender.send(message); + logger.info("发送html邮件发送成功: "+ Noson.reversal(message)); + return true; + } catch (MessagingException e) { + logger.info("发送html邮件发送失败: "+ Noson.reversal(message)); + logger.info("发送html邮件发送失败: "+e); + return true; + } + } + + /** + * 发送带附件的邮件 + * @param to + * @param subject + * @param content + * @param filePath + */ + public boolean sendAttachmentsMail(String to, String subject, String content, String filePath){ + MimeMessage message = mailSender.createMimeMessage(); + + try { + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setFrom(from); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(content, true); + + FileSystemResource file = new FileSystemResource(new File(filePath)); + String fileName = filePath.substring(filePath.lastIndexOf(File.separator)); + helper.addAttachment(fileName, file); + + mailSender.send(message); + logger.info("带附件邮件发送成功: "+ Noson.reversal(message)); + return true; + } catch (MessagingException e) { + logger.info("发送带附件的邮件发送失败: "+ Noson.reversal(message)); + logger.info("发送带附件的邮件发送失败: "+e); + return true; + } + } + + +} diff --git a/api/src/main/java/com/isnico/api/component/RedisUtil.java b/api/src/main/java/com/isnico/api/component/RedisUtil.java new file mode 100644 index 0000000..95c7fc0 --- /dev/null +++ b/api/src/main/java/com/isnico/api/component/RedisUtil.java @@ -0,0 +1,128 @@ +package com.isnico.api.component; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +@Component +public class RedisUtil { + + @Autowired + private StringRedisTemplate redisTemplate; + + private static final Long SUCCESS = 1L; + + /** + * 指定缓存失效时间 + * + * @param key 键 + * @param time 时间(秒) + * @return + */ + public boolean expire(String key, long time) { + return expire(key, time, TimeUnit.SECONDS); + } + + /** + * 指定缓存失效时间 + * + * @param key + * @param time + * @param timeUnit + * @return + */ + public boolean expire(String key, long time, TimeUnit timeUnit) { + try { + if (time > 0) { + redisTemplate.expire(key, time, timeUnit); + } + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * 删除缓存 + * + * @param key 可以传一个值 或多个 + */ + @SuppressWarnings("unchecked") + public void del(String... key) { + if (key != null && key.length > 0) { + if (key.length == 1) { + redisTemplate.delete(key[0]); + } else { + redisTemplate.delete(CollectionUtils.arrayToList(key)); + } + } + } + + //============================String============================= + + /** + * 普通缓存获取 + * + * @param key 键 + * @return 值 + */ + public String get(String key) { + return key == null ? null : redisTemplate.opsForValue().get(key); + } + + /** + * 普通缓存放入 + * + * @param key 键 + * @param value 值 + * @return true成功 false失败 + */ + public boolean set(String key, String value) { + try { + redisTemplate.opsForValue().set(key, value); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + } + + /** + * 普通缓存放入并设置时间 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 + * @return true成功 false 失败 + */ + public boolean set(String key, String value, long time) { + return set(key, value, time, TimeUnit.SECONDS); + } + + public boolean set(String key, String value, long time, TimeUnit timeUnit) { + try { + if (time > 0) { + redisTemplate.opsForValue().set(key, value, time, timeUnit); + } else { + set(key, value); + } + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + +} diff --git a/api/src/main/java/com/isnico/api/config/MybatisConfig.java b/api/src/main/java/com/isnico/api/config/MybatisConfig.java new file mode 100644 index 0000000..0220aa2 --- /dev/null +++ b/api/src/main/java/com/isnico/api/config/MybatisConfig.java @@ -0,0 +1,25 @@ +package com.isnico.api.config; + +import javax.sql.DataSource; + +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@MapperScan(basePackages = "com.isnico.api.mapper" , sqlSessionFactoryRef = "sqlSessionFactory") +public class MybatisConfig { + + @Bean(name = "sqlSessionFactory") + public SqlSessionFactory sqlSessionFactoryBean(@Qualifier("dataSource")DataSource dataSource) throws Exception { + SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); + sqlSessionFactoryBean.setDataSource(dataSource); + return sqlSessionFactoryBean.getObject(); + } + +} + + diff --git a/api/src/main/java/com/isnico/api/config/RedisConfig.java b/api/src/main/java/com/isnico/api/config/RedisConfig.java new file mode 100644 index 0000000..e42d282 --- /dev/null +++ b/api/src/main/java/com/isnico/api/config/RedisConfig.java @@ -0,0 +1,65 @@ +package com.isnico.api.config; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory, + RedisSerializer redisSerializer) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + + // key采用String的序列化方式 + template.setKeySerializer(StringRedisSerializer.UTF_8); + template.setHashKeySerializer(StringRedisSerializer.UTF_8); + + // value序列化方式采用jackson + template.setDefaultSerializer(redisSerializer); + template.setValueSerializer(redisSerializer); + template.setHashValueSerializer(redisSerializer); + template.afterPropertiesSet(); + return template; + } + + @Bean + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { + return new StringRedisTemplate(redisConnectionFactory); + } + + @Bean + public RedisSerializer redisSerializer() { + ObjectMapper objectMapper = new ObjectMapper(); + //反序列化时候遇到不匹配的属性并不抛出异常 + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + //序列化时候遇到空对象不抛出异常 + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + //反序列化的时候如果是无效子类型,不抛出异常 + objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false); + //不使用默认的dateTime进行序列化, + objectMapper.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false); + //使用JSR310提供的序列化类,里面包含了大量的JDK8时间序列化类 + objectMapper.registerModule(new JavaTimeModule()); + //启用反序列化所需的类型信息,在属性中添加@class + objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, + JsonTypeInfo.As.PROPERTY); + //配置null值的序列化器 + GenericJackson2JsonRedisSerializer.registerNullValueSerializer(objectMapper, null); + return new GenericJackson2JsonRedisSerializer(objectMapper); + } +} diff --git a/api/src/main/java/com/isnico/api/config/SwaggerConfig.java b/api/src/main/java/com/isnico/api/config/SwaggerConfig.java new file mode 100644 index 0000000..2590ac8 --- /dev/null +++ b/api/src/main/java/com/isnico/api/config/SwaggerConfig.java @@ -0,0 +1,34 @@ +package com.isnico.api.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + + @Bean + public Docket managerDocument() { + return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()) + .groupName("ratel") + .select() + .apis(RequestHandlerSelectors.basePackage("com.isnico.api.controller")) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("Ratel") + .description("ratel") + .version("v1.0.0") + .build(); + } +} diff --git a/api/src/main/java/com/isnico/api/consts/AppConst.java b/api/src/main/java/com/isnico/api/consts/AppConst.java new file mode 100644 index 0000000..de5733d --- /dev/null +++ b/api/src/main/java/com/isnico/api/consts/AppConst.java @@ -0,0 +1,21 @@ +package com.isnico.api.consts; + +public interface AppConst { + + // mail 验证码长度 + int MAIL_AUTH_CODE_LENGTH = 5; + + int TRUE = 1; + int FALSE = 2; + + //用户token缓存key + String USER_TOKEN = "ratel:user:token@"; + + //邮箱认证缓存key + String USER_REGISTER_AUTH_CODE = "ratel:user:register:auth:code@"; + + // 正则 + String REGEX_EMAIL = "^\\w+((-\\w+)|(\\.\\w+))*\\@[A-Za-z0-9]+((\\.|-)[A-Za-z0-9]+)*\\.[A-Za-z0-9]+$"; + String REGEX_PWD = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,15}$"; + String REGEX_NAME = "^[\\u4e00-\\u9fa5_a-zA-Z0-9]+$"; +} diff --git a/api/src/main/java/com/isnico/api/controller/AuthController.java b/api/src/main/java/com/isnico/api/controller/AuthController.java new file mode 100644 index 0000000..e904208 --- /dev/null +++ b/api/src/main/java/com/isnico/api/controller/AuthController.java @@ -0,0 +1,58 @@ +package com.isnico.api.controller; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +import com.isnico.api.component.RedisUtil; +import com.isnico.api.consts.AppConst; +import com.isnico.api.model.po.User; +import com.isnico.api.enums.ResultCode; +import com.isnico.api.model.Result; +import com.isnico.api.model.vo.LoginResp; +import com.isnico.api.util.StringUtil; +import io.swagger.annotations.Api; +import org.hibernate.validator.constraints.Length; +import com.isnico.api.service.AuthService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; + +import java.util.Locale; +import java.util.Objects; + +@RestController +@RequestMapping("/api/v1") +@Validated +@Api(tags = "Auth") +public class AuthController { + + @Autowired + private AuthService authService; + + @ApiOperation(value = "注册") + @PostMapping("/register") + public Result register( + @ApiParam(value = "账户", required = true) @Pattern(regexp = AppConst.REGEX_EMAIL) @RequestParam String username, + @ApiParam(value = "密码", required = true) @Pattern(regexp = AppConst.REGEX_PWD, message = "密码需要由长度为6~15的字母和数字组成") @RequestParam String password, + @ApiParam(value = "昵称", required = true) @Length(min = 3, max = 15) @Pattern(regexp = AppConst.REGEX_NAME, message = "昵称只能为字母、汉字或数字") @RequestParam String name, + @ApiParam(value = "验证码", required = true) @RequestParam String code + ){ + return Result.ok(authService.register(username, password, name, code)); + } + + @ApiOperation(value = "登录") + @PostMapping("/login") + public Result login( + @ApiParam(value = "账户", required = true) @NotNull @RequestParam String username, + @ApiParam(value = "密码", required = true) @NotNull @RequestParam String password + ){ + return Result.ok(authService.login(username, password)); + } + +} diff --git a/api/src/main/java/com/isnico/api/controller/MailController.java b/api/src/main/java/com/isnico/api/controller/MailController.java new file mode 100644 index 0000000..4208b17 --- /dev/null +++ b/api/src/main/java/com/isnico/api/controller/MailController.java @@ -0,0 +1,53 @@ +package com.isnico.api.controller; + +import java.util.concurrent.TimeUnit; + +import javax.validation.constraints.Pattern; + +import com.isnico.api.component.MailUtil; +import com.isnico.api.component.RedisUtil; +import com.isnico.api.consts.AppConst; +import com.isnico.api.enums.ResultCode; +import com.isnico.api.model.Result; +import com.isnico.api.util.StringUtil; +import io.swagger.annotations.Api; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; + +@RestController +@RequestMapping("/api/v1/mail") +@Validated +@Api(tags = "Mail") +public class MailController { + + @Autowired + private MailUtil mailUtil; + + @Autowired + private RedisUtil redisUtil; + + @ApiOperation(value = "发送邮件") + @PostMapping + public Result send( + @ApiParam(value = "账户", required = true) + @Pattern(regexp = AppConst.REGEX_EMAIL, message = "不符合邮箱格式") + @RequestParam String email + ){ + String code = StringUtil.random(AppConst.MAIL_AUTH_CODE_LENGTH); + boolean success = mailUtil.sendSimpleMail(email, "NicoNicoNi! Register Auth Code Mail !", "Auth Code: " + code); + if(success) { + redisUtil.set(AppConst.USER_REGISTER_AUTH_CODE + email, code, 5L, TimeUnit.MINUTES); + return Result.ok(); + } + return Result.fail(ResultCode.ERROR); + } + + +} diff --git a/api/src/main/java/com/isnico/api/controller/UserController.java b/api/src/main/java/com/isnico/api/controller/UserController.java new file mode 100644 index 0000000..023706d --- /dev/null +++ b/api/src/main/java/com/isnico/api/controller/UserController.java @@ -0,0 +1,31 @@ +package com.isnico.api.controller; + +import com.isnico.api.annotation.Auth; +import com.isnico.api.model.Context; +import com.isnico.api.model.Result; +import com.isnico.api.model.vo.UserResp; +import com.isnico.api.service.UserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequestMapping("/api/v1/user") +@Validated +@Api(tags = "User") +public class UserController { + + @Autowired + private UserService userService; + + @Auth + @GetMapping + @ApiOperation(value = "用户信息") + public Result userInfo(){ + return Result.ok(userService.getUserInfo(Context.get().getId())); + } + +} diff --git a/api/src/main/java/com/isnico/api/enums/ResultCode.java b/api/src/main/java/com/isnico/api/enums/ResultCode.java new file mode 100644 index 0000000..6468417 --- /dev/null +++ b/api/src/main/java/com/isnico/api/enums/ResultCode.java @@ -0,0 +1,45 @@ +package com.isnico.api.enums; + +import com.isnico.api.exception.BusinessException; + +public enum ResultCode { + + SUCCESS(0, "请求成功"), + ERROR_ON_TOKEN_INVALID(401, "无效令牌"), + ERROR(500, "系统错误"), + + ERROR_ON_AUTH_CODE_AUTH_FAIL(100001, "验证码错误"), + ERROR_ON_USER_NOT_EXIST(100002, "账户不存在"), + ERROR_ON_USER_PASSWORD_ERROR(100003, "账号或密码错误"), + ; + + private int code; + + private String msg; + + private ResultCode(int code, String msg) { + this.code = code; + this.msg = msg; + } + + public BusinessException error(){ + return new BusinessException(this); + } + + public final int getCode() { + return code; + } + + public final void setCode(int code) { + this.code = code; + } + + public final String getMsg() { + return msg; + } + + public final void setMsg(String msg) { + this.msg = msg; + } + +} diff --git a/api/src/main/java/com/isnico/api/exception/BusinessException.java b/api/src/main/java/com/isnico/api/exception/BusinessException.java new file mode 100644 index 0000000..bd5548f --- /dev/null +++ b/api/src/main/java/com/isnico/api/exception/BusinessException.java @@ -0,0 +1,43 @@ +package com.isnico.api.exception; + +import com.isnico.api.enums.ResultCode; +import lombok.Data; + +@Data +public class BusinessException extends RuntimeException{ + + private static final long serialVersionUID = -4285570151529511652L; + + private int code; + + private String msg; + + private ResultCode resultCode; + + public BusinessException(ResultCode resultCode) { + this.code = resultCode.getCode(); + this.msg = resultCode.getMsg(); + this.resultCode = resultCode; + } + + public BusinessException() { + super(); + } + + public BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public BusinessException(String message, Throwable cause) { + super(message, cause); + } + + public BusinessException(String message) { + super(message); + } + + public BusinessException(Throwable cause) { + super(cause); + } + +} diff --git a/api/src/main/java/com/isnico/api/exception/GlobalExceptionAdvice.java b/api/src/main/java/com/isnico/api/exception/GlobalExceptionAdvice.java new file mode 100644 index 0000000..0aceb29 --- /dev/null +++ b/api/src/main/java/com/isnico/api/exception/GlobalExceptionAdvice.java @@ -0,0 +1,36 @@ +package com.isnico.api.exception; + +import javax.validation.ConstraintViolationException; + +import com.isnico.api.enums.ResultCode; +import com.isnico.api.model.Result; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + + +@ControllerAdvice +public class GlobalExceptionAdvice { + + @ResponseBody + @ExceptionHandler(value = Exception.class) + public Result errorHandler(Exception ex) { + ex.printStackTrace(); + return new Result<>(ResultCode.ERROR); + } + + @ResponseBody + @ExceptionHandler(value = BusinessException.class) + public Result errorHandler(BusinessException ex) { + ex.printStackTrace(); + return new Result<>(ex.getResultCode()); + } + + @ResponseBody + @ExceptionHandler(value = ConstraintViolationException.class) + public Result errorHandler(ConstraintViolationException ex) { + ex.printStackTrace(); + return Result.fail(ResultCode.ERROR.getCode(), ex.getMessage()); + } + +} diff --git a/api/src/main/java/com/isnico/api/filter/AuthFilter.java b/api/src/main/java/com/isnico/api/filter/AuthFilter.java new file mode 100644 index 0000000..5439e23 --- /dev/null +++ b/api/src/main/java/com/isnico/api/filter/AuthFilter.java @@ -0,0 +1,40 @@ +package com.isnico.api.filter; + +import com.isnico.api.annotation.Auth; +import com.isnico.api.enums.ResultCode; +import com.isnico.api.exception.BusinessException; +import com.isnico.api.model.Context; +import com.isnico.api.model.Result; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class AuthFilter { + + @Pointcut("@annotation(auth)") + public void authPath(Auth auth){} + + @Around(value="authPath(auth)", argNames = "point,auth") + public Object aroundMethod(ProceedingJoinPoint point, Auth auth){ + Object result; + try { + if (Context.get() == null){ + result = Result.fail(ResultCode.ERROR_ON_TOKEN_INVALID); + }else{ + result = point.proceed(); + } + } catch(BusinessException e) { + result = Result.fail(e.getResultCode()); + } catch (Throwable e) { + result = Result.fail(ResultCode.ERROR.getCode(), e.getMessage()); + } + return result; + } +} diff --git a/api/src/main/java/com/isnico/api/filter/ContextFilter.java b/api/src/main/java/com/isnico/api/filter/ContextFilter.java new file mode 100644 index 0000000..41f9117 --- /dev/null +++ b/api/src/main/java/com/isnico/api/filter/ContextFilter.java @@ -0,0 +1,59 @@ +package com.isnico.api.filter; + +import com.isnico.api.component.RedisUtil; +import com.isnico.api.consts.AppConst; +import com.isnico.api.model.Context; +import com.isnico.api.model.vo.LoginResp; +import com.isnico.api.util.StringUtil; +import org.apache.commons.lang3.StringUtils; +import org.nico.noson.Noson; +import org.slf4j.MDC; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.servlet.annotation.WebFilter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +@WebFilter(filterName = "contextFilter", urlPatterns = "/*") +public class ContextFilter implements Filter { + + @Autowired + private RedisUtil redisUtil; + + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + this.doIt((HttpServletRequest)request, (HttpServletResponse)response, chain); + } + + protected void doIt(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + try { + String authorization = request.getHeader("Authorization"); + if (StringUtils.isNotBlank(authorization)){ + if (authorization.startsWith("Bearer ")){ + authorization = authorization.substring(7); + } + String loginUserInfoJson = redisUtil.get(AppConst.USER_TOKEN + authorization); + if (StringUtils.isNotBlank(loginUserInfoJson)){ + LoginResp loginResp = Noson.convert(loginUserInfoJson, LoginResp.class); + Context ctx = new Context(); + ctx.setId(loginResp.getId()); + ctx.setUsername(loginResp.getUsername()); + Context.set(ctx); + } + } + String traceId = request.getHeader("TraceID"); + if (StringUtils.isNotBlank(traceId)){ + traceId = StringUtil.uuid(); + } + MDC.put("traceId", traceId); + chain.doFilter(request, response); + } finally { + Context.clear(); + MDC.clear(); + } + + } +} diff --git a/api/src/main/java/com/isnico/api/mapper/UserMapper.java b/api/src/main/java/com/isnico/api/mapper/UserMapper.java new file mode 100644 index 0000000..fc10ba0 --- /dev/null +++ b/api/src/main/java/com/isnico/api/mapper/UserMapper.java @@ -0,0 +1,8 @@ +package com.isnico.api.mapper; + +import com.isnico.api.model.po.User; +import org.nico.ourbatis.mapper.SimpleMapper; + +public interface UserMapper extends SimpleMapper{ + +} diff --git a/api/src/main/java/com/isnico/api/model/Context.java b/api/src/main/java/com/isnico/api/model/Context.java new file mode 100644 index 0000000..583c0dd --- /dev/null +++ b/api/src/main/java/com/isnico/api/model/Context.java @@ -0,0 +1,26 @@ +package com.isnico.api.model; + +import lombok.Data; + +@Data +public class Context { + + private static final ThreadLocal CONTEXTS = new ThreadLocal<>(); + + private Long id; + + private String username; + + public static void set(Context ctx){ + CONTEXTS.set(ctx); + } + + public static void clear(){ + CONTEXTS.remove(); + } + + public static Context get(){ + return CONTEXTS.get(); + } + +} diff --git a/api/src/main/java/com/isnico/api/model/Result.java b/api/src/main/java/com/isnico/api/model/Result.java new file mode 100644 index 0000000..19f26af --- /dev/null +++ b/api/src/main/java/com/isnico/api/model/Result.java @@ -0,0 +1,49 @@ +package com.isnico.api.model; + +import com.isnico.api.enums.ResultCode; +import lombok.Data; + +@Data +public class Result { + + private int code; + + private String msg; + + private T data; + + public Result() { } + + public Result(ResultCode code) { + this.code = code.getCode(); + this.msg = code.getMsg(); + } + + public Result(ResultCode code, T data) { + this.code = code.getCode(); + this.msg = code.getMsg(); + this.data = data; + } + + public static Result ok(T data){ + Result result = new Result(ResultCode.SUCCESS); + result.setData(data); + return result; + } + + public static Result ok(){ + return new Result(ResultCode.SUCCESS); + } + + public static Result fail(ResultCode code){ + return new Result(code); + } + + public static Result fail(int code, String msg){ + Result result = new Result(); + result.setCode(code); + result.setMsg(msg); + return result; + } + +} diff --git a/api/src/main/java/com/isnico/api/model/po/User.java b/api/src/main/java/com/isnico/api/model/po/User.java new file mode 100644 index 0000000..44ee2f0 --- /dev/null +++ b/api/src/main/java/com/isnico/api/model/po/User.java @@ -0,0 +1,34 @@ +package com.isnico.api.model.po; + +import java.util.Date; + +import lombok.Data; +import org.nico.ourbatis.annotation.RenderPrimary; + +@Data +public class User { + + @RenderPrimary + private Long id; + + private String name; + + private String avatar; + + private Long score; + + private Long victory; + + private Long defeat; + + private String username; + + private String password; + + private Date createTime; + + private Date updateTime; + + private Integer deleted; + +} diff --git a/api/src/main/java/com/isnico/api/model/vo/LoginResp.java b/api/src/main/java/com/isnico/api/model/vo/LoginResp.java new file mode 100644 index 0000000..891fe7a --- /dev/null +++ b/api/src/main/java/com/isnico/api/model/vo/LoginResp.java @@ -0,0 +1,24 @@ +package com.isnico.api.model.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class LoginResp { + + @ApiModelProperty("用户ID") + private Long id; + + @ApiModelProperty("名称") + private String name; + + @ApiModelProperty("积分") + private Long score; + + @ApiModelProperty("用户昵称") + private String username; + + @ApiModelProperty("令牌") + private String token; + +} diff --git a/api/src/main/java/com/isnico/api/model/vo/UserResp.java b/api/src/main/java/com/isnico/api/model/vo/UserResp.java new file mode 100644 index 0000000..0f6c1a8 --- /dev/null +++ b/api/src/main/java/com/isnico/api/model/vo/UserResp.java @@ -0,0 +1,29 @@ +package com.isnico.api.model.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class UserResp { + + @ApiModelProperty("id") + private Long id; + + @ApiModelProperty("姓名") + private String name; + + @ApiModelProperty("头像") + private String avatar; + + @ApiModelProperty("账号") + private String username; + + @ApiModelProperty("积分") + private Long score; + + @ApiModelProperty("胜场") + private Long victory; + + @ApiModelProperty("败场") + private Long defeat; +} diff --git a/api/src/main/java/com/isnico/api/service/AuthService.java b/api/src/main/java/com/isnico/api/service/AuthService.java new file mode 100644 index 0000000..5363c24 --- /dev/null +++ b/api/src/main/java/com/isnico/api/service/AuthService.java @@ -0,0 +1,65 @@ +package com.isnico.api.service; + +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import com.isnico.api.component.RedisUtil; +import com.isnico.api.consts.AppConst; +import com.isnico.api.enums.ResultCode; +import com.isnico.api.exception.BusinessException; +import com.isnico.api.model.po.User; +import com.isnico.api.mapper.UserMapper; +import com.isnico.api.model.vo.LoginResp; +import com.isnico.api.util.StringUtil; +import org.nico.noson.Noson; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class AuthService { + + @Autowired + private UserMapper userMapper; + + @Autowired + private RedisUtil redisUtil; + + public LoginResp register(String username, String password, String name, String code){ + String authCode = String.valueOf(redisUtil.get(AppConst.USER_REGISTER_AUTH_CODE + username)); + if (!Objects.equals(authCode.toLowerCase(Locale.ROOT), code.toLowerCase(Locale.ROOT))){ + throw new BusinessException(ResultCode.ERROR_ON_AUTH_CODE_AUTH_FAIL); + } + User user = new User(); + user.setName(name); + user.setUsername(username); + user.setPassword(StringUtil.md5(username + "@" + password)); + userMapper.insertSelective(user); + return setLoginInfo(user); + } + + public LoginResp login(String username, String password){ + User cond = new User(); + cond.setUsername(username); + User user = userMapper.selectEntity(cond); + if(user == null) { + throw new BusinessException(ResultCode.ERROR_ON_USER_NOT_EXIST); + } + if(user.getPassword().equalsIgnoreCase(StringUtil.md5(StringUtil.md5(username + "@" + password)))) { + throw new BusinessException(ResultCode.ERROR_ON_USER_PASSWORD_ERROR); + } + return setLoginInfo(user); + } + + public LoginResp setLoginInfo(User user){ + LoginResp resp = new LoginResp(); + resp.setId(user.getId()); + resp.setName(user.getName()); + resp.setScore(user.getScore()); + resp.setUsername(user.getUsername()); + resp.setToken(StringUtil.random(48)); + redisUtil.set(AppConst.USER_TOKEN + resp.getToken(), Noson.reversal(resp), 15, TimeUnit.DAYS); + return resp; + } + +} diff --git a/api/src/main/java/com/isnico/api/service/UserService.java b/api/src/main/java/com/isnico/api/service/UserService.java new file mode 100644 index 0000000..c7b850b --- /dev/null +++ b/api/src/main/java/com/isnico/api/service/UserService.java @@ -0,0 +1,31 @@ +package com.isnico.api.service; + +import com.isnico.api.consts.AppConst; +import com.isnico.api.enums.ResultCode; +import com.isnico.api.exception.BusinessException; +import com.isnico.api.mapper.UserMapper; +import com.isnico.api.model.po.User; +import com.isnico.api.model.vo.UserResp; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class UserService { + + @Autowired + private UserMapper userMapper; + + public UserResp getUserInfo(Long id){ + User cond = new User(); + cond.setId(id); + cond.setDeleted(AppConst.FALSE); + User user = userMapper.selectEntity(cond); + if (user == null){ + throw ResultCode.ERROR_ON_USER_NOT_EXIST.error(); + } + UserResp userResp = new UserResp(); + BeanUtils.copyProperties(user, userResp); + return userResp; + } +} diff --git a/api/src/main/java/com/isnico/api/util/HttpUtil.java b/api/src/main/java/com/isnico/api/util/HttpUtil.java new file mode 100644 index 0000000..7d68257 --- /dev/null +++ b/api/src/main/java/com/isnico/api/util/HttpUtil.java @@ -0,0 +1,105 @@ +package com.isnico.api.util; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; + +/** + * @author dataochen + * @Description + * @date: 2017/11/7 17:49 + */ +public class HttpUtil { + + private static CloseableHttpClient httpClient; + + static { + PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); + cm.setMaxTotal(100); + cm.setDefaultMaxPerRoute(20); + cm.setDefaultMaxPerRoute(50); + httpClient = HttpClients.custom().setConnectionManager(cm).build(); + } + + public static String get(String url) { + CloseableHttpResponse response = null; + BufferedReader in = null; + String result = ""; + try { + HttpGet httpGet = new HttpGet(url); + RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(30000).setConnectionRequestTimeout(30000).setSocketTimeout(30000).build(); + httpGet.setConfig(requestConfig); + httpGet.setConfig(requestConfig); + httpGet.addHeader("Content-type", "application/json; charset=utf-8"); + httpGet.setHeader("Accept", "application/json"); + response = httpClient.execute(httpGet); + in = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); + StringBuffer sb = new StringBuffer(""); + String line = ""; + String NL = System.getProperty("line.separator"); + while ((line = in.readLine()) != null) { + sb.append(line + NL); + } + in.close(); + result = sb.toString(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (null != response) { + response.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return result; + } + + public static String post(String url, String jsonString) { + CloseableHttpResponse response = null; + BufferedReader in = null; + String result = ""; + try { + HttpPost httpPost = new HttpPost(url); + RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(30000).setConnectionRequestTimeout(30000).setSocketTimeout(30000).build(); + httpPost.setConfig(requestConfig); + httpPost.setConfig(requestConfig); + httpPost.addHeader("Content-type", "application/json; charset=utf-8"); + httpPost.setHeader("Accept", "application/json"); + httpPost.setEntity(new StringEntity(jsonString, Charset.forName("UTF-8"))); + response = httpClient.execute(httpPost); + in = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); + StringBuffer sb = new StringBuffer(""); + String line = ""; + String NL = System.getProperty("line.separator"); + while ((line = in.readLine()) != null) { + sb.append(line + NL); + } + in.close(); + result = sb.toString(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (null != response) { + response.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return result; + } + +} diff --git a/api/src/main/java/com/isnico/api/util/StringUtil.java b/api/src/main/java/com/isnico/api/util/StringUtil.java new file mode 100644 index 0000000..eaf6568 --- /dev/null +++ b/api/src/main/java/com/isnico/api/util/StringUtil.java @@ -0,0 +1,40 @@ +package com.isnico.api.util; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Random; +import java.util.UUID; + +public class StringUtil { + + public static String random(int len){ + String sources = "0123456789ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz"; + Random rand = new Random(); + StringBuffer flag = new StringBuffer(); + for (int j = 0; j < len; j++) + { + flag.append(sources.charAt(rand.nextInt(sources.length() - 1)) + ""); + } + return flag.toString(); + } + + public static String md5(String plainText) { + byte[] secretBytes = null; + try { + secretBytes = MessageDigest.getInstance("md5").digest( + plainText.getBytes()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + StringBuilder md5code = new StringBuilder(new BigInteger(1, secretBytes).toString(16)); + for (int i = 0; i < 32 - md5code.length(); i++) { + md5code.insert(0, "0"); + } + return md5code.toString(); + } + + public static String uuid(){ + return UUID.randomUUID().toString().replaceAll("-",""); + } +} diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties new file mode 100644 index 0000000..aff3f19 --- /dev/null +++ b/api/src/main/resources/application.properties @@ -0,0 +1,32 @@ +server.port=9088 + +spring.datasource.url=jdbc:mysql://127.0.0.1:3306/ratel?characterEncoding=UTF-8&useUnicode=true&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useAffectedRows=true +spring.datasource.username=root +spring.datasource.password=root +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +spring.redis.database=0 +spring.redis.host=127.0.0.1 +spring.redis.port=6379 +spring.redis.password= + +ourbatis.domain-locations=com.isnico.api.model.po +logging.level.org.nico.ourbatis=debug + +spring.mail.host=smtp.qq.com +spring.mail.username=xxx +spring.mail.password=xxx +spring.mail.default-encoding=UTF-8 +spring.mail.properties.mail.smtp.starttls.enable=true +spring.mail.properties.mail.smtp.starttls.required=true +spring.mail.properties.mail.smtp.socketFactory.port=465 +spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory +spring.mail.properties.mail.smtp.ssl.trust=smtp.exmail.qq.com +spring.mail.properties.mail.smtp.connectiontimeout=30000 +spring.mail.properties.mail.smtp.timeout=20000 +spring.mail.properties.mail.smtp.writetimeout=10000 + +pagehelper.helperDialect=mysql +pagehelper.reasonable=false +pagehelper.supportMethodsArguments=true +pagehelper.autoRuntimeDialect=true diff --git a/api/src/main/resources/logback-spring.xml b/api/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..c41724a --- /dev/null +++ b/api/src/main/resources/logback-spring.xml @@ -0,0 +1,172 @@ + + + + + + ${spring.application.name} + + + + + + + + + ${log.pattern} + + + + + + + ${logPath}/${serviceName}/debug.log + + + + ${logPath}/${serviceName}/debug/debug.%d{yyyy-MM-dd}.%i.log.gz + + ${log.maxHistory} + ${log.maxFileSize}MB + + + ${log.pattern} + + + + + + + ${logPath}/${serviceName}/info.log + + + + ${logPath}/${serviceName}/info/info.%d{yyyy-MM-dd}.%i.log.gz + + ${log.maxHistory} + ${log.maxFileSize}MB + + + ${log.pattern} + + + + + + + ${logPath}/${serviceName}/warn.log + + + + ${logPath}/${serviceName}/warn/warn.%d{yyyy-MM-dd}.%i.log.gz + + ${log.maxHistory} + ${log.maxFileSize}MB + + + ${log.pattern} + + + + + + + ${logPath}/${serviceName}/error.log + + + + ${logPath}/${serviceName}/error/error.%d{yyyy-MM-dd}.%i.log.gz + + ${log.maxHistory} + ${log.maxFileSize}MB + + + ${log.pattern} + + + + + + + ${logPath}/${serviceName}/rr.log + + + + ${logPath}/${serviceName}/rr/rr.%d{yyyy-MM-dd}.%i.log.gz + + ${log.maxHistory} + ${log.maxFileSize}MB + + + ${log.pattern} + + + + + + + + DEBUG + ACCEPT + DENY + + + + + + + INFO + ACCEPT + DENY + + + + + + + WARN + ACCEPT + DENY + + + + + + + ERROR + ACCEPT + DENY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/LICENSE b/client/LICENSE new file mode 100644 index 0000000..b777b61 --- /dev/null +++ b/client/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 ratel-online + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..7d95379 --- /dev/null +++ b/client/README.md @@ -0,0 +1,18 @@ +# Install +```shell +go install github.com/ratel-online/client@latest +``` + +# Usage +连接本地 +```shell +client +``` +连接远程 +```shell +client -h 49.235.95.125 +``` +连接时指定名字 +```shell +client -n nico +``` \ No newline at end of file diff --git a/client/api/login.go b/client/api/login.go new file mode 100644 index 0000000..ae8ba9e --- /dev/null +++ b/client/api/login.go @@ -0,0 +1,22 @@ +package api + +import ( + "github.com/ratel-online/client/model" + "github.com/ratel-online/core/util/http" + "github.com/ratel-online/core/util/json" +) + +const API = "http://127.0.0.1:9088" + +func Login(username, password string) (*model.LoginResp, error) { + body, err := http.Post(API+"/api/v1/login", map[string]interface{}{ + "username": username, + "password": password, + }, nil) + if err != nil { + return nil, err + } + resp := model.LoginResp{} + _ = json.Unmarshal(body, &resp) + return &resp, nil +} diff --git a/client/ctx/ctx.go b/client/ctx/ctx.go new file mode 100644 index 0000000..5230794 --- /dev/null +++ b/client/ctx/ctx.go @@ -0,0 +1,138 @@ +package ctx + +import ( + "errors" + "fmt" + "github.com/gorilla/websocket" + "github.com/ratel-online/client/model" + "github.com/ratel-online/client/util" + "github.com/ratel-online/core/consts" + "github.com/ratel-online/core/log" + modelx "github.com/ratel-online/core/model" + "github.com/ratel-online/core/network" + "github.com/ratel-online/core/protocol" + "github.com/ratel-online/core/util/async" + "net" + "net/url" + "strings" + "sync" +) + +const cleanLine = "\r\r \r\r" + +type Context struct { + sync.Mutex + id int64 + name string + score int64 + token string + roomId int64 + + conn *network.Conn +} + +type netConnector func(addr string) (*network.Conn, error) + +var netConnectors = map[string]netConnector{ + "tcp": tcpConnect, + "ws": websocketConnect, +} + +func New(user model.LoginRespData) *Context { + return &Context{ + id: user.ID, + name: user.Name, + score: user.Score, + token: user.Token, + } +} + +func (c *Context) Connect(net string, addr string) error { + if connector, ok := netConnectors[net]; ok { + conn, err := connector(addr) + if err != nil { + return err + } + c.conn = conn + return nil + } + return errors.New(fmt.Sprintf("unsupported net type: %s", net)) +} + +func (c *Context) Auth() error { + return c.conn.Write(protocol.ObjectPacket(modelx.AuthInfo{ + ID: c.id, + Name: c.name, + Score: c.score, + })) +} + +func (c *Context) Listener() error { + is := false + async.Async(func() { + for { + line, err := util.Readline() + if err != nil { + log.Panic(err) + } + if !is { + continue + } + c.print(fmt.Sprintf(cleanLine+"[%s@ratel %s]# ", strings.TrimSpace(strings.ToLower(c.name)), "~")) + err = c.conn.Write(protocol.Packet{ + Body: line, + }) + if err != nil { + continue + } + } + }) + return c.conn.Accept(func(packet protocol.Packet, conn *network.Conn) { + data := string(packet.Body) + if data == consts.IsStart { + if !is { + c.print(fmt.Sprintf(cleanLine+"[%s@ratel %s]# ", strings.TrimSpace(strings.ToLower(c.name)), "~")) + } + is = true + return + } else if data == consts.IsStop { + if is { + c.print(cleanLine) + } + is = false + return + } + if is { + c.print(cleanLine + data + fmt.Sprintf(cleanLine+"[%s@ratel %s]# ", strings.TrimSpace(strings.ToLower(c.name)), "~")) + } else { + c.print(data) + } + }) +} + +func (c *Context) print(str string) { + c.Lock() + defer c.Unlock() + fmt.Print(str) +} + +func tcpConnect(addr string) (*network.Conn, error) { + tcpAddr, err := net.ResolveTCPAddr("tcp", addr) + if err != nil { + return nil, errors.New(fmt.Sprintf("tcp server error: %v", err)) + } + conn, err := net.DialTCP("tcp", nil, tcpAddr) + if err != nil { + return nil, errors.New(fmt.Sprintf("tcp server error: %v", err)) + } + return network.Wrapper(protocol.NewTcpReadWriteCloser(conn)), nil +} + +func websocketConnect(addr string) (*network.Conn, error) { + u := url.URL{Scheme: "ws", Host: addr, Path: "/ws"} + conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + return nil, errors.New(fmt.Sprintf("ws server error: %v", err)) + } + return network.Wrapper(protocol.NewWebsocketReadWriteCloser(conn)), nil +} diff --git a/client/ctx/ctx_test.go b/client/ctx/ctx_test.go new file mode 100644 index 0000000..70e9e3c --- /dev/null +++ b/client/ctx/ctx_test.go @@ -0,0 +1,69 @@ +package ctx + +import ( + "encoding/binary" + "github.com/ratel-online/client/model" + "github.com/ratel-online/core/consts" + "github.com/ratel-online/core/log" + "github.com/ratel-online/core/network" + "github.com/ratel-online/core/protocol" + "github.com/ratel-online/core/util/async" + "net" + "testing" +) + +func TestContext_Connect(t *testing.T) { + data := make([]byte, 4) + binary.BigEndian.PutUint32(data, 4294967294) + tcpAddr, err := net.ResolveTCPAddr("tcp", "49.235.95.125:9999") + if err != nil { + t.Log(err) + return + } + conn, err := net.DialTCP("tcp", nil, tcpAddr) + if err != nil { + t.Log(err) + return + } + conn.Write(data) + select {} +} + +func TestContext_Listener(t *testing.T) { + for i := 1; i <= 5000; i++ { + id := i + async.Async(func() { + _singleTest(int64(id)) + }) + } + select {} +} + +func _singleTest(id int64) { + ctx := New(model.LoginRespData{ + ID: id, + Name: "a", + Score: 100, + Username: "b", + Token: "aeiou", + }) + err := ctx.Connect("tcp", "49.235.95.125:9999") + if err != nil { + log.Error(err) + return + } + err = ctx.Auth() + if err != nil { + log.Error(err) + return + } + _ = ctx.conn.Accept(func(packet protocol.Packet, conn *network.Conn) { + data := string(packet.Body) + if data == consts.IS_START { + _ = conn.Write(protocol.Packet{ + Body: []byte("2"), + }) + return + } + }) +} diff --git a/client/go.mod b/client/go.mod new file mode 100644 index 0000000..0f5be07 --- /dev/null +++ b/client/go.mod @@ -0,0 +1,15 @@ +module github.com/ratel-online/client + +go 1.17 + +require ( + github.com/gorilla/websocket v1.4.2 + github.com/ratel-online/core v0.0.0-20220812071405-08d34103ae05 +) + +require ( + github.com/awesome-cap/im v0.0.0-20210720090440-7556eb92965d // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect +) diff --git a/client/go.sum b/client/go.sum new file mode 100644 index 0000000..9d9b1f1 --- /dev/null +++ b/client/go.sum @@ -0,0 +1,51 @@ +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/awesome-cap/im v0.0.0-20210720090440-7556eb92965d h1:lc5kZK/lqmqAfNeXcRPkwXKdwO/9fQ09Wpz9p/qp4So= +github.com/awesome-cap/im v0.0.0-20210720090440-7556eb92965d/go.mod h1:dIipyrtcBTc6up0MNw5w7ONoNKGksz2EGCVlcwiRjE4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/memberlist v0.2.4/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/ratel-online/core v0.0.0-20220812071405-08d34103ae05 h1:aE5pagmuK1V0JR9/ldzOOjiVsXuv1vP5G41Fl3cEjCU= +github.com/ratel-online/core v0.0.0-20220812071405-08d34103ae05/go.mod h1:8SYaPGDk9dVGnUIEkanZvV1ErTqd8OdedKNCwyXgRjI= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/client/main.go b/client/main.go new file mode 100644 index 0000000..45b0d70 --- /dev/null +++ b/client/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "flag" + "fmt" + "github.com/ratel-online/client/shell" + "github.com/ratel-online/core/log" +) + +var ( + host string + port int + name string +) + +func init() { + flag.StringVar(&host, "h", "127.0.0.1", "host") + flag.IntVar(&port, "p", 9999, "port") + flag.StringVar(&name, "n", "", "name") + flag.Parse() +} + +func main() { + addr := fmt.Sprintf("%s:%d", host, port) + log.Error(shell.New(addr, name).Start()) +} diff --git a/client/model/model.go b/client/model/model.go new file mode 100644 index 0000000..acd630b --- /dev/null +++ b/client/model/model.go @@ -0,0 +1,19 @@ +package model + +type Common struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +type LoginResp struct { + Common + Data LoginRespData `json:"data"` +} + +type LoginRespData struct { + ID int64 `json:"id"` + Name string `json:"name"` + Score int64 `json:"score"` + Username string `json:"username"` + Token string `json:"token"` +} diff --git a/client/shell/shell.go b/client/shell/shell.go new file mode 100644 index 0000000..6642dbc --- /dev/null +++ b/client/shell/shell.go @@ -0,0 +1,52 @@ +package shell + +import ( + "github.com/ratel-online/client/ctx" + "github.com/ratel-online/client/model" + "github.com/ratel-online/client/util" + "github.com/ratel-online/core/log" + "strings" + "time" +) + +type shell struct { + ctx *ctx.Context + addr string + name string +} + +func New(addr, name string) *shell { + return &shell{ + addr: addr, + name: name, + } +} + +func (s *shell) Start() error { + name := util.RandomName() + if s.name != "" { + name = s.name + } + s.ctx = ctx.New(model.LoginRespData{ + ID: time.Now().UnixNano(), + Name: name, + Score: 100, + Username: name, + Token: "aeiou", + }) + net := "tcp" + if strings.HasSuffix(s.addr, "9998") { + net = "ws" + } + err := s.ctx.Connect(net, s.addr) + if err != nil { + log.Error(err) + return err + } + err = s.ctx.Auth() + if err != nil { + log.Error(err) + return err + } + return s.ctx.Listener() +} diff --git a/client/util/rand.go b/client/util/rand.go new file mode 100644 index 0000000..47dc1e4 --- /dev/null +++ b/client/util/rand.go @@ -0,0 +1,56 @@ +package util + +import ( + "fmt" + "math/rand" + "time" +) + +var lastName = []string{ + "赵", "钱", "孙", "李", "周", "吴", "郑", "王", "冯", "陈", "褚", "卫", "蒋", + "沈", "韩", "杨", "朱", "秦", "尤", "许", "何", "吕", "施", "张", "孔", "曹", "严", "华", "金", "魏", + "陶", "姜", "戚", "谢", "邹", "喻", "柏", "水", "窦", "章", "云", "苏", "潘", "葛", "奚", "范", "彭", + "郎", "鲁", "韦", "昌", "马", "苗", "凤", "花", "方", "任", "袁", "柳", "鲍", "史", "唐", "费", "薛", + "雷", "贺", "倪", "汤", "滕", "殷", "罗", "毕", "郝", "安", "常", "傅", "卞", "齐", "元", "顾", "孟", + "平", "黄", "穆", "萧", "尹", "姚", "邵", "湛", "汪", "祁", "毛", "狄", "米", "伏", "成", "戴", "谈", + "宋", "茅", "庞", "熊", "纪", "舒", "屈", "项", "祝", "董", "梁", "杜", "阮", "蓝", "闵", "季", "贾", + "路", "娄", "江", "童", "颜", "郭", "梅", "盛", "林", "钟", "徐", "邱", "骆", "高", "夏", "蔡", "田", + "樊", "胡", "凌", "霍", "虞", "万", "支", "柯", "管", "卢", "莫", "柯", "房", "裘", "缪", "解", "应", + "宗", "丁", "宣", "邓", "单", "杭", "洪", "包", "诸", "左", "石", "崔", "吉", "龚", "程", "嵇", "邢", + "裴", "陆", "荣", "翁", "荀", "于", "惠", "甄", "曲", "封", "储", "仲", "伊", "宁", "仇", "甘", "武", + "符", "刘", "景", "詹", "龙", "叶", "幸", "司", "黎", "溥", "印", "怀", "蒲", "邰", "从", "索", "赖", + "卓", "屠", "池", "乔", "胥", "闻", "莘", "党", "翟", "谭", "贡", "劳", "逄", "姬", "申", "扶", "堵", + "冉", "宰", "雍", "桑", "寿", "通", "燕", "浦", "尚", "农", "温", "别", "庄", "晏", "柴", "瞿", "阎", + "连", "习", "容", "向", "古", "易", "廖", "庾", "终", "步", "都", "耿", "满", "弘", "匡", "国", "文", + "寇", "广", "禄", "阙", "东", "欧", "利", "师", "巩", "聂", "关", "荆", "司马", "上官", "欧阳", "夏侯", + "诸葛", "闻人", "东方", "赫连", "皇甫", "尉迟", "公羊", "澹台", "公冶", "宗政", "濮阳", "淳于", "单于", + "太叔", "申屠", "公孙", "仲孙", "轩辕", "令狐", "徐离", "宇文", "长孙", "慕容", "司徒", "司空"} +var firstName = []string{ + "伟", "刚", "勇", "毅", "俊", "峰", "强", "军", "平", "保", "东", "文", "辉", "力", "明", "永", "健", "世", "广", "志", "义", + "兴", "良", "海", "山", "仁", "波", "宁", "贵", "福", "生", "龙", "元", "全", "国", "胜", "学", "祥", "才", "发", "武", "新", + "利", "清", "飞", "彬", "富", "顺", "信", "子", "杰", "涛", "昌", "成", "康", "星", "光", "天", "达", "安", "岩", "中", "茂", + "进", "林", "有", "坚", "和", "彪", "博", "诚", "先", "敬", "震", "振", "壮", "会", "思", "群", "豪", "心", "邦", "承", "乐", + "绍", "功", "松", "善", "厚", "庆", "磊", "民", "友", "裕", "河", "哲", "江", "超", "浩", "亮", "政", "谦", "亨", "奇", "固", + "之", "轮", "翰", "朗", "伯", "宏", "言", "若", "鸣", "朋", "斌", "梁", "栋", "维", "启", "克", "伦", "翔", "旭", "鹏", "泽", + "晨", "辰", "士", "以", "建", "家", "致", "树", "炎", "德", "行", "时", "泰", "盛", "雄", "琛", "钧", "冠", "策", "腾", "楠", + "榕", "风", "航", "弘", "秀", "娟", "英", "华", "慧", "巧", "美", "娜", "静", "淑", "惠", "珠", "翠", "雅", "芝", "玉", "萍", + "红", "娥", "玲", "芬", "芳", "燕", "彩", "春", "菊", "兰", "凤", "洁", "梅", "琳", "素", "云", "莲", "真", "环", "雪", "荣", + "爱", "妹", "霞", "香", "月", "莺", "媛", "艳", "瑞", "凡", "佳", "嘉", "琼", "勤", "珍", "贞", "莉", "桂", "娣", "叶", "璧", + "璐", "娅", "琦", "晶", "妍", "茜", "秋", "珊", "莎", "锦", "黛", "青", "倩", "婷", "姣", "婉", "娴", "瑾", "颖", "露", "瑶", + "怡", "婵", "雁", "蓓", "纨", "仪", "荷", "丹", "蓉", "眉", "君", "琴", "蕊", "薇", "菁", "梦", "岚", "苑", "婕", "馨", "瑗", + "琰", "韵", "融", "园", "艺", "咏", "卿", "聪", "澜", "纯", "毓", "悦", "昭", "冰", "爽", "琬", "茗", "羽", "希", "欣", "飘", + "育", "滢", "馥", "筠", "柔", "竹", "霭", "凝", "晓", "欢", "霄", "枫", "芸", "菲", "寒", "伊", "亚", "宜", "可", "姬", "舒", + "影", "荔", "枝", "丽", "阳", "妮", "宝", "贝", "初", "程", "梵", "罡", "恒", "鸿", "桦", "骅", "剑", "娇", "纪", "宽", "苛", + "灵", "玛", "媚", "琪", "晴", "容", "睿", "烁", "堂", "唯", "威", "韦", "雯", "苇", "萱", "阅", "彦", "宇", "雨", "洋", "忠", + "宗", "曼", "紫", "逸", "贤", "蝶", "菡", "绿", "蓝", "儿", "翠", "烟", "小", "轩"} +var lastNameLen = len(lastName) +var firstNameLen = len(firstName) + +func RandomName() string { + rand.Seed(time.Now().UnixNano()) //设置随机数种子 + var first string //名 + for i := 0; i <= rand.Intn(1); i++ { //随机产生2位或者3位的名 + first = fmt.Sprint(firstName[rand.Intn(firstNameLen-1)]) + } + return fmt.Sprintf("%s%s", fmt.Sprint(lastName[rand.Intn(lastNameLen-1)]), first) +} diff --git a/client/util/stdin.go b/client/util/stdin.go new file mode 100644 index 0000000..13fab8f --- /dev/null +++ b/client/util/stdin.go @@ -0,0 +1,36 @@ +/** +2 * @Author: Nico +3 * @Date: 2021/5/23 20:28 +4 */ +package util + +import ( + "bufio" + "bytes" + "fmt" + "os" + "strings" +) + +var ( + in *bufio.Reader = bufio.NewReader(os.Stdin) + out *bufio.Writer = bufio.NewWriter(os.Stdout) + buffer = bytes.Buffer{} +) + +func Readline() ([]byte, error) { + lines, err := in.ReadBytes('\n') + if err != nil { + return nil, err + } + return []byte(strings.TrimSpace(string(lines[0 : len(lines)-1]))), nil +} + +func ReadBuffer() []byte { + return buffer.Bytes() +} + +func EraseLine() { + fmt.Printf("\033[1A") + fmt.Printf("\r\r") +} diff --git a/core/LICENSE b/core/LICENSE new file mode 100644 index 0000000..b777b61 --- /dev/null +++ b/core/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 ratel-online + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..4c56471 --- /dev/null +++ b/core/README.md @@ -0,0 +1 @@ +# core \ No newline at end of file diff --git a/core/consts/consts.go b/core/consts/consts.go new file mode 100644 index 0000000..500a477 --- /dev/null +++ b/core/consts/consts.go @@ -0,0 +1,45 @@ +package consts + +const ( + IsStart = "INTERACTIVE_SIGNAL_START" + IsStop = "INTERACTIVE_SIGNAL_STOP" + MaxPacketSize = 65536 +) + +const ( + CodeWelcome = 1001 + CodeHomeOptions = 1002 + CodeRoomList = 1003 + CodeRoomEventJoin = 1004 + CodeGameTypeOptions = 1005 + CodeRoomEventCreate = 1006 + CodeRoomEventExit = 1007 + CodeRoomEventOffline = 1008 + CodeRoomEventOwnerChange = 1009 +) + +type FacesType int + +const ( + _ FacesType = iota + FacesBomb = 1 //炸彈 + FacesSingle = 2 //單牌 + FacesDouble = 3 //對子 + FacesTriple = 4 //三張 + FacesUnion3 = 5 //三帶一 + FacesUnion4 = 6 //四帶二 + FacesStraight = 7 //順子 or 連隊 + FacesUnion3Straight = 8 //飛機 在跑得快下飛機可以多帶幾張 如 333444 帶 5679 + FacesInvalid = 9 + FacesUnion3c2 = 10 //跑得快三帶二 + FacesUnion3c2s = 11 //跑得快標準三帶二 + FacesUnion4C3 = 12 //跑得快四帶3 + FacesUnion4C3s = 13 //跑得快標準四帶3 + FacesUnion3c2C = 14 //跑得快二連三帶2 + FacesUnion3c2Cs = 15 //跑得快標準二連三帶2 + FacesUnion3c2CM = 16 //跑得快三連三帶2 + FacesUnion3c2CsM = 17 //跑得快標準三連三帶2 + FacesDoubles = 18 //跑得快連隊 + FacesStraights = 19 //跑得快順子 + FacesThreeStraights = 20 //跑得快三連隊 +) diff --git a/core/go.mod b/core/go.mod new file mode 100644 index 0000000..99fbde9 --- /dev/null +++ b/core/go.mod @@ -0,0 +1,20 @@ +module github.com/ratel-online/core + +go 1.17 + +require ( + github.com/awesome-cap/im v0.0.0-20210720090440-7556eb92965d + github.com/gorilla/websocket v1.4.2 + github.com/json-iterator/go v1.1.12 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect +) + +require ( + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/stretchr/testify v1.3.0 +) diff --git a/core/go.sum b/core/go.sum new file mode 100644 index 0000000..71f9664 --- /dev/null +++ b/core/go.sum @@ -0,0 +1,49 @@ +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/awesome-cap/im v0.0.0-20210720090440-7556eb92965d h1:lc5kZK/lqmqAfNeXcRPkwXKdwO/9fQ09Wpz9p/qp4So= +github.com/awesome-cap/im v0.0.0-20210720090440-7556eb92965d/go.mod h1:dIipyrtcBTc6up0MNw5w7ONoNKGksz2EGCVlcwiRjE4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/memberlist v0.2.4/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/core/log/log.go b/core/log/log.go new file mode 100644 index 0000000..0da5982 --- /dev/null +++ b/core/log/log.go @@ -0,0 +1,42 @@ +package log + +import ( + "fmt" + "path/filepath" + "runtime" + "time" +) + +func sprintf(t string, format string, args ...interface{}) string { + _, path, line, _ := runtime.Caller(3) + _, file := filepath.Split(path) + return fmt.Sprintf(fmt.Sprintf("%s [%s] %s:%d %s", time.Now().Format("2006-01-02 15:04:05.999"), t, file, line, format), args...) +} + +func printf(t string, format string, args ...interface{}) { + fmt.Printf(sprintf(t, format, args...)) +} + +func Infof(format string, args ...interface{}) { + printf("INFO", format, args...) +} + +func Info(arg interface{}) { + printf("INFO", "%v\n", arg) +} + +func Errorf(format string, args ...interface{}) { + printf("ERROR", format, args...) +} + +func Error(arg interface{}) { + printf("ERROR", "%v\n", arg) +} + +func Panicf(format string, args ...interface{}) { + panic(sprintf("PANIC", format, args...)) +} + +func Panic(arg interface{}) { + printf("PANIC", "%v\n", arg) +} diff --git a/core/model/model.go b/core/model/model.go new file mode 100644 index 0000000..000ebc7 --- /dev/null +++ b/core/model/model.go @@ -0,0 +1,71 @@ +package model + +type AuthInfo struct { + ID int64 `json:"id"` + Name string `json:"name"` + Score int64 `json:"score"` +} + +type Data struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +type Option struct { + ID int `json:"id"` + Name string `json:"name"` +} + +type Player struct { + ID int64 `json:"id"` + Name string `json:"name"` + Score int64 `json:"score"` + Group int `json:"group"` + Pokers int `json:"pokers"` +} + +type Room struct { + ID int64 `json:"id"` + Name string `json:"name"` + Type int `json:"type"` + TypeDesc string `json:"typeDesc"` + Players int `json:"players"` + State int `json:"state"` + StateDesc string `json:"stateDesc"` + Creator int64 `json:"creator"` +} + +type Options struct { + Data + Options []Option `json:"options"` +} + +type RoomList struct { + Data + Rooms []Room `json:"rooms"` +} + +type RoomInfo struct { + Data + Room Room `json:"room"` + Players []Player `json:"players"` +} + +type RoomEvent struct { + Data + Room Room `json:"room"` + Player Player `json:"player"` +} + +type Play struct { + Data + Player Player `json:"player"` + Pokers Pokers `json:"pokers"` +} + +type GameEvent struct { + Data + Room Room `json:"room"` + Player Player `json:"player"` + Pokers Pokers `json:"pokers"` +} diff --git a/core/model/poker.go b/core/model/poker.go new file mode 100644 index 0000000..37ae2c6 --- /dev/null +++ b/core/model/poker.go @@ -0,0 +1,183 @@ +package model + +import ( + "bytes" + "fmt" + "github.com/ratel-online/core/consts" + "github.com/ratel-online/core/util/arrays" + "math/rand" + "sort" + "strconv" + "time" +) + +type PokerSuit int + +const ( + Spade PokerSuit = iota + Heart + Club + Diamond +) + +func (s PokerSuit) String() string { + switch s { + case Spade: + return "♠" + case Heart: + return "♥" + case Club: + return "♣" + case Diamond: + return "♦" + } + return "" +} + +type Pokers []Poker + +type Poker struct { + Key int `json:"key"` + Val int `json:"val"` + Desc string `json:"desc"` + Oaa bool `json:"oaa"` + Suit PokerSuit `json:"suit"` +} + +type Faces struct { + Keys []int `json:"keys"` + Values []int `json:"values"` + Score int64 `json:"score"` + Type consts.FacesType `json:"type"` + Main int `json:"main"` + Extra int `json:"extra"` + HasOaa bool `json:"hasOaa"` +} + +func (f Faces) String() string { + return fmt.Sprintf("keys: %v, main %v, extra %v, type %v", f.Keys, f.Main, f.Extra, f.Type) +} + +func (f *Faces) SetValues(values []int) *Faces { + f.Values = values + return f +} + +func (f *Faces) SetKeys(keys []int) *Faces { + f.Keys = keys + return f +} + +func (f *Faces) SetMain(main int) *Faces { + f.Main = main + return f +} + +func (f *Faces) SetExtra(extra int) *Faces { + f.Extra = extra + return f +} + +func (f *Faces) SetScore(score int64) *Faces { + f.Score = score + return f +} + +func (f *Faces) SetType(t consts.FacesType) *Faces { + f.Type = t + return f +} + +func (f *Faces) Hash() string { + buf := bytes.Buffer{} + for _, k := range f.Keys { + buf.WriteString(strconv.Itoa(k)) + } + return buf.String() +} + +func (f Faces) Compare(lastFaces Faces) bool { + if f.Type == consts.FacesBomb { + return f.Score > lastFaces.Score + } + if f.Type != lastFaces.Type { + return false + } + return f.Score > lastFaces.Score && f.Main == lastFaces.Main && f.Extra == lastFaces.Extra +} + +func (pokers Pokers) Shuffle(n int, k int) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + for i := n - 1; i > 0; i -= k { + j := int(r.Int31n(int32(i + 1))) + pokers.Swap(i, j) + } +} + +func (pokers Pokers) Swap(i, j int) { + pokers[i], pokers[j] = pokers[j], pokers[i] +} + +func (pokers Pokers) SortByKey() { + sort.Slice(pokers, func(i, j int) bool { + return pokers[i].Key < pokers[j].Key + }) +} + +func (pokers Pokers) SortByValue() { + sort.Slice(pokers, func(i, j int) bool { + return pokers[i].Val < pokers[j].Val + }) +} + +func (pokers Pokers) SetOaa(oaa ...int) { + for i := range pokers { + if arrays.Contains(oaa, pokers[i].Key) { + pokers[i].Oaa = true + } + } +} + +func (pokers Pokers) SortByOaaValue() { + sort.Slice(pokers, func(i, j int) bool { + if pokers[i].Oaa == pokers[j].Oaa { + return pokers[i].Val < pokers[j].Val + } else if pokers[i].Oaa { + return false + } else { + return true + } + }) +} + +func (pokers Pokers) String() string { + return pokers.OaaString() +} + +func (pokers Pokers) OaaString() string { + buf := bytes.Buffer{} + for i := len(pokers) - 1; i >= 0; i-- { + poker := pokers[i] + flag := "" + if poker.Oaa { + flag = "*" + } + buf.WriteString(fmt.Sprintf("%s%v", flag, poker.Desc)) + if i != 0 { + buf.WriteString(" ") + } + } + return buf.String() +} + +func (pokers Pokers) TexasString() string { + buf := bytes.Buffer{} + for i := 0; i < len(pokers); i++ { + poker := pokers[i] + buf.WriteString(fmt.Sprintf("%s%v ", poker.Suit, poker.Desc)) + } + if buf.Len() > 0 { + buf.Truncate(buf.Len() - 1) + } + return buf.String() +} diff --git a/core/model/poker_test.go b/core/model/poker_test.go new file mode 100644 index 0000000..1ff7c0f --- /dev/null +++ b/core/model/poker_test.go @@ -0,0 +1,20 @@ +package model + +import ( + "fmt" + "testing" +) + +func TestPokers_Shuffle(t *testing.T) { + pokers := make(Pokers, 0) + for k := 1; k <= 54; k++ { + pokers = append(pokers, Poker{ + Key: k, + Val: 0, + }) + } + //pokers.Shuffle(len(pokers)) + for _, v := range pokers { + fmt.Print(v.Key, " ") + } +} diff --git a/core/model/texas.go b/core/model/texas.go new file mode 100644 index 0000000..ddcd501 --- /dev/null +++ b/core/model/texas.go @@ -0,0 +1,48 @@ +package model + +type TexasFacesType int + +const ( + _ TexasFacesType = iota + TexasFacesTypeHigh // 高牌 + TexasFacesTypeOnePair // 一对 + TexasFacesTypeTwoPairs // 两对 + TexasFacesTypeThreeOfAKind // 三条 + TexasFacesTypeStraight // 顺子 + TexasFacesTypeFlush // 同花 + TexasFacesTypeFullHouse // 葫芦 + TexasFacesTypeFourOfAKind // 四条 + TexasFacesTypeStraightFlush // 同花顺 + TexasFacesTypeRoyalFlush // 皇家同花顺 +) + +func (t TexasFacesType) String() string { + switch t { + case TexasFacesTypeHigh: + return "高牌(High)" + case TexasFacesTypeOnePair: + return "一对(One Pair)" + case TexasFacesTypeTwoPairs: + return "两对(Two Pairs)" + case TexasFacesTypeThreeOfAKind: + return "三条(Three of a Kind)" + case TexasFacesTypeStraight: + return "顺子(Straight)" + case TexasFacesTypeFlush: + return "同花(Flush)" + case TexasFacesTypeFullHouse: + return "葫芦(Full House)" + case TexasFacesTypeFourOfAKind: + return "四条(Four of a Kind)" + case TexasFacesTypeStraightFlush: + return "同花顺(Straight Flush)" + case TexasFacesTypeRoyalFlush: + return "皇家同花顺(Royal Flush)" + } + return "Unknown" +} + +type TexasFaces struct { + Score int64 `json:"score"` + Type TexasFacesType `json:"type"` +} diff --git a/core/network/conn.go b/core/network/conn.go new file mode 100644 index 0000000..ec88bbe --- /dev/null +++ b/core/network/conn.go @@ -0,0 +1,56 @@ +package network + +import ( + "github.com/ratel-online/core/protocol" + "sync/atomic" +) + +var connId int64 + +type Conn struct { + id int64 + state int + conn protocol.ReadWriteCloser +} + +func Wrapper(conn protocol.ReadWriteCloser) *Conn { + return &Conn{ + id: atomic.AddInt64(&connId, 1), + conn: conn, + } +} + +func (c *Conn) ID() int64 { + return c.id +} + +func (c *Conn) IP() string { + return c.conn.IP() +} + +func (c *Conn) Close() error { + c.state = 1 + return c.conn.Close() +} + +func (c *Conn) State() int { + return c.state +} + +func (c *Conn) Accept(apply func(msg protocol.Packet, c *Conn)) error { + for { + packet, err := c.conn.Read() + if err != nil { + return err + } + apply(*packet, c) + } +} + +func (c *Conn) Write(packet protocol.Packet) error { + return c.conn.Write(packet) +} + +func (c *Conn) Read() (*protocol.Packet, error) { + return c.conn.Read() +} diff --git a/core/pkg/holdem/cards.go b/core/pkg/holdem/cards.go new file mode 100644 index 0000000..0039f88 --- /dev/null +++ b/core/pkg/holdem/cards.go @@ -0,0 +1,119 @@ +package holdem + +import "log" + +type Card uint8 + +const AceSpades = Card(0xE1) +const TwoSpades = Card(0x21) +const ThreeSpades = Card(0x31) +const FourSpades = Card(0x41) +const FiveSpades = Card(0x51) +const SixSpades = Card(0x61) +const SevenSpades = Card(0x71) +const EightSpades = Card(0x81) +const NineSpades = Card(0x91) +const TenSpades = Card(0xA1) +const JackSpades = Card(0xB1) +const QueenSpades = Card(0xC1) +const KingSpades = Card(0xD1) + +const AceClubs = Card(0xE2) +const TwoClubs = Card(0x22) +const ThreeClubs = Card(0x32) +const FourClubs = Card(0x42) +const FiveClubs = Card(0x52) +const SixClubs = Card(0x62) +const SevenClubs = Card(0x72) +const EightClubs = Card(0x82) +const NineClubs = Card(0x92) +const TenClubs = Card(0xA2) +const JackClubs = Card(0xB2) +const QueenClubs = Card(0xC2) +const KingClubs = Card(0xD2) + +const AceHearts = Card(0xE3) +const TwoHearts = Card(0x23) +const ThreeHearts = Card(0x33) +const FourHearts = Card(0x43) +const FiveHearts = Card(0x53) +const SixHearts = Card(0x63) +const SevenHearts = Card(0x73) +const EightHearts = Card(0x83) +const NineHearts = Card(0x93) +const TenHearts = Card(0xA3) +const JackHearts = Card(0xB3) +const QueenHearts = Card(0xC3) +const KingHearts = Card(0xD3) + +const AceDiamonds = Card(0xE4) +const TwoDiamonds = Card(0x24) +const ThreeDiamonds = Card(0x34) +const FourDiamonds = Card(0x44) +const FiveDiamonds = Card(0x54) +const SixDiamonds = Card(0x64) +const SevenDiamonds = Card(0x74) +const EightDiamonds = Card(0x84) +const NineDiamonds = Card(0x94) +const TenDiamonds = Card(0xA4) +const JackDiamonds = Card(0xB4) +const QueenDiamonds = Card(0xC4) +const KingDiamonds = Card(0xD4) + +var Deck = [52]Card{ + AceSpades, TwoSpades, ThreeSpades, FourSpades, FiveSpades, SixSpades, SevenSpades, EightSpades, NineSpades, TenSpades, JackSpades, QueenSpades, KingSpades, + AceClubs, TwoClubs, ThreeClubs, FourClubs, FiveClubs, SixClubs, SevenClubs, EightClubs, NineClubs, TenClubs, JackClubs, QueenClubs, KingClubs, + AceHearts, TwoHearts, ThreeHearts, FourHearts, FiveHearts, SixHearts, SevenHearts, EightHearts, NineHearts, TenHearts, JackHearts, QueenHearts, KingHearts, + AceDiamonds, TwoDiamonds, ThreeDiamonds, FourDiamonds, FiveDiamonds, SixDiamonds, SevenDiamonds, EightDiamonds, NineDiamonds, TenDiamonds, JackDiamonds, QueenDiamonds, KingDiamonds, +} + +func (card Card) String() string { + var cardNumber, cardSuite string + num := card >> 4 + switch num { + case 0x0E: + cardNumber = "A" + case 0x02: + cardNumber = "2" + case 0x03: + cardNumber = "3" + case 0x04: + cardNumber = "4" + case 0x05: + cardNumber = "5" + case 0x06: + cardNumber = "6" + case 0x07: + cardNumber = "7" + case 0x08: + cardNumber = "8" + case 0x09: + cardNumber = "9" + case 0x0A: + cardNumber = "10" + case 0x0B: + cardNumber = "J" + case 0x0C: + cardNumber = "Q" + case 0x0D: + cardNumber = "K" + default: + log.Panicf("Invalid Card: %02x Number: %02x\n", uint8(card), cardNumber) + } + + suit := card & 0x0F + switch suit { + case 0x01: + cardSuite = "s" + case 0x02: + cardSuite = "c" + case 0x03: + cardSuite = "h" + case 0x04: + cardSuite = "d" + default: + log.Panicf("Invalid Card: %02x Suit: %02x\n", uint8(card), cardSuite) + } + + return cardNumber + cardSuite +} diff --git a/core/pkg/holdem/holdem.go b/core/pkg/holdem/holdem.go new file mode 100644 index 0000000..8694a1c --- /dev/null +++ b/core/pkg/holdem/holdem.go @@ -0,0 +1,241 @@ +package holdem + +import ( + "fmt" + "math/rand" + "sort" + "strings" + "time" +) + +func init() { + rand.Seed(time.Now().UTC().UnixNano()) +} + +type HandType uint8 +type HandValue int + +const HighCard = HandType(0x00) +const OnePair = HandType(0x01) +const TwoPair = HandType(0x02) +const Trips = HandType(0x03) +const Straight = HandType(0x04) +const Flush = HandType(0x05) +const FullHouse = HandType(0x06) +const Quads = HandType(0x07) +const StraightFlush = HandType(0x08) +const RoyalFlush = HandType(0x09) + +type Hand struct { + Cards [2]Card + Score HandValue + HighHand [5]Card +} + +type Round struct { + Hands []Hand + Flop [3]Card + Turn Card + River Card +} + +func (round *Round) String() string { + var buffer []string = make([]string, len(round.Hands)+3) + buffer[0] = fmt.Sprintf("Draw: %s %s %s", round.Flop[0], round.Flop[1], round.Flop[2]) + buffer[1] = fmt.Sprintf("Turn: %s", round.Turn) + buffer[2] = fmt.Sprintf("River: %s", round.River) + for i, hand := range round.Hands { + buffer[i+3] = fmt.Sprintf("Player %2d: %3s %3s (%06x using %3s %3s %3s %3s %3s)", i+1, + hand.Cards[0], hand.Cards[1], hand.Score, hand.HighHand[0], hand.HighHand[1], hand.HighHand[2], hand.HighHand[3], hand.HighHand[4]) + } + return strings.Join(buffer, "\n") +} + +func NewRound(playerCount int8) (*Round, error) { + if playerCount > 22 { + return nil, fmt.Errorf("Too many players in a round: %d", playerCount) + } + if playerCount < 2 { + return nil, fmt.Errorf("Too few players in a round: %d", playerCount) + } + shuffle := rand.Perm(len(Deck)) + round := &Round{ + Hands: make([]Hand, playerCount), + Flop: [3]Card{Deck[shuffle[0]], Deck[shuffle[1]], Deck[shuffle[2]]}, + Turn: Deck[shuffle[3]], + River: Deck[shuffle[4]], + } + for i := int8(0); i < playerCount; i++ { + hand := [2]Card{Deck[shuffle[5+(i*2)]], Deck[shuffle[5+(i*2)+1]]} + round.Hands[i].Cards = hand + round.Hands[i].Score, round.Hands[i].HighHand = HighestHandValue([7]Card{ + hand[0], hand[1], + round.Flop[0], round.Flop[1], round.Flop[2], + round.Turn, round.River, + }) + } + return round, nil +} + +func handValue(handType HandType, sortedValues [5]int) HandValue { + return HandValue(0) | + HandValue(handType)<<20 | + HandValue(sortedValues[4])<<16 | + HandValue(sortedValues[3])<<12 | + HandValue(sortedValues[2])<<8 | + HandValue(sortedValues[1])<<4 | + HandValue(sortedValues[0]) +} + +func CalculateHandValue(hand [5]Card) HandValue { + suit1 := hand[0] & 0x0F + suit2 := hand[1] & 0x0F + suit3 := hand[2] & 0x0F + suit4 := hand[3] & 0x0F + suit5 := hand[4] & 0x0F + flush := bool(suit1 == suit2 && suit1 == suit3 && suit1 == suit4 && suit1 == suit5) + + cardValues := [5]int{ + int(hand[0] >> 4), + int(hand[1] >> 4), + int(hand[2] >> 4), + int(hand[3] >> 4), + int(hand[4] >> 4), + } + + cardValuesSlice := sort.IntSlice(cardValues[:]) + cardValuesSlice.Sort() + + if cardValues[1] == cardValues[0]+1 && cardValues[2] == cardValues[1]+1 && cardValues[3] == cardValues[2]+1 { + straight := false + if cardValues[4] == cardValues[3]+1 { + straight = true + } else if cardValues[4] == 0x0E && cardValues[0] == 0x02 { //Low wheel + cardValues = [5]int{0x01, 0x02, 0x03, 0x04, 0x05} + straight = true + } + if straight && flush { + if cardValues[4] == 0x0E { + return handValue(RoyalFlush, cardValues) + } + return handValue(StraightFlush, cardValues) + } else if straight { + return handValue(Straight, cardValues) + } + } + + if flush { + return handValue(Flush, cardValues) + } + + // Check for Quads + if cardValues[1] == cardValues[2] && cardValues[1] == cardValues[3] { + + if cardValues[0] == cardValues[1] { + return handValue(Quads, cardValues) + cardValuesSlice.Swap(0, 4) + } else if cardValues[3] == cardValues[4] { + return handValue(Quads, cardValues) + } else { + cardValuesSlice.Swap(1, 4) + return handValue(Trips, cardValues) + } + + } + + // Check for Full House + if cardValues[0] == cardValues[1] && cardValues[3] == cardValues[4] { + if cardValues[1] == cardValues[2] { + cardValuesSlice.Swap(0, 4) + cardValuesSlice.Swap(1, 3) + return handValue(FullHouse, cardValues) + } else if cardValues[2] == cardValues[3] { + return handValue(FullHouse, cardValues) + } else { + // Swaping the middle card for the last card. Moves the two pairs in front for ranking. + cardValuesSlice.Swap(2, 0) + return handValue(TwoPair, cardValues) + } + } + + // Check for Trips (middle trip already covered in Quad check) + if cardValues[0] == cardValues[1] && cardValues[1] == cardValues[2] { + cardValuesSlice.Swap(0, 3) + cardValuesSlice.Swap(1, 4) + return handValue(Trips, cardValues) + } else if cardValues[2] == cardValues[3] && cardValues[3] == cardValues[4] { + return handValue(Trips, cardValues) + } + + // Check for Two Pair (0+1 + 3+4 already covered by full house check) + if cardValues[0] == cardValues[1] && cardValues[2] == cardValues[3] { + cardValuesSlice.Swap(2, 4) + cardValuesSlice.Swap(2, 0) + return handValue(TwoPair, cardValues) + } else if cardValues[1] == cardValues[2] && cardValues[3] == cardValues[4] { + return handValue(TwoPair, cardValues) + } + + // Check for Pair + if cardValues[0] == cardValues[1] { + cardValuesSlice.Swap(2, 4) + cardValuesSlice.Swap(0, 4) + cardValuesSlice.Swap(1, 3) + return handValue(OnePair, cardValues) + } else if cardValues[1] == cardValues[2] { + cardValuesSlice.Swap(2, 4) + cardValuesSlice.Swap(1, 3) + return handValue(OnePair, cardValues) + } else if cardValues[2] == cardValues[3] { + cardValuesSlice.Swap(2, 4) + return handValue(OnePair, cardValues) + } else if cardValues[3] == cardValues[4] { + return handValue(OnePair, cardValues) + } + + return handValue(HighCard, cardValues) +} + +var HandCombinations = [21][5]int8{ + [5]int8{1, 2, 3, 4, 5}, + [5]int8{1, 2, 3, 4, 6}, + [5]int8{1, 2, 3, 4, 7}, + [5]int8{1, 2, 3, 5, 6}, + [5]int8{1, 2, 3, 5, 7}, + [5]int8{1, 2, 3, 6, 7}, + [5]int8{1, 2, 4, 5, 6}, + [5]int8{1, 2, 4, 5, 7}, + [5]int8{1, 2, 4, 6, 7}, + [5]int8{1, 2, 5, 6, 7}, + [5]int8{1, 3, 4, 5, 6}, + [5]int8{1, 3, 4, 5, 7}, + [5]int8{1, 3, 4, 6, 7}, + [5]int8{1, 3, 5, 6, 7}, + [5]int8{1, 4, 5, 6, 7}, + [5]int8{2, 3, 4, 5, 6}, + [5]int8{2, 3, 4, 5, 7}, + [5]int8{2, 3, 4, 6, 7}, + [5]int8{2, 3, 5, 6, 7}, + [5]int8{2, 4, 5, 6, 7}, + [5]int8{3, 4, 5, 6, 7}, +} + +func HighestHandValue(cards [7]Card) (HandValue, [5]Card) { + highest := HandValue(0) + var highHand [5]Card + for _, combination := range HandCombinations { + hand := [5]Card{ + cards[combination[0]-1], + cards[combination[1]-1], + cards[combination[2]-1], + cards[combination[3]-1], + cards[combination[4]-1], + } + value := CalculateHandValue(hand) + if value > highest { + highest = value + highHand = hand + } + } + return highest, highHand +} diff --git a/core/protocol/protocol.go b/core/protocol/protocol.go new file mode 100644 index 0000000..b58c22d --- /dev/null +++ b/core/protocol/protocol.go @@ -0,0 +1,105 @@ +package protocol + +import ( + "encoding/binary" + "errors" + "github.com/ratel-online/core/consts" + "github.com/ratel-online/core/util/json" + "io" + "strconv" +) + +var ( + lenSize = 4 +) + +type Packet struct { + Body []byte `json:"data"` +} + +func (p Packet) Int() (int, error) { + v, err := strconv.ParseInt(p.String(), 10, 64) + return int(v), err +} + +func (p Packet) Int64() (int64, error) { + v, _ := strconv.ParseInt(p.String(), 10, 64) + return v, nil +} + +func (p Packet) String() string { + return string(p.Body) +} + +func (p Packet) Unmarshal(v interface{}) error { + return json.Unmarshal(p.Body, v) +} + +func StringPacket(msg string) Packet { + return Packet{ + Body: []byte(msg), + } +} + +func ErrorPacket(err error) Packet { + return Packet{ + Body: []byte(err.Error()), + } +} + +func ObjectPacket(obj interface{}) Packet { + return Packet{ + Body: json.Marshal(obj), + } +} + +type ReadWriteCloser interface { + Read() (*Packet, error) + Write(msg Packet) error + Close() error + IP() string +} + +func readUint32(reader io.Reader) (uint32, error) { + data := make([]byte, 4) + _, err := io.ReadFull(reader, data) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint32(data), nil +} + +func readUint64(reader io.Reader) (uint64, error) { + data := make([]byte, 8) + _, err := io.ReadFull(reader, data) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint64(data), nil +} + +func encode(msg Packet) []byte { + lenBytes := make([]byte, lenSize) + binary.BigEndian.PutUint32(lenBytes, uint32(len(msg.Body))) + data := make([]byte, 0) + data = append(data, lenBytes...) + return append(data, msg.Body...) +} + +func decode(r io.Reader) (*Packet, error) { + l, err := readUint32(r) + if err != nil { + return nil, err + } + if l > consts.MaxPacketSize { + return nil, errors.New("Overflow max packet size " + strconv.Itoa(consts.MaxPacketSize)) + } + dataBytes := make([]byte, l) + _, err = io.ReadFull(r, dataBytes) + if err != nil { + return nil, err + } + return &Packet{ + Body: dataBytes, + }, nil +} diff --git a/core/protocol/tcp.go b/core/protocol/tcp.go new file mode 100644 index 0000000..a302148 --- /dev/null +++ b/core/protocol/tcp.go @@ -0,0 +1,30 @@ +package protocol + +import ( + "net" +) + +type TcpReadWriteCloser struct { + conn net.Conn +} + +func NewTcpReadWriteCloser(conn net.Conn) TcpReadWriteCloser { + return TcpReadWriteCloser{conn: conn} +} + +func (t TcpReadWriteCloser) Read() (*Packet, error) { + return decode(t.conn) +} + +func (t TcpReadWriteCloser) Write(msg Packet) error { + _, err := t.conn.Write(encode(msg)) + return err +} + +func (t TcpReadWriteCloser) Close() error { + return t.conn.Close() +} + +func (t TcpReadWriteCloser) IP() string { + return t.conn.RemoteAddr().String() +} diff --git a/core/protocol/websocket.go b/core/protocol/websocket.go new file mode 100644 index 0000000..8a9b29c --- /dev/null +++ b/core/protocol/websocket.go @@ -0,0 +1,36 @@ +package protocol + +import ( + "github.com/awesome-cap/im/core/util/json" + "github.com/gorilla/websocket" +) + +type WebsocketReadWriteCloser struct { + conn *websocket.Conn +} + +func NewWebsocketReadWriteCloser(conn *websocket.Conn) WebsocketReadWriteCloser { + return WebsocketReadWriteCloser{conn: conn} +} + +func (w WebsocketReadWriteCloser) Read() (*Packet, error) { + _, b, err := w.conn.ReadMessage() + if err != nil { + return nil, err + } + msg := &Packet{} + json.Unmarshal(b, msg) + return msg, nil +} + +func (w WebsocketReadWriteCloser) Write(msg Packet) error { + return w.conn.WriteMessage(websocket.BinaryMessage, json.Marshal(msg)) +} + +func (w WebsocketReadWriteCloser) Close() error { + return w.conn.Close() +} + +func (w WebsocketReadWriteCloser) IP() string { + return w.conn.RemoteAddr().String() +} diff --git a/core/util/arrays/arrays.go b/core/util/arrays/arrays.go new file mode 100644 index 0000000..3726473 --- /dev/null +++ b/core/util/arrays/arrays.go @@ -0,0 +1,26 @@ +package arrays + +func AppendN(arr []int, num, n int) []int { + for i := 0; i < n; i++ { + arr = append(arr, num) + } + return arr +} + +func Contains(arr []int, target int) bool { + for _, v := range arr { + if v == target { + return true + } + } + return false +} + +func IndexOf(arr []int64, v int64) int { + for i, a := range arr { + if a == v { + return i + } + } + return -1 +} diff --git a/core/util/async/async.go b/core/util/async/async.go new file mode 100644 index 0000000..9dabc28 --- /dev/null +++ b/core/util/async/async.go @@ -0,0 +1,31 @@ +package async + +import ( + "bytes" + "fmt" + "runtime" +) + +func Async(fun func()) { + go func() { + defer func() { + if err := recover(); err != nil { + PrintStackTrace(err) + } + }() + fun() + }() +} + +func PrintStackTrace(err interface{}) { + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("%v\n", err)) + for i := 1; ; i++ { + pc, file, line, ok := runtime.Caller(i) + if !ok { + break + } + buf.WriteString(fmt.Sprintf("%s:%d (0x%x)\n", file, line, pc)) + } + fmt.Println(buf.String()) +} diff --git a/core/util/bytes/bytes.go b/core/util/bytes/bytes.go new file mode 100644 index 0000000..6c01e02 --- /dev/null +++ b/core/util/bytes/bytes.go @@ -0,0 +1,2 @@ +package bytes + diff --git a/core/util/http/http.go b/core/util/http/http.go new file mode 100644 index 0000000..c131983 --- /dev/null +++ b/core/util/http/http.go @@ -0,0 +1,72 @@ +package http + +import ( + "bytes" + "crypto/tls" + "fmt" + "github.com/ratel-online/core/util/json" + "github.com/ratel-online/core/util/strings" + "io/ioutil" + "net/http" + "net/url" + "time" +) + +var httpClient = &http.Client{} +var emtpy = struct{}{} + +func init() { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + httpClient = &http.Client{ + Transport: tr, + Timeout: time.Duration(3) * time.Second, + } +} + +func Get(url string, params map[string]interface{}) ([]byte, error) { + resp, err := httpClient.Get(url + encodeQueries(params)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return data, nil +} + +func Post(url string, params map[string]interface{}, body interface{}) ([]byte, error) { + if body == nil { + body = emtpy + } + resp, err := httpClient.Post(url+encodeQueries(params), "application/json", bytes.NewReader(json.Marshal(body))) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return data, nil +} + +func encodeQueries(params map[string]interface{}) string { + if len(params) == 0 { + return "" + } + var buffer bytes.Buffer + buffer.WriteString("?") + for k, v := range params { + if v == nil { + buffer.WriteString(fmt.Sprintf("%s=", url.QueryEscape(k))) + continue + } + buffer.WriteString(fmt.Sprintf("%s=%v&", url.QueryEscape(k), url.QueryEscape(strings.String(v)))) + } + buffer.Truncate(buffer.Len() - 1) + return buffer.String() +} diff --git a/core/util/json/json.go b/core/util/json/json.go new file mode 100644 index 0000000..8511293 --- /dev/null +++ b/core/util/json/json.go @@ -0,0 +1,26 @@ +package json + +import ( + "github.com/json-iterator/go" + "unsafe" +) + +func init() { + jsoniter.RegisterTypeEncoderFunc("[]uint8", func(ptr unsafe.Pointer, stream *jsoniter.Stream) { + t := *((*[]byte)(ptr)) + stream.WriteString(string(t)) + }, nil) + jsoniter.RegisterTypeDecoderFunc("[]uint8", func(ptr unsafe.Pointer, iter *jsoniter.Iterator) { + str := iter.ReadString() + *((*[]byte)(ptr)) = []byte(str) + }) +} + +func Marshal(v interface{}) []byte { + data, _ := jsoniter.Marshal(v) + return data +} + +func Unmarshal(data []byte, v interface{}) error { + return jsoniter.Unmarshal(data, v) +} diff --git a/core/util/math/math.go b/core/util/math/math.go new file mode 100644 index 0000000..cc5df21 --- /dev/null +++ b/core/util/math/math.go @@ -0,0 +1,15 @@ +package math + +import "math" + +func Pow(x, y int64) int64 { + return int64(math.Pow(float64(x), float64(y))) +} + +func Sum(x ...int) int { + s := 0 + for _, v := range x { + s += v + } + return s +} diff --git a/core/util/poker/consts.go b/core/util/poker/consts.go new file mode 100644 index 0000000..b4dccf4 --- /dev/null +++ b/core/util/poker/consts.go @@ -0,0 +1,133 @@ +package poker + +import ( + "github.com/ratel-online/core/model" + "github.com/ratel-online/core/util/strings" + "math/rand" + "time" +) + +var ( + base = make(model.Pokers, 0) + desc = map[int]string{} + keysAlias = map[int][]string{} + aliasKeys = map[string]int{} +) + +func init() { + for k := 1; k <= 15; k++ { + switch k { + case 1: + desc[k] = "A" + keysAlias[k] = []string{"1", "a", "A"} + case 10: + desc[k] = "10" + keysAlias[k] = []string{"0", "t", "T"} + case 11: + desc[k] = "J" + keysAlias[k] = []string{"j", "J"} + case 12: + desc[k] = "Q" + keysAlias[k] = []string{"q", "Q"} + case 13: + desc[k] = "K" + keysAlias[k] = []string{"k", "K"} + case 14: + desc[k] = "S" + keysAlias[k] = []string{"s", "S"} + case 15: + desc[k] = "X" + keysAlias[k] = []string{"x", "X"} + default: + desc[k] = strings.String(k) + keysAlias[k] = []string{strings.String(k)} + } + } + for k, aliases := range keysAlias { + for _, alias := range aliases { + aliasKeys[alias] = k + } + } + for k := 1; k <= 13; k++ { + for t := 0; t < 4; t++ { + base = append(base, model.Poker{Key: k, Val: 0, Desc: desc[k], Suit: model.PokerSuit(t)}) + } + } + for k := 14; k <= 15; k++ { + base = append(base, model.Poker{Key: k, Val: 0, Desc: desc[k]}) + } +} + +func GetKey(alias string) int { + return aliasKeys[alias] +} + +func GetAlias(key int) string { + if len(keysAlias[key]) > 0 { + return keysAlias[key][0] + } + return "" +} + +func GetDesc(key int) string { + return desc[key] +} + +func GetDontShuffleBase() model.Pokers { + pokers := model.Pokers{} + keys := make([]int, 0) + for k := 1; k <= 15; k++ { + keys = append(keys, k) + } + r := rand.New(rand.NewSource(time.Now().UnixNano())) + r.Shuffle(len(keys), func(i, j int) { + keys[i], keys[j] = keys[j], keys[i] + }) + for _, k := range keys { + if k <= 13 { + for t := 1; t <= 4; t++ { + pokers = append(pokers, model.Poker{Key: k, Val: 0, Desc: desc[k]}) + } + } else { + pokers = append(pokers, model.Poker{Key: k, Val: 0, Desc: desc[k]}) + } + } + return pokers +} + +func GetRunFastDontShuffleBase() model.Pokers { + pokers := model.Pokers{} + keys := make([]int, 0) + for k := 1; k <= 13; k++ { + keys = append(keys, k) + } + r := rand.New(rand.NewSource(time.Now().UnixNano())) + r.Shuffle(len(keys), func(i, j int) { + keys[i], keys[j] = keys[j], keys[i] + }) + for _, k := range keys { + if k == 1 { + for t := 1; t <= 3; t++ { + pokers = append(pokers, model.Poker{Key: k, Val: 0, Desc: desc[k]}) + } + } else if k == 2 { + pokers = append(pokers, model.Poker{Key: k, Val: 0, Desc: desc[k]}) + } else if k <= 13 { + for t := 1; t <= 4; t++ { + pokers = append(pokers, model.Poker{Key: k, Val: 0, Desc: desc[k]}) + } + } + } + return pokers +} + +func GetTexasBase() model.Pokers { + pokers := model.Pokers{} + for _, poker := range base { + if poker.Key > 13 { + continue + } + pokers = append(pokers, model.Poker{Key: poker.Key, Val: poker.Val, Desc: poker.Desc, Suit: poker.Suit}) + } + return pokers +} diff --git a/core/util/poker/consts_test.go b/core/util/poker/consts_test.go new file mode 100644 index 0000000..c2d94ab --- /dev/null +++ b/core/util/poker/consts_test.go @@ -0,0 +1,11 @@ +package poker + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetTexasBase(t *testing.T) { + b := GetTexasBase() + assert.Equal(t, 52, len(b)) +} diff --git a/core/util/poker/poker.go b/core/util/poker/poker.go new file mode 100644 index 0000000..e064ba2 --- /dev/null +++ b/core/util/poker/poker.go @@ -0,0 +1,380 @@ +package poker + +import ( + "github.com/ratel-online/core/consts" + "github.com/ratel-online/core/model" + "github.com/ratel-online/core/util/arrays" + "math/rand" + "sort" + "time" +) + +type Rules interface { + Value(key int) int + IsStraight(faces []int, count int) bool + StraightBoundary() (int, int) + Reserved() bool +} + +func Sets(number int) int { + sets := number / 3 + if number%3 > 0 { + sets++ + } + return sets +} + +// Distribute number is players number. n is shuffle times. +func Distribute(number int, dontShuffle bool, rules Rules) ([]model.Pokers, int) { + sets := Sets(number) + pokers := make(model.Pokers, 0) + _base := base + if dontShuffle { + _base = GetDontShuffleBase() + } + for i := 0; i < sets; i++ { + pokers = append(pokers, _base...) + } + for i := range pokers { + pokers[i].Val = rules.Value(pokers[i].Key) + } + size := len(pokers) + if dontShuffle { + pokers.Shuffle(size, 4) + } else { + pokers.Shuffle(size, 1) + } + reserve := 0 + if rules.Reserved() { + if size%number == 0 { + reserve = number * sets + } else { + reserve = number + size%number + } + } else { + reserve = size % number + } + avgNum := (size - reserve) / number + pokersArr := make([]model.Pokers, 0) + for i := 0; i < number; i++ { + pokerArr := make([]model.Poker, 0) + pokersArr = append(pokersArr, append(pokerArr, pokers[i*avgNum:(i+1)*avgNum]...)) + } + if reserve > 0 { + pokerArr := make([]model.Poker, 0) + pokersArr = append(pokersArr, append(pokerArr, pokers[size-reserve:]...)) + } + for i := range pokersArr { + pokersArr[i].SortByValue() + } + return pokersArr, sets +} + +func SetValue(pokers model.Pokers, kv map[int]int) { + for i := range pokers { + pokers[i].Val = kv[pokers[i].Key] + } +} + +func ParseFaces(pokers model.Pokers, rules Rules) []model.Faces { + hasOaa := false + for _, currPoker := range pokers { + if currPoker.Oaa { + hasOaa = true + break + } + } + list := parseFaces(pokers, rules) + if len(list) == 0 { + return list + } + mapping := map[int]int{} + for i := 1; i <= 15; i++ { + mapping[rules.Value(i)] = i + } + for i, faces := range list { + keys := make([]int, 0) + for _, v := range faces.Values { + keys = append(keys, mapping[v]) + } + list[i].Keys = keys + list[i].HasOaa = hasOaa + if hasOaa && faces.Type == consts.FacesBomb { + //賴子炸分數減300 + list[i].Score -= 300 + } + } + return list +} + +func parseFaces(pokers model.Pokers, rules Rules) []model.Faces { + if len(pokers) == 0 { + return nil + } + sc, xc, score := 0, 0, int64(0) + stats := map[int]int{} + group := map[int][]int{} + counts := make([]int, 0) + values := make([]int, 0) + //單排記分 + for _, poker := range pokers { + if poker.Key < 0 || poker.Key > 15 { + return nil + } + poker.Val = rules.Value(poker.Key) + score += int64(poker.Val) + values = append(values, poker.Val) + stats[poker.Val]++ + if poker.Key == 14 { + sc++ + } else if poker.Key == 15 { + xc++ + } + } + for v, c := range stats { + group[c] = append(group[c], v) + } + for c := range group { + counts = append(counts, c) + sort.Ints(group[c]) + } + sort.Ints(counts) + for i := 0; i < len(counts)/2; i++ { + counts[i], counts[len(counts)-i-1] = counts[len(counts)-i-1], counts[i] + } + list := make([]model.Faces, 0) + if sc+xc == len(pokers) && sc+xc > 1 { + list = append(list, model.Faces{Values: values, Score: int64(sc*14+xc*15)*2 + int64(len(pokers)*2*1000), Type: consts.FacesBomb}) + } else if counts[0] == 1 { + if len(group[counts[0]]) == 1 { + list = append(list, model.Faces{Values: values, Score: score, Type: consts.FacesSingle}) + } else if rules.IsStraight(group[counts[0]], counts[0]) { + list = append(list, model.Faces{Values: values, Score: score, Main: len(group[counts[0]]), Type: consts.FacesStraight}) + } + } else if counts[0] == 2 && len(counts) == 1 { + if len(group[counts[0]]) == 1 { + list = append(list, model.Faces{Values: values, Score: score, Type: consts.FacesDouble}) + } else if rules.IsStraight(group[counts[0]], counts[0]) { + list = append(list, model.Faces{Values: values, Score: score, Main: len(group[counts[0]]), Type: consts.FacesStraight}) + } + } else if counts[0] >= 3 { + if len(counts) == 1 && len(group[counts[0]]) == 1 { + if counts[0] == 3 { + list = append(list, model.Faces{Values: values, Score: score, Type: consts.FacesTriple}) + } else { + list = append(list, model.Faces{Values: values, Score: int64(group[counts[0]][0]*counts[0]) + int64(len(pokers)*1000), Type: consts.FacesBomb}) + } + } else if len(counts) == 1 && rules.IsStraight(group[counts[0]], counts[0]) { + if counts[0] == 3 { + list = append(list, model.Faces{Values: values, Score: score, Main: len(group[counts[0]]), Type: consts.FacesStraight}) + } else if counts[0] == 4 { + values = make([]int, 0) + for _, v := range group[counts[0]] { + values = arrays.AppendN(values, v, 3) + } + for _, v := range group[counts[0]] { + values = append(values, v) + } + list = append(list, model.Faces{Values: values, Score: score / 4 * 3, Main: len(group[counts[0]]), Extra: 1, Type: consts.FacesUnion3Straight}) + } else if counts[0] == 5 { + values = make([]int, 0) + for _, v := range group[counts[0]] { + values = arrays.AppendN(values, v, 3) + } + for _, v := range group[counts[0]] { + values = arrays.AppendN(values, v, 2) + } + list = append(list, model.Faces{Values: values, Score: score / 5 * 3, Main: len(group[counts[0]]), Extra: 1, Type: consts.FacesUnion3Straight}) + } + } else if len(group[3]) > 0 { + list = parseUnionOrStraight(group, rules) + } else if counts[0] == 4 && len(counts) == 2 && counts[1] <= 2 { + if len(group[counts[0]]) == 1 && ((counts[1] == 2 && len(group[counts[1]]) <= 2) || (counts[1] == 1 && len(group[counts[1]]) == 2)) { + extra := counts[1] + if extra == 2 && len(group[extra]) == 1 { + extra = 1 + } + list = append(list, model.Faces{Values: values, Score: int64(group[counts[0]][0] * counts[0]), Main: len(group[counts[0]]), Extra: extra, Type: consts.FacesUnion4}) + } + } + if counts[0] == 4 && len(counts) == 1 && len(group[counts[0]]) == 2 { + values = make([]int, 0) + values = arrays.AppendN(values, group[counts[0]][1], counts[0]) + values = arrays.AppendN(values, group[counts[0]][0], counts[0]) + list = append(list, model.Faces{Values: values, Score: int64(group[counts[0]][1] * counts[0]), Main: 1, Extra: 1, Type: consts.FacesUnion4}) + } + } + return list +} + +func parseUnionOrStraight(group map[int][]int, rules Rules) []model.Faces { + list := make([]model.Faces, 0) + extras := make([]int, 0) + mains := make([]int, 0) + for k, arr := range group { + for _, v := range arr { + if k > 3 { + extras = arrays.AppendN(extras, v, k-3) + mains = append(mains, v) + } else if k == 3 { + mains = append(mains, v) + } else if k < 3 { + extras = arrays.AppendN(extras, v, k) + } + } + } + sort.Ints(mains) + valid := map[int]int{} + sta, pre := mains[0], mains[0] + for i := 1; i < len(mains); i++ { + if mains[i] > pre+1 { + valid[sta] = pre + sta = mains[i] + } + pre = mains[i] + } + valid[sta] = mains[len(mains)-1] + + target := 0 + for k, v := range valid { + if target == 0 { + target = k + continue + } + if v-k > valid[target]-target || (v-k == valid[target]-target && k > target) { + target = k + } + } + + for _, v := range mains { + if v < target || v > valid[target] { + extras = arrays.AppendN(extras, v, 3) + } + } + + ml, mr := target, valid[target] + ll, lr := rules.StraightBoundary() + main := mr - ml + 1 + for extra := 1; extra <= 2; extra++ { + tmpExtras := make([]int, len(extras)) + copy(tmpExtras, extras) + access, vl, vr, tmpExtras := isValidUnionStraight(main, extra, tmpExtras, ml, mr, ll, lr) + if access { + values := make([]int, 0) + score := 0 + main = vr - vl + 1 + for i := vl; i <= vr; i++ { + values = arrays.AppendN(values, i, 3) + score += 3 * i + } + sort.Ints(tmpExtras) + for _, v := range tmpExtras { + values = append(values, v) + } + faces := model.Faces{ + Main: main, + Extra: extra, + Score: int64(score), + Values: values, + } + if main == 1 { + faces.SetType(consts.FacesUnion3) + } else { + faces.SetType(consts.FacesUnion3Straight) + } + list = append(list, faces) + } + } + return list +} + +func isValidUnionStraight(main, extra int, extras []int, ml, mr, ll, lr int) (bool, int, int, []int) { + access := false + if extra == 2 { + counts := map[int]int{} + for _, v := range extras { + counts[v]++ + } + single := map[int]bool{} + for k, v := range counts { + if v%2 == 1 { + single[k] = true + } + } + for len(single) > 0 { + if single[mr] { + extras = arrays.AppendN(extras, mr, 3) + delete(single, mr) + mr-- + } else if single[ml] { + extras = arrays.AppendN(extras, ml, 3) + delete(single, ml) + ml++ + } else { + return false, ml, mr, extras + } + } + if len(extras)%2 != 0 { + return false, ml, mr, extras + } + main = mr - ml + 1 + access = main == len(extras)/2 + } else { + for main > len(extras) && (main-1-len(extras))%3 == 0 { + if mr > lr { + extras = arrays.AppendN(extras, mr, 3) + mr-- + } else { + extras = arrays.AppendN(extras, ml, 3) + ml++ + } + main = mr - ml + 1 + } + access = main == len(extras) + } + if main > 1 { + access = access && ml >= ll && mr <= lr + } + return access, ml, mr, extras +} + +// Random _ random to get a poker, if exclude is not empty, will skip excluded keys. +func Random(exclude ...int) int { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + times := 0 + for { + times++ + k := r.Intn(15) + 1 + if !arrays.Contains(exclude, k) || times > 64 { + return k + } + } +} + +func RandomN(n int, exclude ...int) []int { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + keys := make([]int, 0) + times := 0 + for i := 0; i < n; i++ { + for { + times++ + k := r.Intn(15) + 1 + if !arrays.Contains(exclude, k) || times > 64 { + keys = append(keys, k) + break + } + } + } + return keys +} + +func GetPokers(keys ...int) model.Pokers { + pokers := make(model.Pokers, 0) + for _, k := range keys { + pokers = append(pokers, model.Poker{ + Key: k, + Desc: desc[k], + }) + } + return pokers +} diff --git a/core/util/poker/poker_test.go b/core/util/poker/poker_test.go new file mode 100644 index 0000000..27eedc2 --- /dev/null +++ b/core/util/poker/poker_test.go @@ -0,0 +1,227 @@ +package poker + +import ( + "fmt" + "github.com/ratel-online/core/consts" + "github.com/ratel-online/core/model" + "testing" +) + +var defaultRules = _defaultRules{} + +type _defaultRules struct { +} + +func (d _defaultRules) Value(key int) int { + if key == 1 { + return 12 + } else if key == 2 { + return 13 + } else if key > 13 { + return key + } + return key - 2 +} + +func (d _defaultRules) IsStraight(faces []int, count int) bool { + if faces[len(faces)-1]-faces[0] != len(faces)-1 { + return false + } + if faces[len(faces)-1] > 12 { + return false + } + if count == 1 { + return len(faces) >= 5 + } else if count == 2 { + return len(faces) >= 3 + } else if count > 2 { + return len(faces) >= 2 + } + return false +} + +func (d _defaultRules) StraightBoundary() (int, int) { + return 1, 12 +} + +func (d _defaultRules) Reserved() bool { + return true +} + +func getPokers(keys ...int) model.Pokers { + pokers := make(model.Pokers, 0) + for _, k := range keys { + pokers = append(pokers, model.Poker{ + Key: k, + Desc: desc[k], + }) + } + return pokers +} + +func TestDistribute(t *testing.T) { + //pokersArr, _ := Distribute(3, defaultRules) + //for _, pokers := range pokersArr { + // pokers.SortByKey() + // t.Log(pokers.String()) + //} + //pokersArr, _ = Distribute(5, defaultRules) + //for _, pokers := range pokersArr { + // pokers.SortByKey() + // t.Log(pokers.String()) + //} + //pokersArr, _ = Distribute(7, defaultRules) + //for _, pokers := range pokersArr { + // pokers.SortByKey() + // t.Log(pokers.String()) + //} + pokersArr, _ := Distribute(3, true, defaultRules) + for _, pokers := range pokersArr { + pokers.SortByKey() + t.Log(pokers.String()) + } +} + +type parseFacesCase struct { + pokers model.Pokers + actualType consts.FacesType +} + +func testParseFaces(t *testing.T, pokers model.Pokers, expected consts.FacesType) { + list := ParseFaces(pokers, defaultRules) + if expected == consts.FacesInvalid && len(list) > 0 { + t.Error("err at", pokers.String(), "expected invalid", "actual ", list[0].Values) + return + } + access := false + t.Log(pokers.String()) + for _, faces := range list { + if expected == faces.Type { + t.Log("\t\t->", faces.Keys) + access = true + } + } + if !access && expected != consts.FacesInvalid { + t.Error("err at", pokers.String(), "expected ", expected, "actual ", list[0].Values) + } +} + +func TestParseFaces(t *testing.T) { + testCases := []parseFacesCase{ + {getPokers(3), consts.FacesSingle}, + {getPokers(15), consts.FacesSingle}, + {getPokers(3, 3), consts.FacesDouble}, + {getPokers(3, 3, 3), consts.FacesTriple}, + {getPokers(3, 3, 3, 4), consts.FacesUnion3}, + {getPokers(3, 3, 3, 4, 4), consts.FacesUnion3}, + {getPokers(3, 3, 3, 4, 4, 5), consts.FacesInvalid}, + {getPokers(3, 3, 3, 3, 4), consts.FacesInvalid}, + {getPokers(3, 3, 3, 3, 4, 5), consts.FacesUnion4}, + {getPokers(3, 3, 3, 3, 3, 4, 5), consts.FacesInvalid}, + {getPokers(3, 4, 5, 6, 7), consts.FacesStraight}, + {getPokers(3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1), consts.FacesStraight}, + {getPokers(2, 3, 4, 5, 6, 7), consts.FacesInvalid}, + {getPokers(10, 11, 12, 13, 1, 2), consts.FacesInvalid}, + {getPokers(3, 3, 4, 4, 5, 5), consts.FacesStraight}, + {getPokers(3, 3, 3, 4, 4, 4), consts.FacesStraight}, + {getPokers(3, 3, 3, 3, 4, 4, 4, 4), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 3, 3, 4, 4, 4, 4, 4), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 4, 4, 4, 5, 6), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 4, 4, 4, 5, 5, 6, 6), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 3, 4, 4, 4, 4, 5, 6), consts.FacesInvalid}, + {getPokers(3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 6, 6), consts.FacesInvalid}, + {getPokers(3, 3, 3, 3, 4, 4, 4, 4, 5, 6, 7, 8), consts.FacesInvalid}, + {getPokers(3, 3, 3, 3), consts.FacesBomb}, + {getPokers(3, 3, 3, 3, 3), consts.FacesBomb}, + {getPokers(14, 15), consts.FacesBomb}, + {getPokers(14, 15, 15), consts.FacesBomb}, + {getPokers(14, 14, 15, 15), consts.FacesBomb}, + {getPokers(14, 14, 14, 15, 15, 16), consts.FacesInvalid}, + {getPokers(4, 4, 4, 4, 6, 6, 6, 6), consts.FacesInvalid}, + {getPokers(4, 4, 4, 16), consts.FacesInvalid}, + {getPokers(3, 3, 3, 4, 4, 4, 5, 5), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 4, 4, 4, 5, 5, 5, 7, 7, 7), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 7, 7), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5), consts.FacesUnion3Straight}, + {getPokers(3, 3, 4, 3, 3, 4, 4, 4, 5, 5, 5, 5), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 3, 3, 4, 4, 4, 4, 4), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 3, 4, 4, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 9, 9), consts.FacesUnion3Straight}, + {getPokers(3, 3, 3, 3, 3, 4, 4, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 9, 9), consts.FacesInvalid}, + {getPokers(3, 2, 2, 2, 3), consts.FacesUnion3}, + {getPokers(9, 9, 9, 8, 8, 8, 7, 7, 7, 11, 1, 15), consts.FacesUnion3Straight}, + } + for _, testCase := range testCases { + testParseFaces(t, testCase.pokers, testCase.actualType) + } +} + +func TestParseFaces2(t *testing.T) { + pokers1 := getPokers(3, 3, 3, 3, 4, 4, 4, 4) + faces1 := ParseFaces(pokers1, defaultRules) + fmt.Println(faces1) + + //pokers1 := getPokers(8, 8, 8, 8, 2, 2, 2, 2) + //faces1 := ParseFaces(pokers1, defaultRules) + //pokers2 := getPokers(9, 9, 9, 9, 1, 2) + //faces2 := ParseFaces(pokers2, defaultRules) + //fmt.Println(faces1[0]) + //fmt.Println(faces2[0]) +} + +func TestParseFacesScore(t *testing.T) { + testCases := []parseFacesCase{ + {pokers: getPokers(3, 3, 3, 3)}, + {pokers: getPokers(2, 2, 2, 2)}, + {pokers: getPokers(14, 14)}, + {pokers: getPokers(14, 15)}, + {pokers: getPokers(15, 15)}, + {pokers: getPokers(3, 3, 3, 3, 3)}, + {pokers: getPokers(2, 2, 2, 2, 2)}, + {pokers: getPokers(3, 3, 3, 3, 3, 3)}, + {pokers: getPokers(2, 2, 2, 2, 2, 2)}, + {pokers: getPokers(14, 14, 14)}, + {pokers: getPokers(14, 14, 15)}, + {pokers: getPokers(14, 15, 15)}, + {pokers: getPokers(15, 15, 15)}, + {pokers: getPokers(3, 3, 3, 3, 3, 3, 3)}, + {pokers: getPokers(2, 2, 2, 2, 2, 2, 2)}, + {pokers: getPokers(3, 3, 3, 3, 3, 3, 3, 3)}, + {pokers: getPokers(2, 2, 2, 2, 2, 2, 2, 2)}, + {pokers: getPokers(14, 14, 15, 15)}, + } + preScore := int64(-1) + for _, testCase := range testCases { + list := ParseFaces(testCase.pokers, defaultRules) + if len(list) > 0 { + faces := list[0] + if faces.Score < preScore { + t.Error(fmt.Sprintf("err score, pre %v should lt %v", preScore, faces.Score)) + } + preScore = faces.Score + t.Log(testCase.pokers.String(), faces.Score, faces.Type) + } + } +} + +func TestSort(t *testing.T) { + pokers := getPokers(3, 8, 9) + for i := range pokers { + pokers[i].Val = defaultRules.Value(pokers[i].Key) + } + pokers.SetOaa(8, 9) + pokers.SortByOaaValue() + t.Log(pokers.String()) + + pokers = getPokers(2, 5, 4, 9, 8) + for i := range pokers { + pokers[i].Val = defaultRules.Value(pokers[i].Key) + } + pokers.SetOaa(8, 9) + pokers.SortByOaaValue() + t.Log(pokers.String()) +} diff --git a/core/util/poker/runfast_poker.go b/core/util/poker/runfast_poker.go new file mode 100644 index 0000000..9e2c7b1 --- /dev/null +++ b/core/util/poker/runfast_poker.go @@ -0,0 +1,622 @@ +package poker + +import ( + "github.com/ratel-online/core/consts" + "github.com/ratel-online/core/model" + "github.com/ratel-online/core/util/arrays" + "math" + "sort" +) + +func RunFastDistribute(dontShuffle bool, rules Rules) []model.Pokers { + pokers := append(make(model.Pokers, 0), GetRunFastDontShuffleBase()...) + for i := range pokers { + pokers[i].Val = rules.Value(pokers[i].Key) + } + size := len(pokers) + if dontShuffle { + pokers.Shuffle(size, 3) + } else { + pokers.Shuffle(size, 1) + } + reserve := 0 + avgNum := (size - reserve) / 3 + pokersArr := make([]model.Pokers, 0) + for i := 0; i < 3; i++ { + pokerArr := make([]model.Poker, 0) + pokersArr = append(pokersArr, append(pokerArr, pokers[i*avgNum:(i+1)*avgNum]...)) + } + for i := range pokersArr { + pokersArr[i].SortByValue() + } + return pokersArr +} + +func RunFastParseFaces(pokers model.Pokers, rules Rules) []model.Faces { + if len(pokers) == 0 { + return nil + } + score := int64(0) + stats := map[int]int{} + group := map[int][]int{} + counts := make([]int, 0) + values := make([]int, 0) + //單排記分 + for _, poker := range pokers { + if poker.Key < 0 || poker.Key > 15 { + return nil + } + poker.Val = rules.Value(poker.Key) + score += int64(poker.Val) + values = append(values, poker.Val) + stats[poker.Val]++ + } + for v, c := range stats { + group[c] = append(group[c], v) + } + for c := range group { + counts = append(counts, c) + sort.Ints(group[c]) + } + sort.Ints(counts) + for i := 0; i < len(counts)/2; i++ { + counts[i], counts[len(counts)-i-1] = counts[len(counts)-i-1], counts[i] + } + list := make([]model.Faces, 0) + if 4 == len(pokers) && len(group[4]) == 1 { + list = append(list, model.Faces{Values: values, Score: int64(group[4][0])*2 + int64(len(pokers)*1000), Type: consts.FacesBomb}) + } else if counts[0] == 1 { + if len(group[counts[0]]) == 1 { + list = append(list, model.Faces{Values: values, Score: score, Type: consts.FacesSingle}) + } else if rules.IsStraight(group[counts[0]], counts[0]) { + list = append(list, model.Faces{Values: values, Score: score, Main: len(group[counts[0]]), Type: consts.FacesStraights}) + } + } else if counts[0] == 2 && len(counts) == 1 { + if len(group[counts[0]]) == 1 { + list = append(list, model.Faces{Values: values, Score: score, Type: consts.FacesDouble}) + } else if rules.IsStraight(group[counts[0]], counts[0]) { + list = append(list, model.Faces{Values: values, Score: score, Main: len(group[counts[0]]), Type: consts.FacesDoubles}) + } + } else if counts[0] >= 3 { + if len(counts) == 1 && len(group[counts[0]]) == 1 { + if counts[0] == 3 { + list = append(list, model.Faces{Values: values, Score: score, Type: consts.FacesTriple}) + } else { + list = append(list, model.Faces{Values: values, Score: int64(group[counts[0]][0]*counts[0]) + int64(len(pokers)*1000), Type: consts.FacesBomb}) + } + } else if len(counts) == 1 && rules.IsStraight(group[counts[0]], counts[0]) { + if counts[0] == 3 { + list = append(list, model.Faces{Values: values, Score: score, Main: len(group[counts[0]]), Type: consts.FacesThreeStraights}) + } else if counts[0] == 4 { + values = make([]int, 0) + for _, v := range group[counts[0]] { + values = arrays.AppendN(values, v, 3) + } + for _, v := range group[counts[0]] { + values = append(values, v) + } + list = append(list, model.Faces{Values: values, Score: score / 4 * 3, Main: len(group[counts[0]]), Extra: 1, Type: consts.FacesUnion3Straight}) + } else if counts[0] == 5 { + values = make([]int, 0) + for _, v := range group[counts[0]] { + values = arrays.AppendN(values, v, 3) + } + for _, v := range group[counts[0]] { + values = arrays.AppendN(values, v, 2) + } + list = append(list, model.Faces{Values: values, Score: score / 5 * 3, Main: len(group[counts[0]]), Extra: 1, Type: consts.FacesUnion3Straight}) + } + } else if len(group[3]) > 0 || (len(group[4]) > 0 && len(values)-4 != 3) { + //三帶二 + score := int64(0) + main := 0 + extra := 0 + _type := consts.FacesUnion3c2 + if len(group[4]) == 1 && len(values)-3 == 2 { + _type = consts.FacesUnion3c2s + main = 3 + score = int64(group[4][0] * 3) + list = append(list, model.Faces{Values: values, Score: score, Main: main, Extra: extra, Type: consts.FacesType(_type)}) + } else if len(group[4]) == 1 && len(group[3]) >= 1 { + temp := append([]int{group[4][0]}) + for k := range group[3] { + temp = append(temp, group[3][k]) + } + sort.Ints(temp) + switch len(temp) - 1 { + case 1: + if math.Abs(float64(group[4][0]-group[3][0])) == 1 { + _type = consts.FacesUnion3c2Cs + main = 6 + extra = len(values) - main + score = int64(group[4][0]*3 + group[3][0]*3) + list = append(list, model.Faces{Values: values, Score: score, Main: main, Extra: extra, Type: consts.FacesType(_type)}) + } + break + case 2: + if temp[2]-temp[0] == 2 { + _type = consts.FacesUnion3c2CsM + main = 9 + extra = len(values) - main + score = int64(group[4][0]*3 + group[3][0]*3 + group[3][1]*3) + list = append(list, model.Faces{Values: values, Score: score, Main: main, Extra: extra, Type: consts.FacesType(_type)}) + } else if temp[2]-temp[1] == 1 { + _type = consts.FacesUnion3c2Cs + main = 6 + extra = len(values) - main + score = int64(temp[2]*3 + temp[1]*3) + list = append(list, model.Faces{Values: values, Score: score, Main: main, Extra: extra, Type: consts.FacesType(_type)}) + } + break + } + } else if len(group[4]) == 2 && math.Abs(float64(group[4][0]-group[4][1])) <= 2 && len(group[3]) == 1 { + if math.Abs(float64(group[4][0]-group[4][1])) == 2 { + if math.Abs(float64(group[4][0]-group[3][0])) == 1 && math.Abs(float64(group[4][1]-group[3][0])) == 1 { + _type = consts.FacesUnion3c2CsM + main = 9 + extra = len(values) - main + score += int64(group[4][0]*3 + group[4][1]*3 + group[3][0]*3) + list = append(list, model.Faces{Values: values, Score: score, Main: main, Extra: extra, Type: consts.FacesType(_type)}) + } + } else { + if math.Abs(float64(group[4][0]-group[3][0])) == 1 || math.Abs(float64(group[4][1]-group[3][0])) == 1 { + _type = consts.FacesUnion3c2CsM + main = 9 + extra = len(values) - main + score += int64(group[4][0]*3 + group[4][1]*3 + group[3][0]*3) + list = append(list, model.Faces{Values: values, Score: score, Main: main, Extra: extra, Type: consts.FacesType(_type)}) + } + } + } else if len(group[4]) == 2 && len(values)-6 == 4 && math.Abs(float64(group[4][0]-group[4][1])) == 1 { + _type = consts.FacesUnion3c2Cs + main = 6 + extra = len(values) - main + score += int64(group[4][0]*3 + group[4][1]*3) + list = append(list, model.Faces{Values: values, Score: score, Main: main, Extra: extra, Type: consts.FacesType(_type)}) + } else if len(group[4]) >= 2 { + temp := make([]int, 0) + for k := range group[4] { + temp = append(temp, group[4][k]) + } + for k := range group[3] { + temp = append(temp, group[3][k]) + } + sort.Ints(temp) + for k := range temp { + if k+2 >= len(temp) { + break + } + if temp[k+2]-temp[k] == 2 { + _type = consts.FacesUnion3c2CM + if len(values)-9 == 6 { + _type = consts.FacesUnion3c2CsM + } + main = 9 + extra = len(values) - main + score += int64(temp[k]*3 + temp[k+1]*3 + temp[k+2]*3) + list = append(list, model.Faces{Values: values, Score: score, Main: main, Extra: extra, Type: consts.FacesType(_type)}) + break + } + } + } else if len(group[3]) == 1 && len(values)-3 <= 2 { + if len(values)-3 == 2 { + _type = consts.FacesUnion3c2s + } + main = 3 + score = int64(group[3][0] * 3) + list = append(list, model.Faces{Values: values, Score: score, Main: main, Extra: extra, Type: consts.FacesType(_type)}) + } else if len(group[3]) == 2 && len(values)-6 <= 4 && math.Abs(float64(group[3][0]-group[3][1])) == 1 { + _type = consts.FacesUnion3c2C + if len(values)-6 == 4 { + _type = consts.FacesUnion3c2Cs + } + main = 6 + extra = len(values) - main + score += int64(group[3][0]*3 + group[3][1]*3) + list = append(list, model.Faces{Values: values, Score: score, Main: main, Extra: extra, Type: consts.FacesType(_type)}) + } else if len(group[3]) > 2 && len(values)-6 == 4 { + temp := make([]int, 0) + for k := range group[3] { + temp = append(temp, group[3][k]) + } + sort.Ints(temp) + for k := range temp { + if k+1 >= len(temp) { + break + } + if temp[k+1]-temp[k] == 1 { + _type = consts.FacesUnion3c2Cs + main = 6 + extra = len(values) - main + score += int64(temp[k]*3 + temp[k+1]*3) + list = append(list, model.Faces{Values: values, Score: score, Main: main, Extra: extra, Type: consts.FacesType(_type)}) + break + } + } + } else if len(group[3]) == 3 && len(values)-9 <= 6 { + _type = consts.FacesUnion3c2CM + if len(values)-9 == 6 { + _type = consts.FacesUnion3c2CsM + } + sort.Ints(group[3]) + if group[3][2]-group[3][0] == 2 { + main = 9 + extra = len(values) - main + score += int64(group[3][0]*3 + group[3][1]*3 + group[3][1]*3) + } else { + list = parseUnionOrStraight(group, rules) + return list + } + list = append(list, model.Faces{Values: values, Score: score, Main: main, Extra: extra, Type: consts.FacesType(_type)}) + } else if len(group[3]) > 3 && len(values)-9 <= 6 { + //極端 + sort.Ints(group[3]) + for k := range group[3] { + if k+2 >= len(group[3]) { + break + } + if group[3][k+2]-group[3][k] == 2 { + _type = consts.FacesUnion3c2CM + if len(values)-9 == 6 { + _type = consts.FacesUnion3c2CsM + } + main = 9 + extra = len(values) - main + score += int64(group[3][k]*3 + group[3][k+1]*3 + group[3][k+2]*3) + list = append(list, model.Faces{Values: values, Score: score, Main: main, Extra: extra, Type: consts.FacesType(_type)}) + break + } + } + } else { + list = parseUnionOrStraight(group, rules) + } + } + if counts[0] == 4 && len(values)-4 <= 3 { + _type := consts.FacesUnion4C3 + if len(values)-4 == 3 { + _type = consts.FacesUnion4C3s + } + extra := len(values) - 4 + list = append(list, model.Faces{Values: values, Score: int64(group[counts[0]][0] * counts[0]), Main: len(group[counts[0]]), Extra: extra, Type: consts.FacesType(_type)}) + } + if counts[0] == 4 && len(counts) == 1 && len(group[counts[0]]) == 2 { + values = make([]int, 0) + values = arrays.AppendN(values, group[counts[0]][1], counts[0]) + values = arrays.AppendN(values, group[counts[0]][0], counts[0]) + list = append(list, model.Faces{Values: values, Score: int64(group[counts[0]][1] * counts[0]), Main: 1, Extra: 1, Type: consts.FacesUnion4}) + } + } + return list +} + +//RunFastComparativeFaces 计算能打的牌型 +func RunFastComparativeFaces(lastPokers model.Faces, pokers model.Pokers, rules Rules) []model.Faces { + accord := make([]model.Faces, 0) + score := int64(0) + stats := map[int]int{} + valStats := map[int]int{} + group := map[int][]int{} + counts := make([]int, 0) + values := make([]int, 0) + keys := make([]int, 0) + //單排記分 + for _, poker := range pokers { + poker.Val = rules.Value(poker.Key) + score += int64(poker.Val) + values = append(values, poker.Val) + keys = append(keys, poker.Key) + stats[poker.Key]++ + valStats[rules.Value(poker.Key)]++ + } + + for v, c := range stats { + group[c] = append(group[c], v) + } + + for c := range group { + counts = append(counts, c) + sort.Ints(group[c]) + } + sort.Ints(counts) + + switch lastPokers.Type { + //炸彈處理 + case consts.FacesBomb: + for k := len(group[4]) - 1; k >= 0; k-- { + if rules.Value(group[4][k]) > lastPokers.Values[0] { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesBomb, Keys: []int{group[4][k], group[4][k], group[4][k], group[4][k]}, Values: []int{rules.Value(group[4][k]), rules.Value(group[4][k]), rules.Value(group[4][k]), rules.Value(group[4][k])}}) + } + } + return accord + //單排處理 + case consts.FacesSingle: + // 倒序單排 + for k := len(keys) - 1; k >= 0; k-- { + if rules.Value(keys[k]) > lastPokers.Values[0] { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesSingle, Keys: []int{keys[k]}, Values: []int{rules.Value(keys[k])}}) + } + } + + break + //對子處理 + case consts.FacesDouble: + //倒序對子 + for k := len(group[2]) - 1; k >= 0; k-- { + if rules.Value(group[2][k]) > lastPokers.Values[0] { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesDouble, Keys: []int{group[2][k], group[2][k]}, Values: []int{rules.Value(group[2][k]), rules.Value(group[2][k])}}) + } + } + // 倒序拆三張 + for k := len(group[3]) - 1; k >= 0; k-- { + if rules.Value(group[3][k]) > lastPokers.Values[0] { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesDouble, Keys: []int{group[3][k], group[3][k]}, Values: []int{rules.Value(group[3][k]), rules.Value(group[3][k])}}) + } + } + // 倒序拆炸彈 + for k := len(group[4]) - 1; k >= 0; k-- { + if rules.Value(group[4][k]) > lastPokers.Values[0] { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesDouble, Keys: []int{group[4][k], group[4][k]}, Values: []int{rules.Value(group[4][k]), rules.Value(group[4][k])}}) + } + } + break + //三張處理 + case consts.FacesTriple, consts.FacesUnion3, consts.FacesUnion3c2, consts.FacesUnion3c2s: + // 倒序三張 + for k := len(group[3]) - 1; k >= 0; k-- { + //比分 + if int64(rules.Value(group[3][k])*3) > lastPokers.Score { + key, val := RunFastMake(keys, []int{group[3][k], group[3][k], group[3][k]}, 2, rules) + if len(keys)-3 <= 2 { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesUnion3c2, Keys: key, Values: val}) + } else { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesUnion3c2s, Keys: key, Values: val}) + } + } + } + // 倒序拆炸彈 + for k := len(group[4]) - 1; k >= 0; k-- { + if int64(rules.Value(group[4][k])*3) > lastPokers.Score { + key, val := RunFastMake(keys, []int{group[4][k], group[4][k], group[4][k]}, 2, rules) + if len(keys)-3 <= 2 { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesUnion3c2, Keys: key, Values: val}) + } else { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesUnion3c2s, Keys: key, Values: val}) + } + } + } + break + //二連三帶二處理 + case consts.FacesUnion3c2C, consts.FacesUnion3c2Cs: + temp := make([]int, 0) + for k := range group[4] { + temp = append(temp, group[4][k]) + } + for k := range group[3] { + temp = append(temp, group[3][k]) + } + sort.Ints(temp) + for k := range temp { + if k+1 >= len(temp) { + break + } + if temp[k+1]-temp[k] == 1 { + //比分數 + list := RunFastParseFaces(GetPokers(temp[k], temp[k], temp[k], temp[k+1], temp[k+1], temp[k+1]), rules) + if len(list) >= 0 { + if list[0].Score > lastPokers.Score { + key, val := RunFastMake(keys, []int{temp[k], temp[k], temp[k], temp[k+1], temp[k+1], temp[k+1]}, 4, rules) + if len(keys)-6 <= 4 { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesUnion3c2C, Keys: key, Values: val}) + } else { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesUnion3c2Cs, Keys: key, Values: val}) + } + } + } + } + } + break + //三連三帶二處理 + case consts.FacesUnion3c2CM, consts.FacesUnion3c2CsM: + temp := make([]int, 0) + for k := range group[4] { + temp = append(temp, group[4][k]) + } + for k := range group[3] { + temp = append(temp, group[3][k]) + } + sort.Ints(temp) + for k := range temp { + if k+2 >= len(temp) { + break + } + if temp[k+2]-temp[k] == 2 { + //比分數 + list := RunFastParseFaces(GetPokers(temp[k], temp[k], temp[k], temp[k+1], temp[k+1], temp[k+1], temp[k+2], temp[k+2], temp[k+2]), rules) + if len(list) >= 0 { + if list[0].Score > lastPokers.Score { + key, val := RunFastMake(keys, []int{temp[k], temp[k], temp[k], temp[k+1], temp[k+1], temp[k+1], temp[k+2], temp[k+2], temp[k+2]}, 6, rules) + if len(keys)-9 <= 6 { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesUnion3c2CM, Keys: key, Values: val}) + } else { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesUnion3c2CsM, Keys: key, Values: val}) + } + } + } + } + } + break + //四張處理 + case consts.FacesUnion4, consts.FacesUnion4C3, consts.FacesUnion4C3s: + for k := len(group[4]) - 1; k >= 0; k-- { + if int64(rules.Value(group[4][k]))*4 > lastPokers.Score { + if len(keys) == 4 { + break + } + key, val := RunFastMake(keys, []int{group[4][k], group[4][k], group[4][k], group[4][k]}, 3, rules) + if len(keys)-4 <= 3 { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesUnion4C3, Keys: key, Values: val}) + } else { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesUnion4C3s, Keys: key, Values: val}) + } + } + } + break + //連隊處理 + //連隊處理` + case consts.FacesDoubles: + if len(keys) >= len(lastPokers.Values) { + temp := make([]int, 0) + for k := range group[4] { + temp = append(temp, group[4][k]) + } + for k := range group[3] { + temp = append(temp, group[3][k]) + } + for k := range group[2] { + temp = append(temp, group[2][k]) + } + sort.Ints(temp) + for k := range temp { + if k+1 >= len(temp) { + break + } + if temp[k+1]-temp[k] == 1 { + //比分數 + list := RunFastParseFaces(GetPokers(temp[k], temp[k], temp[k+1], temp[k+1]), rules) + if len(list) >= 0 { + if list[0].Score > lastPokers.Score { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesDoubles, Keys: []int{temp[k], temp[k], temp[k+1], temp[k+1]}, Values: []int{rules.Value(temp[k]), rules.Value(temp[k]), rules.Value(temp[k+1]), rules.Value(temp[k+1])}}) + } + } + } + } + } + + break + + //三連隊處理 + //三張隊處理 + case consts.FacesThreeStraights: + if len(keys) >= len(lastPokers.Values) { + temp := make([]int, 0) + for k := range group[4] { + temp = append(temp, group[4][k]) + } + for k := range group[3] { + temp = append(temp, group[3][k]) + } + sort.Ints(temp) + for k := range temp { + if k+2 >= len(temp) { + break + } + if temp[k+2]-temp[k] == 2 { + //比分數 + list := RunFastParseFaces(GetPokers(temp[k], temp[k], temp[k], temp[k+1], temp[k+1], temp[k+1], temp[k+2], temp[k+2], temp[k+2]), rules) + if len(list) >= 0 { + if list[0].Score > lastPokers.Score { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesThreeStraights, Keys: []int{temp[k], temp[k], temp[k], temp[k+1], temp[k+1], temp[k+1], temp[k+2], temp[k+2], temp[k+2]}, Values: []int{rules.Value(temp[k]), rules.Value(temp[k]), rules.Value(temp[k]), rules.Value(temp[k+1]), rules.Value(temp[k+1]), rules.Value(temp[k+1]), rules.Value(temp[k+2]), rules.Value(temp[k+2]), rules.Value(temp[k+2])}}) + } + } + } + } + } + break + //順子處理 + //順子處理 + case consts.FacesStraights: + if len(keys) >= len(lastPokers.Values) { + sort.Ints(lastPokers.Values) + //不是封頂順子 + if lastPokers.Values[len(lastPokers.Values)-1] != 12 { + ats := lastPokers.Values[0] + 1 + for { + if ats+(len(lastPokers.Values)-1) <= 12 { + ass := true + val := make([]int, 0) + key := make([]int, 0) + tem := ats + for i := 0; i < len(lastPokers.Values); i++ { + _, ok := valStats[tem] + if !ok { + ass = false + } else { + val = append(val, tem) + key = append(key, RunFastKey(tem)) + } + tem++ + } + if ass { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesStraights, Keys: key, Values: val}) + } + ats++ + } else { + break + } + } + + } + } + break + } + // 倒序炸彈 + for k := len(group[4]) - 1; k >= 0; k-- { + accord = append(accord, model.Faces{Score: lastPokers.Score + 1, Type: consts.FacesBomb, Keys: []int{group[4][k], group[4][k], group[4][k], group[4][k]}, Values: []int{rules.Value(group[4][k]), rules.Value(group[4][k]), rules.Value(group[4][k]), rules.Value(group[4][k])}}) + } + return accord +} + +//RunFastIsMax 是否為跑得快最大的牌 kkkk +func RunFastIsMax(faces model.Faces) bool { + if len(faces.Keys) != 4 { + return false + } + return faces.Keys[0] == 13 && faces.Keys[1] == 13 && faces.Keys[2] == 13 && faces.Keys[3] == 13 +} + +//RunFastKey 跑得快值返回key +func RunFastKey(val int) int { + if val == 12 { + return 1 + } else if val == 13 { + return 2 + } + return val + 2 +} + +//RunFastMake 跑得快带牌算法 +func RunFastMake(keys []int, exclude []int, count int, rules Rules) ([]int, []int) { + values := make([]int, 0) + for _, key := range exclude { + j := 0 + for _, val := range keys { + if val != key { + keys[j] = val + j++ + } + } + keys = keys[:j] + } + for _, key := range keys { + values = append(values, rules.Value(key)) + } + sort.Ints(values) + key := make([]int, 0) + val := make([]int, 0) + if len(keys) >= count { + for i := 0; i < count; i++ { + key = append(key, RunFastKey(values[i])) + val = append(val, values[i]) + } + } else { + for i := 0; i < len(keys); i++ { + key = append(key, RunFastKey(values[i])) + val = append(val, values[i]) + } + } + for _, k := range exclude { + key = append(key, k) + val = append(val, rules.Value(k)) + } + return key, val +} diff --git a/core/util/poker/runfast_poker_test.go b/core/util/poker/runfast_poker_test.go new file mode 100644 index 0000000..f09a701 --- /dev/null +++ b/core/util/poker/runfast_poker_test.go @@ -0,0 +1,132 @@ +package poker + +import ( + "fmt" + "github.com/ratel-online/core/model" + "testing" +) + +var runFastRules = _runFastRules{} + +type _runFastRules struct { +} + +func (d _runFastRules) Value(key int) int { + if key == 1 { + return 12 + } else if key == 2 { + return 13 + } else if key > 13 { + return key + } + return key - 2 +} + +func (d _runFastRules) IsStraight(faces []int, count int) bool { + if faces[len(faces)-1]-faces[0] != len(faces)-1 { + return false + } + if faces[len(faces)-1] > 12 { + return false + } + if count == 1 { + return len(faces) >= 5 + } else if count == 2 { + return len(faces) >= 2 + } else if count > 2 { + return len(faces) >= 2 + } + return false +} + +func (d _runFastRules) StraightBoundary() (int, int) { + return 1, 12 +} + +func (d _runFastRules) Reserved() bool { + return false +} + +//跑得快分牌测试 +func TestRunFastDistribute(t *testing.T) { + for i := 0; i < 100; i++ { + pokersArr := RunFastDistribute(false, runFastRules) + for _, pokers := range pokersArr { + pokers.SortByValue() + t.Log(pokers.String()) + } + t.Log("\n") + } + + //不洗牌模式 + //pokersArr := RunFastDistribute(true, runFastRules) + //for _, pokers := range pokersArr { + // pokers.SortByValue() + // t.Log(pokers.String()) + //} +} + +func TestRunFastParseFacesScore(t *testing.T) { + testCases := []parseFacesCase{ + //10 9 5 5 5 4 4 4 + // + {pokers: getPokers(6, 6, 7, 7)}, + {pokers: getPokers(10, 10, 10, 11, 11, 11, 8, 8, 8, 5)}, + {pokers: getPokers(7, 7, 7, 8, 8, 8, 3, 3, 3, 7)}, + {pokers: getPokers(7, 7, 7, 8, 8, 8, 3, 3, 3, 3)}, + + //順子 or 連隊 + {pokers: getPokers(8, 8, 9, 9)}, + {pokers: getPokers(8, 8, 9, 9, 10, 10)}, + {pokers: getPokers(8, 8, 9, 9, 10, 10, 11, 11)}, + {pokers: getPokers(6, 7, 8, 9, 10)}, + {pokers: getPokers(7, 8, 9, 10, 11)}, + {pokers: getPokers(4, 5, 6, 7, 8, 9, 10)}, + {pokers: getPokers(5, 6, 7, 8, 9, 10, 11)}, + //三張 + {pokers: getPokers(6, 6, 6)}, + //三張連隊 + {pokers: getPokers(6, 6, 6, 7, 7, 7)}, + {pokers: getPokers(6, 6, 6, 7, 7, 7, 8, 8, 8)}, + {pokers: getPokers(6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9)}, + {pokers: getPokers(6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10)}, + //三帶2 + {pokers: getPokers(6, 6, 6, 7)}, + {pokers: getPokers(6, 6, 6, 6, 7)}, + //四帶三 + {pokers: getPokers(6, 6, 6, 6, 7, 7)}, + {pokers: getPokers(6, 6, 6, 6, 7, 7, 7)}, + {pokers: getPokers(6, 6, 6, 6, 7, 7, 7, 4, 5, 5)}, + {pokers: getPokers(5, 6, 7, 8, 9, 10, 11, 12, 13)}, + {pokers: getPokers(6, 6, 6, 6, 7, 7, 7, 7, 5, 5, 5, 4, 9, 8, 3)}, + {pokers: getPokers(6, 6, 6, 7, 7, 7, 8, 8, 8, 6, 7, 8, 3, 3, 3)}, + {pokers: getPokers(6, 6, 6, 7, 7, 7, 8, 8, 8, 5, 5, 5, 3, 3, 3)}, + {pokers: getPokers(6, 6, 6, 7, 7, 7, 5, 5, 5, 7, 5, 10, 10, 10, 10)}, + {pokers: getPokers(9, 9, 9, 9, 5)}, + {pokers: getPokers(3, 3, 3, 3)}, + {pokers: getPokers(2, 2, 2, 2)}, + //連隊 + {pokers: getPokers(11, 11, 11, 11)}, + {pokers: getPokers(13, 13, 13, 13)}, + } + preScore := int64(-1) + for _, testCase := range testCases { + list := RunFastParseFaces(testCase.pokers, runFastRules) + if len(list) > 0 { + faces := list[0] + if faces.Score < preScore { + t.Error(fmt.Sprintf("err score, pre %v should lt %v", preScore, faces.Score)) + } + preScore = faces.Score + t.Log(testCase.pokers.String(), faces.Score, faces.Type) + } + } +} + +func TestRunFastComparativeFaces(t *testing.T) { + list := RunFastComparativeFaces(model.Faces{Keys: []int{6, 6, 7, 7}, Values: []int{4, 4, 5, 5}, Score: int64(18), Main: 1, Type: 18}, getPokers(7, 7, 6, 6), runFastRules) + if len(list) > 0 { + faces := list[0] + t.Log(faces.Values, faces.Score, faces.Type) + } +} diff --git a/core/util/poker/texas.go b/core/util/poker/texas.go new file mode 100644 index 0000000..487e09a --- /dev/null +++ b/core/util/poker/texas.go @@ -0,0 +1,39 @@ +package poker + +import ( + "github.com/ratel-online/core/model" + "github.com/ratel-online/core/pkg/holdem" +) + +func ParseTexasFaces(hand, board model.Pokers) (*model.TexasFaces, error) { + // prevent slice append slice from modifying the original slice + cards := make(model.Pokers, 0) + cards = append(cards, hand...) + cards = append(cards, board...) + + holdemCards := [7]holdem.Card{} + for i, c := range cards { + val := c.Key + if val == 1 { + val = 14 + } + val <<= 4 + switch c.Suit { + case model.Spade: + val |= 1 + case model.Club: + val |= 2 + case model.Heart: + val |= 3 + case model.Diamond: + val |= 4 + } + holdemCards[i] = holdem.Card(val) + } + + score, _ := holdem.HighestHandValue(holdemCards) + return &model.TexasFaces{ + Type: model.TexasFacesType(score>>20 + 1), + Score: int64(score), + }, nil +} diff --git a/core/util/poker/texas_test.go b/core/util/poker/texas_test.go new file mode 100644 index 0000000..ff8b5d1 --- /dev/null +++ b/core/util/poker/texas_test.go @@ -0,0 +1,44 @@ +package poker + +import ( + "github.com/ratel-online/core/model" + "testing" +) + +func TestParseTexasFaces(t *testing.T) { + testCase := []struct { + hand, board model.Pokers + expected *model.TexasFaces + }{ + { + hand: model.Pokers{{Key: 1, Suit: model.Club}, {Key: 2, Suit: model.Club}}, + board: model.Pokers{{Key: 3, Suit: model.Club}, {Key: 4, Suit: model.Club}, {Key: 5, Suit: model.Club}, {Key: 6, Suit: model.Club}, {Key: 7, Suit: model.Club}}, + expected: &model.TexasFaces{Type: model.TexasFacesTypeStraightFlush}, + }, + { + hand: model.Pokers{{Key: 1, Suit: model.Club}, {Key: 2, Suit: model.Club}}, + board: model.Pokers{{Key: 3, Suit: model.Heart}, {Key: 4, Suit: model.Diamond}, {Key: 5, Suit: model.Club}, {Key: 6, Suit: model.Heart}, {Key: 8, Suit: model.Club}}, + expected: &model.TexasFaces{Type: model.TexasFacesTypeStraight}, + }, + { + hand: model.Pokers{{Key: 1, Suit: model.Club}, {Key: 2, Suit: model.Club}}, + board: model.Pokers{{Key: 9, Suit: model.Club}, {Key: 4, Suit: model.Diamond}, {Key: 12, Suit: model.Club}, {Key: 6, Suit: model.Club}, {Key: 8, Suit: model.Club}}, + expected: &model.TexasFaces{Type: model.TexasFacesTypeFlush}, + }, + { + hand: model.Pokers{{Key: 1, Suit: model.Heart}, {Key: 5, Suit: model.Club}}, + board: model.Pokers{{Key: 8, Suit: model.Club}, {Key: 8, Suit: model.Diamond}, {Key: 9, Suit: model.Heart}, {Key: 9, Suit: model.Club}, {Key: 13, Suit: model.Heart}}, + expected: &model.TexasFaces{Type: model.TexasFacesTypeTwoPairs}, + }, + } + for _, tc := range testCase { + actual, err := ParseTexasFaces(tc.hand, tc.board) + if err != nil { + t.Fatal(err) + } + if actual.Type != tc.expected.Type { + t.Fatalf("expected %v, got %v", tc.expected.Type, actual.Type) + } + t.Logf("type: %v, score: %v", actual.Type, actual.Score) + } +} diff --git a/core/util/rand/rand.go b/core/util/rand/rand.go new file mode 100644 index 0000000..fe7b6a8 --- /dev/null +++ b/core/util/rand/rand.go @@ -0,0 +1,11 @@ +package rand + +import ( + "math/rand" + "time" +) + +func Intn(n int) int { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + return r.Intn(n) +} diff --git a/core/util/strings/strings.go b/core/util/strings/strings.go new file mode 100644 index 0000000..84b537a --- /dev/null +++ b/core/util/strings/strings.go @@ -0,0 +1,118 @@ +package strings + +import ( + "encoding/json" + "strconv" + "strings" +) + +var desensitizeWords = [][]string{ + {"邓总", "老哥"}, + {"dengzong", "老哥"}, + {"邓", "哥"}, + {"dz", "老哥"}, + {"deng", "哥"}, + {"zong", "哥"}, + {"死", "爱"}, + {"si", "爱"}, + {"傻逼", "机灵鬼"}, + {"shabi", "机灵鬼"}, + {"傻", "机灵"}, + {"sha", "机灵"}, + {"逼", "哥"}, + {"bi", "哥"}, + {"你妈", "我爹"}, + {"你马", "我爹"}, + {"ma", "爹"}, + {"狗", "神"}, + {"gou", "神"}, + {"sb", "神"}, +} + +func String(dest interface{}) string { + var key string + if dest == nil { + return key + } + switch dest.(type) { + case float64: + key = strconv.FormatFloat(dest.(float64), 'f', -1, 64) + case *float64: + key = strconv.FormatFloat(*dest.(*float64), 'f', -1, 64) + case float32: + key = strconv.FormatFloat(float64(dest.(float32)), 'f', -1, 32) + case *float32: + key = strconv.FormatFloat(float64(*dest.(*float32)), 'f', -1, 32) + case int: + key = strconv.Itoa(dest.(int)) + case *int: + key = strconv.Itoa(*dest.(*int)) + case uint: + key = strconv.Itoa(int(dest.(uint))) + case *uint: + key = strconv.Itoa(int(*dest.(*uint))) + case int8: + key = strconv.Itoa(int(dest.(int8))) + case *int8: + key = strconv.Itoa(int(*dest.(*int8))) + case uint8: + key = strconv.Itoa(int(dest.(uint8))) + case *uint8: + key = strconv.Itoa(int(*dest.(*uint8))) + case int16: + key = strconv.Itoa(int(dest.(int16))) + case *int16: + key = strconv.Itoa(int(*dest.(*int16))) + case uint16: + key = strconv.Itoa(int(dest.(uint16))) + case *uint16: + key = strconv.Itoa(int(*dest.(*uint16))) + case int32: + key = strconv.Itoa(int(dest.(int32))) + case *int32: + key = strconv.Itoa(int(*dest.(*int32))) + case uint32: + key = strconv.Itoa(int(dest.(uint32))) + case *uint32: + key = strconv.Itoa(int(*dest.(*uint32))) + case int64: + key = strconv.FormatInt(dest.(int64), 10) + case *int64: + key = strconv.FormatInt(*dest.(*int64), 10) + case uint64: + key = strconv.FormatUint(dest.(uint64), 10) + case *uint64: + key = strconv.FormatUint(*dest.(*uint64), 10) + case string: + key = dest.(string) + case *string: + key = *dest.(*string) + case []byte: + key = string(dest.([]byte)) + case *[]byte: + key = string(*dest.(*[]byte)) + case bool: + if dest.(bool) { + key = "true" + } else { + key = "false" + } + case *bool: + if *dest.(*bool) { + key = "true" + } else { + key = "false" + } + default: + newValue, _ := json.Marshal(dest) + key = string(newValue) + } + return key +} + +func Desensitize(str string) string { + for _, words := range desensitizeWords { + str = strings.ReplaceAll(str, words[0], words[1]) + } + return str +} diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..45ad5a0 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,56 @@ +# 构建阶段 +FROM golang:1.17-alpine AS builder + +# 设置工作目录 +WORKDIR /app + +# 安装必要的构建工具 +RUN apk add --no-cache git + +# 复制 go.mod 和 go.sum 文件 +COPY go.mod go.sum ./ + +# 下载依赖 +RUN go mod download + +# 复制源代码 +COPY . . + +# 构建应用 +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ratel-server main.go + +# 运行阶段 +FROM alpine:latest + +# 安装必要的运行时依赖 +RUN apk --no-cache add ca-certificates tzdata + +# 设置时区为上海 +ENV TZ=Asia/Shanghai +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# 创建非root用户 +RUN addgroup -g 1000 -S ratel && \ + adduser -u 1000 -S ratel -G ratel + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制二进制文件 +COPY --from=builder /app/ratel-server . + +# 更改文件所有权 +RUN chown -R ratel:ratel /app + +# 切换到非root用户 +USER ratel + +# 暴露端口 +EXPOSE 9998 9999 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD nc -z localhost 9998 && nc -z localhost 9999 || exit 1 + +# 启动应用 +CMD ["./ratel-server"] \ No newline at end of file diff --git a/server/LICENSE b/server/LICENSE new file mode 100644 index 0000000..b777b61 --- /dev/null +++ b/server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 ratel-online + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/server/Makefile b/server/Makefile new file mode 100644 index 0000000..cdaa20b --- /dev/null +++ b/server/Makefile @@ -0,0 +1,80 @@ +.PHONY: help build run stop clean logs shell test prod-build prod-up prod-down + +# 检测 Docker Compose 命令 +DOCKER_COMPOSE := $(shell which docker-compose 2>/dev/null) +ifeq ($(DOCKER_COMPOSE),) + DOCKER_COMPOSE := $(shell docker compose version >/dev/null 2>&1 && echo "docker compose" || echo "") +endif +ifeq ($(DOCKER_COMPOSE),) + $(error "Docker Compose 未安装。请安装 docker-compose 或升级到支持 'docker compose' 的 Docker 版本") +endif + +# 默认目标 +help: + @echo "Ratel游戏服务器 Docker 管理命令" + @echo "" + @echo "当前使用的 Docker Compose 命令: $(DOCKER_COMPOSE)" + @echo "" + @echo "开发环境命令:" + @echo " make build - 构建Docker镜像" + @echo " make run - 启动开发环境" + @echo " make stop - 停止开发环境" + @echo " make logs - 查看日志" + @echo " make shell - 进入容器shell" + @echo " make clean - 清理容器和镜像" + @echo "" + @echo "生产环境命令:" + @echo " make prod-build - 构建生产镜像" + @echo " make prod-up - 启动生产环境" + @echo " make prod-down - 停止生产环境" + @echo "" + @echo "测试命令:" + @echo " make test - 运行测试" + +# 开发环境命令 +build: + $(DOCKER_COMPOSE) build + +run: + $(DOCKER_COMPOSE) up -d + @echo "服务已启动:" + @echo " WebSocket: http://localhost:9998" + @echo " TCP: localhost:9999" + +stop: + $(DOCKER_COMPOSE) down + +logs: + $(DOCKER_COMPOSE) logs -f + +shell: + $(DOCKER_COMPOSE) exec ratel-server sh + +clean: + $(DOCKER_COMPOSE) down -v + docker rmi ratel-server:latest || true + +# 生产环境命令 +prod-build: + docker build -t ratel-server:latest . + +prod-up: + $(DOCKER_COMPOSE) -f docker-compose.prod.yaml up -d + @echo "生产环境已启动:" + @echo " WebSocket: http://localhost:9998" + @echo " TCP: localhost:9999" + @echo " HTTP: http://localhost:80" + +prod-down: + $(DOCKER_COMPOSE) -f docker-compose.prod.yaml down + +# 测试命令 +test: + @echo "测试WebSocket连接..." + @curl -s -o /dev/null -w "HTTP状态码: %{http_code}\n" http://localhost:9998 || echo "WebSocket端口未响应" + @echo "" + @echo "测试TCP连接..." + @nc -zv localhost 9999 2>&1 || echo "TCP端口未响应" + @echo "" + @echo "测试Nginx健康检查..." + @curl -s http://localhost/health || echo "Nginx未运行" \ No newline at end of file diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..8b80fea --- /dev/null +++ b/server/README.md @@ -0,0 +1,134 @@ +# Ratel-Online +[![Go Reference](https://pkg.go.dev/badge/github.com/ratel-online/server.svg)](https://pkg.go.dev/github.com/ratel-online/server) + +命令行在线多人棋牌游戏!支持斗地主,跑得快, 德州扑克, 麻将, 骗子酒馆, Uno 等模式! +## 客户端 +- 浏览器在线:[http://ratel.isnico.com/](http://ratel.isnico.com/) +- 客户端下载:[https://github.com/ratel-online/client/releases](https://github.com/ratel-online/client/releases) +## 服务器 +- Websocket: 192.252.182.94:9998 +- TCP: 192.252.182.94:9999 +## 社区 +- QQ Group: [948365095](https://qm.qq.com/q/tRPNbC6NtC) +- Telegram: [ratel-server](https://t.me/ratel_server) +## 玩法介绍 +### 模式 +- 经典版斗地主模式 +- 癞子版斗地主模式 +- 癞子版技能大招模式 +- 跑得快模式 +- 德州扑克 +- 麻将(存在问题) +- 骗子酒馆 +- Uno(开发中) + +### 德州扑克规则 +游戏人数2~10人不等,每人发2张底牌,5张公共牌,最终组合5张牌中最大的牌型。 + +游戏过程中可输入指令: +- call:跟注 +- raise:加注 +- allin:全下 +- fold:弃牌 +- check:看牌 + +输入其它内容则视为聊天内容,详细规则参考[德州扑克的起源](https://pokerfans.jp/poker-begin) + +### 斗地主类规则 +游戏人数2~6人不等,超过3人2副牌,超过5人3副牌,规则参考欢乐斗地主。 + +出牌时,直接输入想出的牌型,例如3~A顺子:`34567890jqka`,单10:`0`, 对2:`22`,王炸:`sx`。 + +癞子模式下同样,缺失的牌会自动使用癞子牌代替,例如当前牌型是``*7 6 6 5``,输入``6665``时会自动使用癞子牌``*7``来代替缺失的6。 + +更多例子: +- 4个10:`0000` +- 王炸:`sx` +- 3~A顺子:`34567890jqka` +- 3带1:`3334` +- 飞机:`jjjqqq34` + +### 跑得快规则 +游戏人数3人开局,规则参考欢乐斗地主的跑得快 + +出牌时,能够打起的牌`必须出` + +`三带二`,`四带三`,`二连对`,这些是与斗地主不同之处 + +非标准牌只能在最后出,比如 `3334`(三带一) 只能只剩下这四张牌的时候出(四带二也同理) + +更多例子: +- 3带2:`000j8` +- 4带3:`00004j8` +- 4个10:`0000` +- 3~A顺子:`34567890jqka` +- 连队:`3344` +- 飞机:`jjjqqq3457` + +### 麻将规则 +支持经典中国麻将玩法,包含吃、碰、杠、胡等基本操作。 + +### 骗子酒馆规则 +游戏人数2~4人不等,每人5张牌,一张指示牌。 + +游戏目标:通过撒谎和质疑,成为最后一个存活的玩家。 + +游戏规则: +- 每个玩家有一把装有1-6发子弹的左轮手枪 +- 玩家可以出牌(声称是指示牌或大小王) +- 下家可以选择质疑或继续出牌 +- 如果质疑成功,撒谎者扣动扳机;如果质疑失败,质疑者扣动扳机 +- 被击中的玩家淘汰,最后存活的玩家获胜 + +游戏指令: +- 输入牌面(如 `k`, `q`, `a`, `s`, `x`):出牌 +- `c` 或 `质疑`:质疑上家 + +### Uno规则 +经典Uno卡牌游戏,支持多人游戏。 + +### 演示 +视频教程:[https://www.bilibili.com/video/BV16Y411b7BD](https://www.bilibili.com/video/BV16Y411b7BD) + +![demo](demo.gif) +### 指令 +全局指令: +- `v`:刷新可用房间列表/查看房间成员/查看其它玩家游戏状态 +- `e`:退出/返回 + +房间指令: +- `s`:房间内开始游戏 +- `set ds on`: 开启不洗牌模式 +- `set ds off`: 关闭不洗牌模式 +- `set ct off`: 关闭聊天 +- `set ct on`: 开启聊天 +- `set sk on`: 开启技能模式 +- `set sk off`: 关闭技能模式 +- `set lz on`: 开启癞子模式 +- `set lz off`: 关闭癞子模式 +- `set pwd xxxx`:设置密码,例如密码为"xxxx" +- `set pwd off`:取消密码 +- `set pn 6`: 设置房间人数上限,例如最大6个人(默认为3人) +- `set ip on`: 开启显示IP +- `set ip off`: 关闭显示IP +- `set jt on`: 开启允许大小王作为指示牌(骗子酒馆专用) +- `set jt off`: 关闭允许大小王作为指示牌(骗子酒馆专用) +- `k <玩家ID>` 或 `kicking <玩家ID>` 或 `kill <玩家ID>`:房主踢出指定玩家 +- 其余的会转为聊天内容 + +游戏指令: +- `p`:不出 +- 其余的会转为聊天内容 + +## 技能大招 +开启技能模式以后,玩家会随机被分配以下技能中的一个,**主回合**触发: +- **我要色色**:其余玩家沉迷其中,趁机偷掉了他们的最牛的牌 +- **火眼金睛**:看穿对手的手牌 +- **改换家门**:手牌重新分配 +- **破斧沉舟**:只留下5张最强的牌 +- **大幻想家**:最小的一张牌变成了癞子 +- **两极反转**: 随机与一名玩家调换手牌 +- **追亡逐北**:主回合,多获得一次出牌机会 +- **时空裂缝**:其余玩家出牌时间减半 +- **996**:所有对手强制获得9,9,6三张牌 +- **添砖加瓦**:从弃牌池中随机抽取两张牌返还给所有对手 diff --git a/server/bot/bot.go b/server/bot/bot.go new file mode 100644 index 0000000..7bfac63 --- /dev/null +++ b/server/bot/bot.go @@ -0,0 +1,83 @@ +package bot + +import ( + "fmt" + "github.com/Szzrain/Milky-go-sdk" + "github.com/ratel-online/core/log" +) + +// Session 全局机器人会话 +var Session *Milky_go_sdk.Session + +// GroupID 群ID +var GroupID int64 + +// Logger 实现 Milky_go_sdk 的 Logger 接口 +type Logger struct{} + +func (l *Logger) Infof(format string, args ...interface{}) { + log.Infof(format, args...) +} + +func (l *Logger) Errorf(format string, args ...interface{}) { + log.Errorf(format, args...) +} + +func (l *Logger) Debugf(format string, args ...interface{}) { + _ = format + _ = args +} + +func (l *Logger) Warnf(format string, args ...interface{}) { + log.Infof(format, args...) +} + +func (l *Logger) Info(args ...interface{}) { + log.Info(fmt.Sprint(args...)) +} + +func (l *Logger) Error(args ...interface{}) { + log.Error(fmt.Sprint(args...)) +} + +func (l *Logger) Debug(args ...interface{}) { + _ = args +} + +func (l *Logger) Warn(args ...interface{}) { + log.Info(fmt.Sprint(args...)) +} + +// SendGroupMessage 发送群消息 +func SendGroupMessage(groupID int64, content string) error { + if Session == nil { + return fmt.Errorf("bot not connected") + } + var message []Milky_go_sdk.IMessageElement + message = append(message, &Milky_go_sdk.TextElement{Text: content}) + _, err := Session.SendGroupMessage(groupID, &message) + return err +} + +// Connect 连接机器人 +func Connect(addr, token string, groupID int64) error { + GroupID = groupID + m, err := Milky_go_sdk.New("ws://"+addr+"/event", "http://"+addr+"/api", token, &Logger{}) + if err != nil { + return fmt.Errorf("创建Bot会话失败: %v", err) + } + err = m.Open() + if err != nil { + return fmt.Errorf("连接Bot失败: %v", err) + } + Session = m + log.Infof("Bot已连接: %s", addr) + return nil +} + +// Close 关闭机器人连接 +func Close() { + if Session != nil { + Session.Close() + } +} \ No newline at end of file diff --git a/server/build.ps1 b/server/build.ps1 new file mode 100644 index 0000000..dffd9c6 --- /dev/null +++ b/server/build.ps1 @@ -0,0 +1,63 @@ +# ============================== +# Go Multi-platform Build Script +# ============================== + +$projectName = "ratel-server" +$mainFile = "main.go" +$targetDir = "target" + +# Create output directory +if (!(Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir | Out-Null + Write-Host "Created directory: $targetDir" -ForegroundColor DarkGray +} + +# 目标平台列表 +$platforms = @( + @{ os = "windows"; arch = "amd64" }, + @{ os = "windows"; arch = "386" }, + @{ os = "windows"; arch = "arm64" }, + + @{ os = "linux"; arch = "amd64" }, + @{ os = "linux"; arch = "386" }, + @{ os = "linux"; arch = "arm64" }, + @{ os = "linux"; arch = "arm" }, + + @{ os = "darwin"; arch = "amd64" }, + @{ os = "darwin"; arch = "arm64" } +) + +Write-Host "`nStarting Go multi-platform build: $projectName`n" -ForegroundColor Cyan + +foreach ($p in $platforms) { + $goos = $p.os + $goarch = $p.arch + + $ext = if ($goos -eq "windows") { ".exe" } else { "" } + + $outputName = "$projectName-$goos-$goarch$ext" + $outputPath = Join-Path $targetDir $outputName + + Write-Host "> Building $goos/$goarch ..." -NoNewline + + # Environment variables + $env:GOOS = $goos + $env:GOARCH = $goarch + $env:CGO_ENABLED = "0" + + go build -trimpath -ldflags "-s -w" ` + -o $outputPath $mainFile 2>$null + + if ($LASTEXITCODE -eq 0) { + Write-Host " Success -> $outputName" -ForegroundColor Green + } else { + Write-Host " Failed" -ForegroundColor Red + } +} + +# 清理环境变量 +$env:GOOS = "" +$env:GOARCH = "" +$env:CGO_ENABLED = "" + +Write-Host "`nAll platforms build completed. Output directory: $targetDir" -ForegroundColor Yellow diff --git a/server/build.sh b/server/build.sh new file mode 100644 index 0000000..31dea4f --- /dev/null +++ b/server/build.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# Build script: Generate executables for all platforms +PROJECT_NAME="ratel-server" +TARGET_DIR="target" + +# Create target directory if it doesn't exist +if [ ! -d "$TARGET_DIR" ]; then + mkdir -p "$TARGET_DIR" + echo "Created directory: $TARGET_DIR" +fi + +# 定义目标平台 (OS/Arch) +PLATFORMS=( + "windows/amd64" + "windows/386" + "windows/arm64" + "linux/amd64" + "linux/386" + "linux/arm64" + "linux/arm" + "darwin/amd64" + "darwin/arm64" +) + +echo -e "\033[36mStarting project build: $PROJECT_NAME\033[0m\n" + +for PLATFORM in "${PLATFORMS[@]}"; do + GOOS=${PLATFORM%/*} + GOARCH=${PLATFORM#*/} + + EXTENSION="" + if [ "$GOOS" == "windows" ]; then + EXTENSION=".exe" + fi + + OUTPUT_NAME="${PROJECT_NAME}-${GOOS}-${GOARCH}${EXTENSION}" + OUTPUT_PATH="${TARGET_DIR}/${OUTPUT_NAME}" + + echo -n "Building: ${PLATFORM} ..." + + # Set environment variables and build + export GOOS=$GOOS + export GOARCH=$goarch + + go build -o "$OUTPUT_PATH" main.go 2>/dev/null + + if [ $? -eq 0 ]; then + echo -e " \033[32m[Done]\033[0m -> $OUTPUT_NAME" + else + echo -e " \033[31m[Failed]\033[0m" + fi +done + +# 重置环境变量 +unset GOOS +unset GOARCH + +echo -e "\n\033[33mAll build tasks completed. Output directory: $TARGET_DIR\033[0m" diff --git a/server/consts/const.go b/server/consts/const.go new file mode 100644 index 0000000..6b61e57 --- /dev/null +++ b/server/consts/const.go @@ -0,0 +1,144 @@ +package consts + +import ( + "time" + + "github.com/ratel-online/core/consts" +) + +type StateID int + +const ( + _ StateID = iota + StateWelcome + StateHome + StateJoin + StateCreate + StateWaiting + StateGame + StateRunFastGame + StateUnoGame + StateMahjongGame + StateTexasGame + StateLiarGame +) + +type SkillID int + +const ( + _ SkillID = iota - 1 + SkillWYSS + SkillHYJJ + SkillGHJM + SkillPFCZ + SkillDHXJ + SkillLJFZ + SkillZWZB + SkillSKLF + Skill996 + SkillTZJW +) + +const ( + IsStart = consts.IsStart + IsStop = consts.IsStop + + MinPlayers = 3 + // MaxPlayers https://github.com/ratel-online/server/issues/14 小鄧修改 + MaxPlayers = 3 + + RoomStateWaiting = 1 + RoomStateRunning = 2 + + GameTypeClassic = 1 + GameTypeLaiZi = 2 + GameTypeSkill = 3 + GameTypeRunFast = 4 + GameTypeTexas = 5 + GameTypeMahjong = 6 + GameTypeLiar = 7 + GameTypeUno = 8 + + RobTimeout = 20 * time.Second + PlayTimeout = 40 * time.Second + PlayMahjongTimeout = 30 * time.Second + BetTimeout = 60 * time.Second +) + +// Room properties. +const ( + RoomPropsDotShuffle = "ds" + RoomPropsLaiZi = "lz" + RoomPropsSkill = "sk" + RoomPropsPassword = "pwd" + RoomPropsPlayerNum = "pn" + RoomPropsChat = "ct" + RoomPropsShowIP = "ip" + RoomPropsJokerAsTarget = "jt" +) + +var MnemonicSorted = []int{15, 14, 2, 1, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3} + +var RunFastMnemonicSorted = []int{2, 1, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3} + +type Error struct { + Code int + Msg string + Exit bool +} + +func (e Error) Error() string { + return e.Msg +} + +func NewErr(code int, exit bool, msg string) Error { + return Error{Code: code, Exit: exit, Msg: msg} +} + +var ( + ErrorsExist = NewErr(1, true, "Exist. ") + ErrorsChanClosed = NewErr(1, true, "Chan closed. ") + ErrorsTimeout = NewErr(1, false, "Timeout. ") + ErrorsInputInvalid = NewErr(1, false, "Input invalid. ") + ErrorsChatUnopened = NewErr(1, false, "Chat disabled. ") + ErrorsChatUnopenedDuringGame = NewErr(1, false, "Chat disabled during game. ") + ErrorsAuthFail = NewErr(1, true, "Auth fail. ") + ErrorsRoomInvalid = NewErr(1, true, "Room invalid. ") + ErrorsGameTypeInvalid = NewErr(1, false, "Game type invalid. ") + ErrorsRoomPlayersIsFull = NewErr(1, false, "Room players is fill. ") + ErrorsRoomPassword = NewErr(1, false, "Sorry! Password incorrect! ") + ErrorsJoinFailForRoomRunning = NewErr(1, false, "Join fail, room is running. ") + ErrorsJoinFailForKicked = NewErr(1, false, "Join fail, you have been kicked from this room. ") + ErrorsGamePlayersInvalid = NewErr(1, false, "Game players invalid. ") + ErrorsPokersFacesInvalid = NewErr(1, false, "Pokers faces invalid. ") + ErrorsHaveToPlay = NewErr(1, false, "Have to play. ") + ErrorsMustHaveToPlay = NewErr(1, false, "There is a hand that can be played and must be played. ") + ErrorsEndToPlay = NewErr(1, false, "Can only come out at the end. ") + ErrorsUnknownTexasRound = NewErr(1, false, "Unknown texas round. ") + ErrorsGamePlayersInsufficient = NewErr(1, false, "Game players insufficient. ") + ErrorsCannotKickYourself = NewErr(1, false, "Cannot kick yourself. ") + ErrorsPlayerNotInRoom = NewErr(1, true, "Player not in room. ") + GameTypes = map[int]string{ + GameTypeClassic: "斗地主", + GameTypeLaiZi: "斗地主-癞子版", + GameTypeSkill: "斗地主-大招版", + GameTypeRunFast: "跑得快", + GameTypeTexas: "德州扑克", + //GameTypeUno: "Uno", + GameTypeMahjong: "Mahjong", + GameTypeLiar: "liar's bar", + } + GameTypesIds = []int{ + GameTypeClassic, + GameTypeLaiZi, + GameTypeSkill, + GameTypeRunFast, + GameTypeTexas, + GameTypeMahjong, + GameTypeLiar, + } + RoomStates = map[int]string{ + RoomStateWaiting: "Waiting", + RoomStateRunning: "Running", + } +) diff --git a/server/database/database.go b/server/database/database.go new file mode 100644 index 0000000..79a0a1e --- /dev/null +++ b/server/database/database.go @@ -0,0 +1,501 @@ +package database + +import ( + "sort" + "strconv" + stringx "strings" + "sync/atomic" + "time" + + "github.com/awesome-cap/hashmap" + "github.com/ratel-online/core/log" + modelx "github.com/ratel-online/core/model" + "github.com/ratel-online/core/network" + "github.com/ratel-online/core/util/async" + "github.com/ratel-online/core/util/json" + "github.com/ratel-online/core/util/strings" + "github.com/ratel-online/server/consts" +) + +var roomIds int64 = 0 +var players = hashmap.New() // 存储连接过服务器的全部用户 +var connPlayers = hashmap.New() +var rooms = hashmap.New() +var roomPlayers = hashmap.New() +var roomSpectators = hashmap.New() +var roomKickedPlayers = hashmap.New() +var roomPropsSetter = map[string]func(r *Room, v string){ + consts.RoomPropsSkill: func(r *Room, v string) { + r.EnableSkill = v == "on" + r.EnableLandlord = !r.EnableSkill + }, + consts.RoomPropsLaiZi: func(r *Room, v string) { + r.EnableLaiZi = v == "on" + }, + consts.RoomPropsDotShuffle: func(r *Room, v string) { + r.EnableDontShuffle = v == "on" + }, + consts.RoomPropsPassword: func(r *Room, v string) { + if v == "off" { + r.Password = "" + } else { + r.Password = v + } + }, + consts.RoomPropsChat: func(r *Room, v string) { + r.EnableChat = v == "on" + }, + consts.RoomPropsPlayerNum: func(r *Room, v string) { + n, _ := strconv.Atoi(v) + if n < 2 || n > 50 { + n = consts.MaxPlayers + } + r.MaxPlayers = n + }, + consts.RoomPropsShowIP: func(r *Room, v string) { + r.EnableShowIP = v == "on" + }, + consts.RoomPropsJokerAsTarget: func(r *Room, v string) { + r.EnableJokerAsTarget = v == "on" + }, +} + +func init() { + async.Async(func() { + loopCount := 0 + for { + loopCount++ + if loopCount%60 == 0 { + log.Infof("[database.init] Room cleanup loop count: %d (running for %d hours)\n", loopCount, loopCount/60) + } + time.Sleep(1 * time.Minute) + rooms.Foreach(func(e *hashmap.Entry) { + roomCancel(e.Value().(*Room)) + }) + } + }) +} + +func Connected(conn *network.Conn, info *modelx.AuthInfo) *Player { + player := &Player{ + ID: conn.ID(), + IP: conn.IP(), + Name: strings.Desensitize(info.Name), + Amount: 2000, + } + player.Conn(conn) // 初始化play对象 + players.Set(conn.ID(), player) // 写入用户池 + connPlayers.Set(conn.ID(), player) // 写入连接用户池 + return player +} + +func CreateRoom(creator int64, t int) *Room { + room := &Room{ + ID: atomic.AddInt64(&roomIds, 1), + Type: t, + State: consts.RoomStateWaiting, + Creator: creator, + ActiveTime: time.Now(), + MaxPlayers: consts.MaxPlayers, + EnableLandlord: true, + EnableChat: true, + EnableShowIP: false, + } + switch room.Type { + case consts.GameTypeLaiZi: + room.EnableLaiZi = true + case consts.GameTypeSkill: + room.EnableLaiZi = true + room.EnableDontShuffle = true + room.EnableSkill = true + room.EnableLandlord = false + case consts.GameTypeRunFast: + room.MaxPlayers = 3 + room.EnableLaiZi = false + room.EnableLandlord = false + room.EnableDontShuffle = true + case consts.GameTypeTexas: + room.MaxPlayers = 10 + case consts.GameTypeLiar: + room.MaxPlayers = 4 + room.EnableJokerAsTarget = true + } + roomPlayers.Set(room.ID, map[int64]bool{}) + roomSpectators.Set(room.ID, map[int64]int{}) + rooms.Set(room.ID, room) + return room +} + +func deleteRoom(room *Room) { + if room != nil { + rooms.Del(room.ID) + roomPlayers.Del(room.ID) + roomSpectators.Del(room.ID) + if room.Game != nil { + room.Game.Clean() + } + } +} + +func GetRooms() []*Room { + list := make([]*Room, 0) + rooms.Foreach(func(e *hashmap.Entry) { + list = append(list, e.Value().(*Room)) + }) + sort.Slice(list, func(i, j int) bool { + return list[i].ID < list[j].ID + }) + return list +} + +func GetRoom(roomId int64) *Room { + return getRoom(roomId) +} + +func getRoom(roomId int64) *Room { + if v, ok := rooms.Get(roomId); ok { + return v.(*Room) + } + return nil +} + +func getPlayer(playerId int64) *Player { + if v, ok := players.Get(playerId); ok { + return v.(*Player) + } + return nil +} + +func SetRoomProps(room *Room, k, v string) { + // 根据房间类型限制可设置的属性 + allowedProps := getAllowedPropsByGameType(room.Type) + + // 检查属性是否允许设置 + if !allowedProps[k] { + return // 不允许的属性直接返回,不执行设置 + } + + if setter, ok := roomPropsSetter[k]; ok { + setter(room, v) + } +} + +// 根据游戏类型返回允许设置的属性列表 +func getAllowedPropsByGameType(gameType int) map[string]bool { + switch gameType { + case consts.GameTypeLiar: + // 对于骗子酒馆,只允许设置指示牌规则和显示IP + return map[string]bool{ + consts.RoomPropsJokerAsTarget: true, + consts.RoomPropsShowIP: true, + consts.RoomPropsPassword: true, + } + case consts.GameTypeUno, consts.GameTypeMahjong: + // 对于Uno和麻将,允许设置玩家数量和显示IP + return map[string]bool{ + consts.RoomPropsPlayerNum: true, + consts.RoomPropsShowIP: true, + consts.RoomPropsPassword: true, + } + case consts.GameTypeTexas: + // 对于德州扑克,允许设置玩家数量和显示IP + return map[string]bool{ + consts.RoomPropsPlayerNum: true, + consts.RoomPropsShowIP: true, + consts.RoomPropsPassword: true, + } + default: + // 其他游戏类型允许所有常规属性 + return map[string]bool{ + consts.RoomPropsLaiZi: true, + consts.RoomPropsDotShuffle: true, + consts.RoomPropsSkill: true, + consts.RoomPropsPassword: true, + consts.RoomPropsPlayerNum: true, + consts.RoomPropsChat: true, + consts.RoomPropsShowIP: true, + } + } +} + +func getRoomPlayers(roomId int64) map[int64]bool { + if v, ok := roomPlayers.Get(roomId); ok { + return v.(map[int64]bool) + } + return nil +} + +func getRoomSpectators(roomId int64) map[int64]int { + if v, ok := roomSpectators.Get(roomId); ok { + return v.(map[int64]int) + } + return nil +} + +func IsValidPlayer(roomId, playerId int64) bool { + room := getRoom(roomId) + if room != nil { + room.Lock() + defer room.Unlock() + playersIds := getRoomPlayers(roomId) + if playersIds != nil { + _, exists := playersIds[playerId] + if exists { + return true + } + } + spectatorsIds := getRoomSpectators(roomId) + if spectatorsIds != nil { + _, exists := spectatorsIds[playerId] + if exists { + return true + } + } + return false + } + return false +} + +// 加入房间 +func JoinRoom(roomId, playerId int64) error { + // 资源检查 + player := getPlayer(playerId) + if player == nil { + return consts.ErrorsExist + } + room := getRoom(roomId) + if room == nil { + return consts.ErrorsRoomInvalid + } + + // 加锁防止并发异常 + room.Lock() + defer room.Unlock() + + if hasKicked(roomId, playerId) { + return consts.ErrorsJoinFailForKicked + } + + room.ActiveTime = time.Now() + + //房间人数及状态检查 + if room.Players >= room.MaxPlayers || room.State == consts.RoomStateRunning { + spectatorsIds := getRoomSpectators(roomId) + spectatorsIds[playerId] = len(spectatorsIds) + player.RoomID = roomId + player.Role = RoleSpectator + } else { + playersIds := getRoomPlayers(roomId) + playersIds[playerId] = true + room.Players++ + player.RoomID = roomId + player.Role = RolePlayer + if room.Creator == playerId { + player.Role = RoleOwner + } + } + + return nil +} + +func LeaveRoom(roomId, playerId int64) { + room := getRoom(roomId) + if room != nil { + room.Lock() + defer room.Unlock() + leaveRoom(room, getPlayer(playerId)) + } +} + +func Backfill(roomId int64) *Player { + room := getRoom(roomId) + if room != nil { + room.Lock() + defer room.Unlock() + return backfill(room) + } + return nil +} + +func backfill(room *Room) *Player { + if room.Players >= room.MaxPlayers { + return nil + } + spectatorsIds := getRoomSpectators(room.ID) + if len(spectatorsIds) == 0 { + return nil + } + spectators := make([]struct { + id int64 + index int + }, 0) + + for id, index := range spectatorsIds { + spectators = append(spectators, struct { + id int64 + index int + }{id: id, index: index}) + } + sort.Slice(spectators, func(i, j int) bool { + return spectators[i].index < spectators[j].index + }) + + playerId := spectators[0].id + + delete(spectatorsIds, playerId) + playersIds := getRoomPlayers(room.ID) + playersIds[playerId] = true + room.Players++ + player := getPlayer(playerId) + if player != nil { + player.Role = RolePlayer + } + return player +} + +func Kicking(roomId, playerId int64) { + room := getRoom(roomId) + if room != nil { + room.Lock() + defer room.Unlock() + leaveRoom(room, getPlayer(playerId)) + + kickedPlayers, ok := roomKickedPlayers.Get(roomId) + if !ok { + kickedPlayers = map[int64]bool{} + roomKickedPlayers.Set(roomId, kickedPlayers) + } + kickedPlayers.(map[int64]bool)[playerId] = true + } +} + +func hasKicked(roomId, playerId int64) bool { + kickedPlayers, ok := roomKickedPlayers.Get(roomId) + if !ok { + return false + } + _, exists := kickedPlayers.(map[int64]bool)[playerId] + return exists +} + +func leaveRoom(room *Room, player *Player) { + if room == nil || player == nil { + return + } + room.ActiveTime = time.Now() + playersIds := getRoomPlayers(room.ID) + if _, ok := playersIds[player.ID]; ok { + room.Players-- + player.RoomID = 0 + player.Role = "" + delete(playersIds, player.ID) + if len(playersIds) > 0 && room.Creator == player.ID { + for k := range playersIds { + room.Creator = k + if p := getPlayer(k); p != nil { + p.Role = RoleOwner + } + break + } + } + } + spectatorsIds := getRoomSpectators(room.ID) + if _, ok := spectatorsIds[player.ID]; ok { + player.RoomID = 0 + player.Role = "" + delete(spectatorsIds, player.ID) + } + if len(playersIds) == 0 && len(spectatorsIds) == 0 { + deleteRoom(room) + } +} + +func roomCancel(room *Room) { + if room.ActiveTime.Add(24 * time.Hour).Before(time.Now()) { + log.Infof("room %d is timeout 24 hours, removed.\n", room.ID) + deleteRoom(room) + return + } + living := false + playerIds := getRoomPlayers(room.ID) + for id := range playerIds { + if getPlayer(id).online { + living = true + break + } + } + spectatorIds := getRoomSpectators(room.ID) + for id := range spectatorIds { + if getPlayer(id).online { + living = true + break + } + } + if !living { + log.Infof("room %d is not living, removed.\n", room.ID) + deleteRoom(room) + } +} + +func RoomPlayers(roomId int64) map[int64]bool { + return getRoomPlayers(roomId) +} + +func RoomSpectators(roomId int64) map[int64]int { + return getRoomSpectators(roomId) +} + +func broadcast(room *Room, msg string, exclude ...int64) { + room.ActiveTime = time.Now() + excludeSet := map[int64]bool{} + for _, exc := range exclude { + excludeSet[exc] = true + } + for playerId := range getRoomPlayers(room.ID) { + if player := getPlayer(playerId); player != nil && !excludeSet[playerId] { + _ = player.WriteString(">> " + msg) + } + } + for playerId := range getRoomSpectators(room.ID) { + if player := getPlayer(playerId); player != nil && !excludeSet[playerId] { + _ = player.WriteString(">> " + msg) + } + } +} + +func Broadcast(roomId int64, msg string, exclude ...int64) { + room := getRoom(roomId) + if room == nil { + return + } + broadcast(room, msg, exclude...) +} + +func BroadcastChat(player *Player, msg string, exclude ...int64) { + log.Infof("chat msg, player %s[%d] %s say: %s\n", player.Name, player.ID, player.IP, stringx.TrimSpace(msg)) + Broadcast(player.RoomID, strings.Desensitize(msg), exclude...) +} + +func BroadcastObject(roomId int64, object interface{}, exclude ...int64) { + room := getRoom(roomId) + if room == nil { + return + } + room.Lock() + defer room.Unlock() + excludeSet := map[int64]bool{} + for _, exc := range exclude { + excludeSet[exc] = true + } + msg := json.Marshal(object) + playerIds := getRoomPlayers(roomId) + for playerId := range playerIds { + if player := getPlayer(playerId); player != nil && !excludeSet[playerId] { + _ = player.WriteString(string(msg)) + } + } +} + +func GetPlayer(playerId int64) *Player { + return getPlayer(playerId) +} diff --git a/server/database/liar.go b/server/database/liar.go new file mode 100644 index 0000000..c3236d6 --- /dev/null +++ b/server/database/liar.go @@ -0,0 +1,27 @@ +package database + +import ( + "sync" + + "github.com/ratel-online/core/model" +) + +type Liar struct { + sync.Mutex + Room *Room `json:"room"` + PlayerIDs []int64 `json:"playerIds"` + Bullets map[int64]int `json:"bullets"` + Bong map[int64]int `json:"bong"` + Pokers model.Pokers `json:"pokers"` + States map[int64]chan int `json:"states"` + Hands map[int64]model.Pokers `json:"hands"` + Target *model.Poker `json:"target"` + Alive map[int64]bool `json:"alive"` + LastPlayerID int64 `json:"lastPlayerId"` + LastPokers model.Pokers `json:"lastPokers"` + Supervisors map[int64]bool `json:"supervisors"` + AllowJokers bool `json:"allowJokers"` +} + +func (l *Liar) Clean() { +} diff --git a/server/database/mahjong.go b/server/database/mahjong.go new file mode 100644 index 0000000..7fc719f --- /dev/null +++ b/server/database/mahjong.go @@ -0,0 +1,209 @@ +package database + +import ( + "bytes" + "fmt" + "sort" + "strconv" + "strings" + + "github.com/feel-easy/mahjong/card" + "github.com/feel-easy/mahjong/consts" + "github.com/feel-easy/mahjong/event" + "github.com/feel-easy/mahjong/game" + "github.com/feel-easy/mahjong/tile" + "github.com/ratel-online/core/log" + rconsts "github.com/ratel-online/server/consts" +) + +type Mahjong struct { + Room *Room `json:"room"` + PlayerIDs []int `json:"playerIds"` + States map[int]chan int `json:"states"` + Game *game.Game `json:"game"` +} + +func (game *Mahjong) Clean() { + if game != nil { + for _, state := range game.States { + close(state) + } + } +} + +type OP struct { + operation int + tiles []int +} + +func circled(n int) string { + if n >= 1 && n <= 20 { + return string(rune(0x2460 + n - 1)) + } + return strconv.Itoa(n) +} + +type MahjongPlayer struct { + ID int64 `json:"id"` + Name string `json:"name"` +} + +func NewPlayer(user *Player) *MahjongPlayer { + return &MahjongPlayer{ + ID: user.ID, + Name: user.Name, + } +} + +func (p *MahjongPlayer) PlayerID() int { + return int(p.ID) +} + +func (p *MahjongPlayer) NickName() string { + return p.Name +} + +func (mp *MahjongPlayer) OnPlayTile(payload event.PlayTilePayload) { + p := GetPlayer(mp.ID) + p.WriteString(fmt.Sprintf("You play %s ! \n", tile.Tile(payload.Tile))) + Broadcast(p.RoomID, fmt.Sprintf("%s PlayTile %s !\n", payload.PlayerName, tile.Tile(payload.Tile)), p.ID) +} + +func (mp *MahjongPlayer) Take(tiles []int, gameState game.State) (int, []int, error) { + p := GetPlayer(mp.ID) + Broadcast(p.RoomID, fmt.Sprintf("It's %s take mahjong! \n", p.Name), p.ID) + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("It's your take mahjong, %s! \n", p.Name)) + buf.WriteString(gameState.String()) + p.WriteString(buf.String()) + askBuf := bytes.Buffer{} + tileOptions := make(map[string]*OP) + labelCounter := 1 + if pvs, ok := gameState.SpecialPrivileges[int(p.ID)]; ok { + for _, pv := range pvs { + switch pv { + case consts.GANG: + askBuf.WriteString("You can 杠!!!\n") + label := strconv.Itoa(labelCounter) + ts := []int{gameState.LastPlayedTile, gameState.LastPlayedTile, gameState.LastPlayedTile} + tileOptions[label] = &OP{ + operation: consts.GANG, + tiles: append(ts, gameState.LastPlayedTile), + } + askBuf.WriteString(fmt.Sprintf("%s. %s \n", circled(labelCounter), tile.ToTileString(ts))) + labelCounter++ + case consts.PENG: + askBuf.WriteString("You can 碰!!!\n") + label := strconv.Itoa(labelCounter) + ts := []int{gameState.LastPlayedTile, gameState.LastPlayedTile} + tileOptions[label] = &OP{ + operation: consts.PENG, + tiles: append(ts, gameState.LastPlayedTile), + } + askBuf.WriteString(fmt.Sprintf("%s. %s \n", circled(labelCounter), tile.ToTileString(ts))) + labelCounter++ + case consts.CHI: + askBuf.WriteString("You can 吃!!!\n") + for _, ts := range card.CanChiTiles(tiles, gameState.LastPlayedTile) { + label := strconv.Itoa(labelCounter) + tileOptions[label] = &OP{ + operation: consts.CHI, + tiles: append(ts, gameState.LastPlayedTile), + } + askBuf.WriteString(fmt.Sprintf("%s. %s \n", circled(labelCounter), tile.ToTileString(ts))) + labelCounter++ + } + } + } + } + label := strconv.Itoa(labelCounter) + askBuf.WriteString(fmt.Sprintf("%s. %s \n", circled(labelCounter), "no")) + tileOptions[label] = &OP{ + operation: 0, + tiles: []int{}, + } + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[MahjongPlayer.Take] Player %d loop count: %d\n", mp.ID, loopCount) + } + p = getPlayer(p.ID) + p.WriteString(askBuf.String()) + selectedLabel, err := p.AskForString(consts.PlayMahjongTimeout) + if err != nil { + switch err { + case rconsts.ErrorsExist: + p.WriteString("Don't quit a good game!\n") + selectedLabel = "E" + case rconsts.ErrorsTimeout: + // Default to "no" action (skip peng/chi/gang) on timeout + selectedLabel = label + default: + return 0, nil, err + } + } + selected, found := tileOptions[strings.ToUpper(selectedLabel)] + if !found { + BroadcastChat(p, fmt.Sprintf("%s say: %s\n", p.Name, selectedLabel)) + continue + } + return selected.operation, selected.tiles, nil + } +} + +func (mp *MahjongPlayer) Play(tiles []int, gameState game.State) (int, error) { + p := GetPlayer(mp.ID) + Broadcast(p.RoomID, fmt.Sprintf("It's %s turn! \n", p.Name), p.ID) + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("It's your turn, %s! \n", p.Name)) + buf.WriteString(gameState.String()) + p.WriteString(buf.String()) + askBuf := bytes.Buffer{} + askBuf.WriteString("Select a tile to play:\n") + tileOptions := make(map[string]int) + sort.Ints(tiles) + for idx, i := range tiles { + label := strconv.Itoa(idx + 1) + tileOptions[label] = i + askBuf.WriteString(fmt.Sprintf("%s. %-6s", circled(idx+1), tile.Tile(i).String())) + if (idx+1)%6 == 0 { + askBuf.WriteString("\n") + } else { + askBuf.WriteString(" ") + } + } + askBuf.WriteString("\n") + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[MahjongPlayer.Play] Player %d loop count: %d\n", mp.ID, loopCount) + } + p = GetPlayer(p.ID) + p.WriteString(askBuf.String()) + selectedLabel, err := p.AskForString(rconsts.PlayMahjongTimeout) + if err != nil { + switch err { + case rconsts.ErrorsExist: + p.WriteString("Don't quit a good game!\n") + selectedLabel = "E" + case rconsts.ErrorsTimeout: + // Default to the last tile (least likely to be useful) on timeout + selectedLabel = strconv.Itoa(len(tiles)) + default: + return 0, err + } + } + selectedCard, found := tileOptions[strings.ToUpper(selectedLabel)] + if !found { + BroadcastChat(p, fmt.Sprintf("%s say: %s\n", p.Name, selectedLabel)) + continue + } + mp.OnPlayTile(event.PlayTilePayload{ + PlayerName: p.Name, + Tile: selectedCard, + }) + return selectedCard, nil + } +} diff --git a/server/database/model.go b/server/database/model.go new file mode 100644 index 0000000..c4baef5 --- /dev/null +++ b/server/database/model.go @@ -0,0 +1,333 @@ +package database + +import ( + "fmt" + "strconv" + "strings" + "sync" + "time" + + "github.com/ratel-online/core/log" + "github.com/ratel-online/core/model" + "github.com/ratel-online/core/network" + "github.com/ratel-online/core/protocol" + "github.com/ratel-online/core/util/arrays" + "github.com/ratel-online/core/util/json" + "github.com/ratel-online/core/util/poker" + "github.com/ratel-online/server/consts" +) + +const initialRune = 'A' + +type runeSequence struct { + currentRune rune +} + +func (s *runeSequence) next() rune { + if s.currentRune == 0 { + s.currentRune = initialRune + } + currentRune := s.currentRune + s.currentRune++ + return currentRune +} + +type Role string + +const ( + RolePlayer Role = "player" + RoleOwner Role = "owner" + RoleSpectator Role = "spectator" +) + +type Player struct { + ID int64 `json:"id"` + IP string `json:"ip"` + Name string `json:"name"` + Mode int `json:"mode"` + Type int `json:"type"` + Amount uint `json:"amount"` + RoomID int64 `json:"roomId"` + Role Role `json:"role"` + + conn *network.Conn + data chan *protocol.Packet + read bool + state consts.StateID + online bool +} + +func (p *Player) Write(bytes []byte) error { + return p.conn.Write(protocol.Packet{ + Body: bytes, + }) +} + +func (p *Player) IsOnline() bool { + return p.online +} + +func (p *Player) Offline() { + p.online = false + _ = p.conn.Close() + close(p.data) + room := getRoom(p.RoomID) + if room != nil { + room.Lock() + defer room.Unlock() + broadcast(room, fmt.Sprintf("%s lost connection! \n", p.Name)) + if room.State == consts.RoomStateWaiting { + leaveRoom(room, p) + } + roomCancel(room) + } +} + +func (p *Player) Listening() error { + loopCount := 0 + for { + loopCount++ + if loopCount%1000 == 0 { + log.Infof("[Player.Listening] Player %d loop count: %d, online: %v\n", p.ID, loopCount, p.online) + } + pack, err := p.conn.Read() + if err != nil { + log.Error(err) + return err + } + if p.read { + p.data <- pack + } + } +} + +// 向客户端发生消息 +func (p *Player) WriteString(data string) error { + time.Sleep(30 * time.Millisecond) + return p.conn.Write(protocol.Packet{ + Body: []byte(data), + }) +} + +func (p *Player) WriteObject(data interface{}) error { + return p.conn.Write(protocol.Packet{ + Body: json.Marshal(data), + }) +} + +func (p *Player) WriteError(err error) error { + if err == consts.ErrorsExist { + return err + } + return p.conn.Write(protocol.Packet{ + Body: []byte(err.Error() + "\n"), + }) +} + +func (p *Player) AskForPacket(timeout ...time.Duration) (*protocol.Packet, error) { + p.StartTransaction() + defer p.StopTransaction() + return p.askForPacket(timeout...) +} + +func (p *Player) askForPacket(timeout ...time.Duration) (*protocol.Packet, error) { + var packet *protocol.Packet + if len(timeout) > 0 { + select { + case packet = <-p.data: + case <-time.After(timeout[0]): + return nil, consts.ErrorsTimeout + } + } else { + packet = <-p.data + } + if packet == nil { + return nil, consts.ErrorsChanClosed + } + single := strings.ToLower(packet.String()) + if single == "exit" || single == "e" { + return nil, consts.ErrorsExist + } + return packet, nil +} + +func (p *Player) AskForInt(timeout ...time.Duration) (int, error) { + packet, err := p.AskForPacket(timeout...) + if err != nil { + return 0, err + } + return packet.Int() +} + +func (p *Player) AskForInt64(timeout ...time.Duration) (int64, error) { + packet, err := p.AskForPacket(timeout...) + if err != nil { + return 0, err + } + return packet.Int64() +} + +func (p *Player) AskForString(timeout ...time.Duration) (string, error) { + packet, err := p.AskForPacket(timeout...) + if err != nil { + return "", err + } + return packet.String(), nil +} + +func (p *Player) AskForStringWithoutTransaction(timeout ...time.Duration) (string, error) { + packet, err := p.askForPacket(timeout...) + if err != nil { + return "", err + } + return packet.String(), nil +} + +func (p *Player) StartTransaction() { + p.read = true + _ = p.WriteString(consts.IsStart) +} + +func (p *Player) StopTransaction() { + p.read = false + _ = p.WriteString(consts.IsStop) +} + +func (p *Player) State(s consts.StateID) { + p.state = s +} + +func (p *Player) GetState() consts.StateID { + return p.state +} + +func (p *Player) Conn(conn *network.Conn) { + p.conn = conn + p.data = make(chan *protocol.Packet, 8) + p.online = true +} + +func (p Player) Model() model.Player { + modelPlayer := model.Player{ + ID: p.ID, + Name: p.Name, + } + room := getRoom(p.RoomID) + if room != nil && room.Game != nil { + game := room.Game.(*Game) + modelPlayer.Pokers = len(game.Pokers[p.ID]) + modelPlayer.Group = game.Groups[p.ID] + } + return modelPlayer +} + +func (p Player) String() string { + return fmt.Sprintf("%s[%d]", p.Name, p.ID) +} + +type RoomGame interface { + Clean() +} + +type Room struct { + sync.Mutex + + ID int64 `json:"id"` + Type int `json:"type"` + Game RoomGame `json:"gameId"` + State int `json:"state"` + Players int `json:"players"` + Banker int `json:"banker"` + Robots int `json:"robots"` + Creator int64 `json:"creator"` + ActiveTime time.Time `json:"activeTime"` + MaxPlayers int `json:"maxPlayers"` + Password string `json:"password"` + EnableChat bool `json:"enableChat"` + EnableLaiZi bool `json:"enableLaiZi"` + EnableSkill bool `json:"enableSkill"` + EnableLandlord bool `json:"enableLandlord"` + EnableDontShuffle bool `json:"enableDontShuffle"` + EnableShowIP bool `json:"enableShowIP"` + EnableJokerAsTarget bool `json:"enableJokerAsTarget"` +} + +func (r *Room) Model() model.Room { + return model.Room{ + ID: r.ID, + Type: r.Type, + TypeDesc: consts.GameTypes[r.Type], + Players: r.Players, + State: r.State, + StateDesc: consts.RoomStates[r.State], + Creator: r.Creator, + } +} + +type Game struct { + Room *Room `json:"room"` + Players []int64 `json:"players"` + Groups map[int64]int `json:"groups"` + States map[int64]chan int `json:"states"` + Pokers map[int64]model.Pokers `json:"pokers"` + Universals []int `json:"universals"` + Decks int `json:"decks"` + Additional model.Pokers `json:"pocket"` + Multiple int `json:"multiple"` + FirstPlayer int64 `json:"firstPlayer"` + LastPlayer int64 `json:"lastPlayer"` + Robs []int64 `json:"robs"` + FirstRob int64 `json:"firstRob"` + LastRob int64 `json:"lastRob"` + FinalRob bool `json:"finalRob"` + LastFaces *model.Faces `json:"lastFaces"` + LastPokers model.Pokers `json:"lastPokers"` + Mnemonic map[int]int `json:"mnemonic"` + Skills map[int64]int `json:"skills"` + PlayTimes map[int64]int `json:"playTimes"` + PlayTimeOut map[int64]time.Duration `json:"playTimeOut"` + Rules poker.Rules `json:"rules"` + Discards model.Pokers `json:"discards"` +} + +func (game *Game) Clean() { + if game != nil { + for _, state := range game.States { + close(state) + } + } +} + +func (game *Game) Start() { + +} + +func (g Game) NextPlayer(curr int64) int64 { + idx := arrays.IndexOf(g.Players, curr) + return g.Players[(idx+1)%len(g.Players)] +} + +func (g Game) PrevPlayer(curr int64) int64 { + idx := arrays.IndexOf(g.Players, curr) + return g.Players[(idx+len(g.Players))%len(g.Players)] +} + +func (g Game) IsTeammate(player1, player2 int64) bool { + return g.Groups[player1] == g.Groups[player2] +} + +func (g Game) IsLandlord(playerId int64) bool { + return g.Groups[playerId] == 1 +} + +func (g Game) Team(playerId int64) string { + if !g.Room.EnableLandlord { + return "team" + strconv.Itoa(g.Groups[playerId]) + } else { + if !g.IsLandlord(playerId) { + return "peasant" + } else { + return "landlord" + } + } +} diff --git a/server/database/texas.go b/server/database/texas.go new file mode 100644 index 0000000..f5d8ccc --- /dev/null +++ b/server/database/texas.go @@ -0,0 +1,134 @@ +package database + +import ( + "github.com/ratel-online/core/model" +) + +type Texas struct { + Room *Room `json:"room"` + Players []*TexasPlayer `json:"players"` + Pot uint `json:"pot"` + BB int `json:"bb"` + SB int `json:"sb"` + Pool model.Pokers `json:"pool"` + Board model.Pokers `json:"board"` + MaxBetAmount uint `json:"maxBetAmount"` + MaxBetPlayer *TexasPlayer `json:"maxBetPlayer"` + Round string `json:"round"` + Folded int `json:"folded"` + AllIn int `json:"allIn"` +} + +func (g *Texas) Clean() { + if g != nil { + for _, p := range g.Players { + close(p.State) + } + } +} + +func (g *Texas) NextPlayer(id int64) *TexasPlayer { + if g == nil || len(g.Players) == 0 { + return nil + } + idx := -1 + for i, a := range g.Players { + if a.ID == id { + idx = i + break + } + } + if idx == -1 { + return g.Players[0] + } + return g.Players[(idx+1)%len(g.Players)] +} + +func (g *Texas) Player(id int64) *TexasPlayer { + for _, p := range g.Players { + if p.ID == id { + return p + } + } + return nil +} + +func (g *Texas) SBPlayer() *TexasPlayer { + return g.Players[g.SB] +} + +func (g *Texas) BBPlayer() *TexasPlayer { + return g.Players[g.BB] +} + +func (g *Texas) Bet(player *TexasPlayer, amount uint) { + if amount > 0 { + player.Bet(amount) + g.Pot += amount + if player.Amount() == 0 { + player.AllIn = true + g.AllIn++ + } + } + if g.MaxBetPlayer == nil { + g.MaxBetPlayer = player + } + if player.Bets > g.MaxBetAmount { + g.MaxBetAmount = player.Bets + g.MaxBetPlayer = player + } +} + +func (g *Texas) RoundEnd(currentPlayerId int64) bool { + if g.AllIn == len(g.Players) || + g.AllIn+g.Folded == len(g.Players) || + (g.MaxBetPlayer != nil && g.MaxBetPlayer.ID == currentPlayerId) { + return true + } + // all players except one folded or allin + if g.AllIn+g.Folded == len(g.Players)-1 { + isEnd := true + for _, p := range g.Players { + if p.Folded { + continue + } + if p.Bets != g.MaxBetAmount { + isEnd = false + break + } + } + return isEnd + } + return false +} + +type TexasPlayer struct { + ID int64 `json:"id"` + Name string `json:"name"` + State chan int `json:"state"` + Hand model.Pokers `json:"hand"` + Bets uint `json:"bets"` + Folded bool `json:"folded"` + AllIn bool `json:"allIn"` +} + +func (p *TexasPlayer) Reset() { + p.Bets = 0 + p.Folded = false + p.AllIn = false + p.Hand = nil + p.State = make(chan int, 1) +} + +func (p *TexasPlayer) Amount() uint { + return GetPlayer(p.ID).Amount +} + +func (p *TexasPlayer) Bet(amount uint) { + p.Bets += amount + GetPlayer(p.ID).Amount -= amount +} + +func (p *TexasPlayer) Add(amount uint) { + GetPlayer(p.ID).Amount += amount +} diff --git a/server/database/uno.go b/server/database/uno.go new file mode 100644 index 0000000..d07b60d --- /dev/null +++ b/server/database/uno.go @@ -0,0 +1,184 @@ +package database + +import ( + "bytes" + "fmt" + "strings" + + "github.com/feel-easy/uno/card" + "github.com/feel-easy/uno/card/color" + "github.com/feel-easy/uno/event" + "github.com/feel-easy/uno/game" + "github.com/ratel-online/core/log" + "github.com/ratel-online/server/consts" +) + +type UnoGame struct { + Room *Room `json:"room"` + Players []int `json:"players"` + States map[int]chan int `json:"states"` + Game *game.Game `json:"game"` +} + +func (ug *UnoGame) HavePlay(player *Player) bool { + for _, id := range ug.Players { + if id == int(player.ID) && player.online { + return true + } + } + return false +} + +func (un *UnoGame) NeedExit() bool { + return un.Room.Players <= 1 +} + +func (un *UnoGame) Clean() { + if un != nil { + for _, state := range un.States { + close(state) + } + } +} + +type UnoPlayer struct { + ID int `json:"id"` + Name string `json:"name"` +} + +func NewUnoPlayer(p *Player) game.Player { + return &UnoPlayer{ + ID: int(p.ID), + Name: p.Name, + } +} + +func (up *UnoPlayer) PlayerID() int { + return up.ID +} + +func (up *UnoPlayer) NickName() string { + return up.Name +} + +func contains(cards []card.Card, searchedCard card.Card) bool { + for _, card := range cards { + if card.Equal(searchedCard) { + return true + } + } + return false +} + +func (up *UnoPlayer) NotifyCardsDrawn(cards []card.Card) { + p := getPlayer(int64(up.ID)) + getPlayer(p.ID).WriteString(fmt.Sprintf("You drew %s!\n", cards)) +} + +func (up *UnoPlayer) NotifyNoMatchingCardsInHand(lastPlayedCard card.Card, hand []card.Card) { + p := getPlayer(int64(up.ID)) + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("%s, none of your cards match %s! \n", p.Name, lastPlayedCard)) + buf.WriteString(fmt.Sprintf("Your hand is %s \n", hand)) + getPlayer(p.ID).WriteString(buf.String()) +} + +func (up *UnoPlayer) OnFirstCardPlayed(payload event.FirstCardPlayedPayload) { + p := getPlayer(int64(up.ID)) + Broadcast(p.RoomID, fmt.Sprintf("First card is %s\n", payload.Card)) +} + +func (up *UnoPlayer) OnCardPlayed(payload event.CardPlayedPayload) { + p := getPlayer(int64(up.ID)) + Broadcast(p.RoomID, fmt.Sprintf("%s played %s!\n", payload.PlayerName, payload.Card)) +} + +func (up *UnoPlayer) OnColorPicked(payload event.ColorPickedPayload) { + p := getPlayer(int64(up.ID)) + Broadcast(p.RoomID, fmt.Sprintf("%s picked color %s!\n", payload.PlayerName, payload.Color)) +} + +func (up *UnoPlayer) OnPlayerPassed(payload event.PlayerPassedPayload) { + p := getPlayer(int64(up.ID)) + Broadcast(p.RoomID, fmt.Sprintf("%s passed!\n", payload.PlayerName)) +} + +func (up *UnoPlayer) PickColor(gameState game.State) color.Color { + p := getPlayer(int64(up.ID)) + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[UnoPlayer.PickColor] Player %d loop count: %d\n", up.ID, loopCount) + } + p = getPlayer(p.ID) + p.WriteString(fmt.Sprintf( + "Select a color: %s, %s, %s or %s ? \n", + color.Red, + color.Yellow, + color.Green, + color.Blue, + )) + colorName, err := p.AskForString(consts.PlayTimeout) + if err != nil { + if err == consts.ErrorsTimeout { + return color.Red + } + p.WriteString(fmt.Sprintf("Unknown color '%s' \n", colorName)) + continue + } + chosenColor, err := color.ByName(strings.ToLower(colorName)) + if err != nil { + p.WriteString(fmt.Sprintf("Unknown color '%s' \n", colorName)) + continue + } + return chosenColor + } +} + +func (up *UnoPlayer) Play(playableCards []card.Card, gameState game.State) (card.Card, error) { + p := getPlayer(int64(up.ID)) + Broadcast(p.RoomID, fmt.Sprintf("It's %s turn! \n", p.Name), p.ID) + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("It's your turn, %s! \n", p.Name)) + buf.WriteString(gameState.String()) + p.WriteString(buf.String()) + runeSequence := runeSequence{} + cardOptions := make(map[string]card.Card) + for _, card := range playableCards { + label := string(runeSequence.next()) + cardOptions[label] = card + } + cardSelectionLines := []string{"Select a card to play:"} + for label, card := range cardOptions { + cardSelectionLines = append(cardSelectionLines, fmt.Sprintf("%s %s", label, card)) + } + cardSelectionMessage := strings.Join(cardSelectionLines, " \n ") + " \n " + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[UnoPlayer.Play] Player %d loop count: %d\n", up.ID, loopCount) + } + p = getPlayer(p.ID) + p.WriteString(cardSelectionMessage) + selectedLabel, err := p.AskForString(consts.PlayTimeout) + if err != nil { + if err == consts.ErrorsTimeout { + selectedLabel = "A" + } else { + return nil, err + } + } + selectedCard, found := cardOptions[strings.ToUpper(selectedLabel)] + if !found { + BroadcastChat(p, fmt.Sprintf("%s say: %s\n", p.Name, selectedLabel)) + continue + } + if !contains(playableCards, selectedCard) { + p.WriteString(fmt.Sprintf("Cheat detected! Card %s is not in %s's hand! \n", selectedCard, p.Name)) + continue + } + return selectedCard, nil + } +} diff --git a/server/demo.gif b/server/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..530fb465cee121cd81a0e8385800254dbece40fb GIT binary patch literal 324842 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 _{GcR>VlRe$QY(aP2rw6CJS|L%`G#-lUv>-0 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
    D##2E02NiA!%$P~1Uu02!kmDhBs-P(cZ5@rjxt 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≪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 X%GuTwFr9GI>dmhoO^u`=K;i!Y5pgl8 literal 0 HcmV?d00001 diff --git a/server/docker-compose.yaml b/server/docker-compose.yaml new file mode 100644 index 0000000..c6c3511 --- /dev/null +++ b/server/docker-compose.yaml @@ -0,0 +1,47 @@ +version: '3.8' + +services: + ratel-server: + build: + context: . + dockerfile: Dockerfile + image: ratel-server:latest + container_name: ratel-server + restart: unless-stopped + ports: + - "9998:9998" # WebSocket端口 + - "9999:9999" # TCP端口 + environment: + - TZ=Asia/Shanghai + # volumes: + # # 如果需要持久化日志,可以取消下面的注释 + # - ./logs:/app/logs + networks: + - ratel-network + command: ["./ratel-server", "-w", "9998", "-t", "9999"] + healthcheck: + test: ["CMD", "nc", "-z", "localhost", "9998"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 10s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + cpus: '2' + memory: 512M + reservations: + cpus: '0.5' + memory: 128M + +networks: + ratel-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 \ No newline at end of file diff --git a/server/docs/Docker部署指南.md b/server/docs/Docker部署指南.md new file mode 100644 index 0000000..a9c802c --- /dev/null +++ b/server/docs/Docker部署指南.md @@ -0,0 +1,323 @@ +# Ratel游戏服务器 Docker 部署指南 + +## 项目概述 + +Ratel是一个命令行在线多人棋牌游戏服务器,支持斗地主、跑得快、德州扑克等多种游戏模式。本文档提供了使用Docker和Docker Compose进行部署的完整方案。 + +### 系统架构 + +```mermaid +graph TD + A[用户] -->|TCP:9999| B[Ratel Server] + A -->|WebSocket:9998| B + B --> C[内存数据库] + B --> D[游戏逻辑] + D --> E[斗地主] + D --> F[跑得快] + D --> G[德州扑克] + D --> H[UNO] + D --> I[麻将] +``` + +## 快速开始 + +### 前置要求 + +- Docker 20.10+ +- Docker Compose 2.0+ 或 Docker 20.10+(内置 compose 插件) +- Git + +> 注意:新版 Docker 已内置 compose 插件,可以使用 `docker compose` 命令。Makefile 会自动检测并使用可用的命令。 + +### 快速部署 + +1. 克隆项目 +```bash +git clone https://github.com/ratel-online/server.git +cd server +``` + +2. 构建并启动服务 +```bash +docker-compose up -d +``` + +3. 查看服务状态 +```bash +docker-compose ps +docker-compose logs -f +``` + +4. 停止服务 +```bash +docker-compose down +``` + +## 部署方案详解 + +### 1. Dockerfile 说明 + +项目使用多阶段构建优化镜像大小: + +- **构建阶段**:使用 `golang:1.17-alpine` 作为基础镜像编译Go应用 +- **运行阶段**:使用 `alpine:latest` 作为最终镜像,只包含必要的运行时依赖 + +主要特性: +- 多阶段构建减小镜像体积 +- 使用非root用户运行提高安全性 +- 配置健康检查确保服务可用性 +- 设置时区为 Asia/Shanghai + +### 2. Docker Compose 配置 + +提供了两个配置文件: + +#### docker-compose.yaml(开发环境) +- 本地构建镜像 +- 资源限制较宽松 +- 适合开发测试使用 + +#### docker-compose.prod.yaml(生产环境) +- 使用预构建镜像 +- 严格的资源限制 +- 包含Nginx反向代理 +- 日志持久化配置 + +### 3. 端口说明 + +| 端口 | 协议 | 用途 | +|------|------|------| +| 9998 | WebSocket | 浏览器客户端连接 | +| 9999 | TCP | 命令行客户端连接 | +| 80 | HTTP | Nginx反向代理(可选) | +| 443 | HTTPS | Nginx SSL(可选) | + +## 生产环境部署 + +### 1. 构建生产镜像 + +```bash +# 构建镜像 +docker build -t ratel-server:latest . + +# 或者使用特定版本标签 +docker build -t ratel-server:v1.0.0 . +``` + +### 2. 使用生产配置启动 + +```bash +# 使用生产环境配置 +docker-compose -f docker-compose.prod.yaml up -d +``` + +### 3. 配置SSL证书(可选) + +如果需要HTTPS支持: + +```bash +# 创建SSL证书目录 +mkdir -p nginx/ssl + +# 将证书文件复制到目录 +cp your-cert.pem nginx/ssl/cert.pem +cp your-key.pem nginx/ssl/key.pem + +# 修改nginx/nginx.conf,启用HTTPS配置 +# 然后重启nginx服务 +docker-compose -f docker-compose.prod.yaml restart nginx +``` + +### 4. 日志管理 + +日志文件会保存在 `./logs` 目录: + +```bash +# 查看实时日志 +docker-compose -f docker-compose.prod.yaml logs -f ratel-server + +# 查看最近100行日志 +docker-compose -f docker-compose.prod.yaml logs --tail=100 ratel-server +``` + +## 性能优化 + +### 1. 资源配置 + +根据服务器配置调整资源限制: + +```yaml +deploy: + resources: + limits: + cpus: '4' # 最大CPU核心数 + memory: 1G # 最大内存 + reservations: + cpus: '1' # 保留CPU核心数 + memory: 256M # 保留内存 +``` + +### 2. 并发连接优化 + +对于高并发场景,可以调整系统参数: + +```bash +# 增加文件描述符限制 +echo "* soft nofile 65535" >> /etc/security/limits.conf +echo "* hard nofile 65535" >> /etc/security/limits.conf + +# 优化TCP参数 +sysctl -w net.core.somaxconn=65535 +sysctl -w net.ipv4.tcp_max_syn_backlog=65535 +``` + +## 监控和维护 + +### 1. 健康检查 + +服务配置了自动健康检查: + +```bash +# 手动检查服务健康状态 +docker-compose -f docker-compose.prod.yaml exec ratel-server nc -z localhost 9998 +``` + +### 2. 备份策略 + +由于游戏数据存储在内存中,重启会丢失数据。如需持久化,建议: + +1. 定期导出游戏状态(需要修改代码支持) +2. 使用外部数据库存储关键数据 +3. 实现分布式缓存方案 + +### 3. 更新部署 + +```bash +# 拉取最新代码 +git pull + +# 重新构建镜像 +docker-compose -f docker-compose.prod.yaml build + +# 滚动更新 +docker-compose -f docker-compose.prod.yaml up -d --no-deps --build ratel-server +``` + +## 故障排查 + +### 常见问题 + +1. **端口被占用** +```bash +# 检查端口占用 +netstat -tlnp | grep -E '9998|9999' + +# 修改docker-compose.yaml中的端口映射 +ports: + - "19998:9998" # 使用其他端口 + - "19999:9999" +``` + +2. **容器无法启动** +```bash +# 查看容器日志 +docker-compose logs ratel-server + +# 检查容器状态 +docker ps -a +``` + +3. **连接超时** +- 检查防火墙规则 +- 确认Docker网络配置 +- 验证健康检查状态 + +### 调试模式 + +```bash +# 进入容器调试 +docker-compose exec ratel-server sh + +# 查看进程状态 +ps aux + +# 测试网络连接 +nc -zv localhost 9998 +nc -zv localhost 9999 +``` + +## 安全建议 + +1. **网络隔离** + - 使用专用网络避免容器间不必要的通信 + - 限制外部访问,只暴露必要端口 + +2. **用户权限** + - 容器内使用非root用户运行 + - 限制文件系统权限 + +3. **资源限制** + - 设置CPU和内存限制防止资源耗尽 + - 配置日志轮转避免磁盘占满 + +4. **定期更新** + - 及时更新基础镜像 + - 关注安全公告 + +## 扩展部署 + +### 1. Kubernetes 部署 + +可以基于Docker镜像创建Kubernetes部署: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ratel-server +spec: + replicas: 3 + selector: + matchLabels: + app: ratel-server + template: + metadata: + labels: + app: ratel-server + spec: + containers: + - name: ratel-server + image: ratel-server:latest + ports: + - containerPort: 9998 + - containerPort: 9999 +``` + +### 2. Docker Swarm 部署 + +```bash +# 初始化Swarm +docker swarm init + +# 部署服务栈 +docker stack deploy -c docker-compose.prod.yaml ratel +``` + +## 总结 + +本部署方案提供了完整的Docker化部署流程,包括: + +- ✅ 多阶段构建优化镜像大小 +- ✅ 开发和生产环境配置分离 +- ✅ 健康检查和自动重启 +- ✅ 日志管理和资源限制 +- ✅ 可选的Nginx反向代理 +- ✅ 安全最佳实践 + +通过Docker部署可以实现: +- 快速部署和迁移 +- 环境一致性保证 +- 便捷的扩容和管理 +- 与CI/CD流程集成 + +如有问题或需要进一步的帮助,请参考项目的GitHub Issues或联系开发团队。 \ No newline at end of file diff --git a/server/docs/快速开始.md b/server/docs/快速开始.md new file mode 100644 index 0000000..d03f1ea --- /dev/null +++ b/server/docs/快速开始.md @@ -0,0 +1,85 @@ +# Ratel游戏服务器 - 快速开始 + +## 一分钟部署 + +### 1. 克隆项目 +```bash +git clone https://github.com/ratel-online/server.git +cd server +``` + +### 2. 启动服务 +```bash +# 使用 Make 命令(推荐,自动检测 docker-compose 或 docker compose) +make run + +# 或手动使用 Docker Compose +docker compose up -d # 新版 Docker +# 或 +docker-compose up -d # 独立安装的 docker-compose +``` + +### 3. 访问服务 +- WebSocket客户端:ws://localhost:9998 +- TCP客户端:localhost:9999 +- 浏览器访问:http://ratel.isnico.com/ + +## 常用命令 + +| 命令 | 说明 | +|------|------| +| `make run` | 启动服务 | +| `make stop` | 停止服务 | +| `make logs` | 查看日志 | +| `make shell` | 进入容器 | +| `make test` | 测试连接 | + +## 客户端连接 + +### 浏览器客户端 +直接访问:http://ratel.isnico.com/ + +### 命令行客户端 +下载地址:https://github.com/ratel-online/client/releases + +```bash +# 连接本地服务器 +./ratel-client -h localhost -p 9999 +``` + +## 游戏指令 + +### 全局指令 +- `v` - 查看房间列表/房间成员 +- `e` - 退出/返回 + +### 房间指令 +- `s` - 开始游戏 +- `set lz on` - 开启癞子模式 +- `set sk on` - 开启技能模式 + +### 游戏指令 +- 直接输入牌型,如:`334455`(连对) +- `p` - 不出/过牌 + +## 故障排查 + +### 端口被占用 +```bash +# 修改端口映射 +export WSPORT=19998 +export TCPPORT=19999 +docker-compose up -d +``` + +### 查看服务状态 +```bash +docker ps +docker logs ratel-server +``` + +## 了解更多 + +- 详细部署文档:[Docker部署指南](./Docker部署指南.md) +- 项目主页:https://github.com/ratel-online/server +- QQ群:948365095 \ No newline at end of file diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..d6cf97c --- /dev/null +++ b/server/go.mod @@ -0,0 +1,24 @@ +module github.com/ratel-online/server + +go 1.22 + +require ( + github.com/awesome-cap/hashmap v0.0.0-20211211100532-e3300ac4ae14 + github.com/feel-easy/mahjong v0.0.0-20220721030133-7a0f4032c008 + github.com/feel-easy/uno v0.0.0-20220721061415-e6a3189cfd70 + github.com/gorilla/websocket v1.5.3 + github.com/ratel-online/core v0.0.0-20250225062905-81b6faff6d25 +) + +require ( + github.com/Szzrain/Milky-go-sdk v1.0.1 // indirect + github.com/awesome-cap/im v0.0.0-20210720090440-7556eb92965d // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mattn/go-colorable v0.1.9 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/spf13/cast v1.6.0 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..e3c59a8 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,93 @@ +github.com/Szzrain/Milky-go-sdk v1.0.1 h1:dsKIUtW7lI7RGLOuNut2ChxqQpM+bqd+3AafCIIhYRc= +github.com/Szzrain/Milky-go-sdk v1.0.1/go.mod h1:vyl5G/6TQha+Ygf9ZWkDN//7dJFq2V0ezdk3Rm2kDKQ= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/awesome-cap/hashmap v0.0.0-20211211100532-e3300ac4ae14 h1:YH6IXdLClQbuKrhYdyXYzdE9cicwU7HaD7YDMmo0oRY= +github.com/awesome-cap/hashmap v0.0.0-20211211100532-e3300ac4ae14/go.mod h1:5vIRKw3P2HirHOPm1xQfJ6GmPfZQemzbIXfU0MqLlZU= +github.com/awesome-cap/im v0.0.0-20210720090440-7556eb92965d h1:lc5kZK/lqmqAfNeXcRPkwXKdwO/9fQ09Wpz9p/qp4So= +github.com/awesome-cap/im v0.0.0-20210720090440-7556eb92965d/go.mod h1:dIipyrtcBTc6up0MNw5w7ONoNKGksz2EGCVlcwiRjE4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/feel-easy/mahjong v0.0.0-20220721030133-7a0f4032c008 h1:bhsYIxvD3J1bZbS7A7f2YPxadvy909nRRHZ96VBTvGE= +github.com/feel-easy/mahjong v0.0.0-20220721030133-7a0f4032c008/go.mod h1:qDtjtiCnj2u+T0GCOd3ky4gi/0Nx2iDglGC/U9k8SYg= +github.com/feel-easy/uno v0.0.0-20220721061415-e6a3189cfd70 h1:c5e8UeFNbIPsWKMS/arf/VYfGgrKRFUQbdPOfRn0nlE= +github.com/feel-easy/uno v0.0.0-20220721061415-e6a3189cfd70/go.mod h1:SJtQw6YT+CnOWLq21s/NiQeB9UjMkf6ftomfhqrI1Ng= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/memberlist v0.2.4/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/ratel-online/core v0.0.0-20220717092113-9fdec75dd055 h1:DUGSs+HCGwcOpCi24lCUfMXM3TbzRRT7ewLC/4hlIL0= +github.com/ratel-online/core v0.0.0-20220717092113-9fdec75dd055/go.mod h1:8SYaPGDk9dVGnUIEkanZvV1ErTqd8OdedKNCwyXgRjI= +github.com/ratel-online/core v0.0.0-20240410105638-a7e4146f84bc h1:aDktTjLc2Y9dRh1dD2NXlt3ObqAQI64rbhfmVDwIuVU= +github.com/ratel-online/core v0.0.0-20240410105638-a7e4146f84bc/go.mod h1:8SYaPGDk9dVGnUIEkanZvV1ErTqd8OdedKNCwyXgRjI= +github.com/ratel-online/core v0.0.0-20240410123929-a9a4ad51dc6b h1:RNJtWkOx8XGsDhT6APZk4wYpxzs3r/B0h1n/GyP7ids= +github.com/ratel-online/core v0.0.0-20240410123929-a9a4ad51dc6b/go.mod h1:8SYaPGDk9dVGnUIEkanZvV1ErTqd8OdedKNCwyXgRjI= +github.com/ratel-online/core v0.0.0-20240411045052-840504c4e7fc h1:usJbCKX8e1eLyeWc/Alm9IqPHVEwgPWz0sh6MX5dY08= +github.com/ratel-online/core v0.0.0-20240411045052-840504c4e7fc/go.mod h1:8SYaPGDk9dVGnUIEkanZvV1ErTqd8OdedKNCwyXgRjI= +github.com/ratel-online/core v0.0.0-20240411101817-d5cb908bcf49 h1:yESHztiCxllI22QToTpUcQS/MPzeQmxVIpzZdv+fHZM= +github.com/ratel-online/core v0.0.0-20240411101817-d5cb908bcf49/go.mod h1:8SYaPGDk9dVGnUIEkanZvV1ErTqd8OdedKNCwyXgRjI= +github.com/ratel-online/core v0.0.0-20240412044054-42ffde55324d h1:F8Qcc9aUnNv5716ITYXlYKxuoWYDMTXAbBVwjDUmrsI= +github.com/ratel-online/core v0.0.0-20240412044054-42ffde55324d/go.mod h1:8SYaPGDk9dVGnUIEkanZvV1ErTqd8OdedKNCwyXgRjI= +github.com/ratel-online/core v0.0.0-20250225062905-81b6faff6d25 h1:9vdciSkTnXNoZkTqOe9rSP40PGRvNk/OzYxPCmWGh1c= +github.com/ratel-online/core v0.0.0-20250225062905-81b6faff6d25/go.mod h1:8SYaPGDk9dVGnUIEkanZvV1ErTqd8OdedKNCwyXgRjI= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..78f6f1c --- /dev/null +++ b/server/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "flag" + "fmt" + "strconv" + + "github.com/ratel-online/core/log" + "github.com/ratel-online/core/util/async" + "github.com/ratel-online/server/bot" + "github.com/ratel-online/server/network" +) + +var ( + Wsport int + Tcpport int + BotAddr string + BotToken string + BotGroup int64 +) + +func main() { + flag.IntVar(&Wsport, "w", 9998, "WebsocketServer Port") + flag.IntVar(&Tcpport, "t", 9999, "TcpServer Port") + flag.StringVar(&BotAddr, "bot", "", "Bot connection address") + flag.StringVar(&BotToken, "bot-token", "", "Bot token") + flag.Int64Var(&BotGroup, "bot-group", 0, "Bot group ID") + + flag.Parse() + // 连接机器人 + if BotAddr != "" && BotToken != "" && BotGroup != 0 { + err := bot.Connect(BotAddr, BotToken, BotGroup) + if err != nil { + log.Panic(fmt.Sprintf("连接Bot失败: %v", err)) + } + // 发送测试消息到 BotGroup 群 + err = bot.SendGroupMessage(BotGroup, "Server started!") + if err != nil { + log.Errorf("发送群消息失败: %v", err) + } else { + log.Infof("已发送群消息到 %d", BotGroup) + } + defer bot.Close() + } + + async.Async(func() { + wsServer := network.NewWebsocketServer(":" + strconv.Itoa(Wsport)) + log.Panic(wsServer.Serve()) + }) + + server := network.NewTcpServer(":" + strconv.Itoa(Tcpport)) + log.Panic(server.Serve()) +} \ No newline at end of file diff --git a/server/network/network.go b/server/network/network.go new file mode 100644 index 0000000..16e7f7c --- /dev/null +++ b/server/network/network.go @@ -0,0 +1,66 @@ +package network + +import ( + "github.com/ratel-online/core/log" + "github.com/ratel-online/core/model" + "github.com/ratel-online/core/network" + "github.com/ratel-online/core/protocol" + "github.com/ratel-online/core/util/async" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" + "github.com/ratel-online/server/state" + "time" +) + +// Network is interface of all kinds of network. +type Network interface { + Serve() error +} + +func handle(rwc protocol.ReadWriteCloser) error { + // 给新进入的用户分配资源 + c := network.Wrapper(rwc) + defer func() { + err := c.Close() + if err != nil { + log.Error(err) + } + }() + log.Info("new player connected! ") + authInfo, err := loginAuth(c) + if err != nil { + _ = c.Write(protocol.ErrorPacket(err)) + return err + } + player := database.Connected(c, authInfo) + log.Infof("player auth accessed, ip %s, %d:%s\n", player.IP, player.ID, authInfo.Name) + go state.Run(player) + defer player.Offline() + return player.Listening() +} + +// 登陆验签 +func loginAuth(c *network.Conn) (*model.AuthInfo, error) { + authChan := make(chan *model.AuthInfo) + defer close(authChan) + async.Async(func() { + packet, err := c.Read() + if err != nil { + log.Error(err) + return + } + authInfo := &model.AuthInfo{} + err = packet.Unmarshal(authInfo) + if err != nil { + log.Error(err) + return + } + authChan <- authInfo + }) + select { + case authInfo := <-authChan: + return authInfo, nil + case <-time.After(3 * time.Second): + return nil, consts.ErrorsAuthFail + } +} diff --git a/server/network/tcp.go b/server/network/tcp.go new file mode 100644 index 0000000..04eee3a --- /dev/null +++ b/server/network/tcp.go @@ -0,0 +1,43 @@ +package network + +import ( + "github.com/ratel-online/core/log" + "github.com/ratel-online/core/protocol" + "github.com/ratel-online/core/util/async" + "net" +) + +type Tcp struct { + addr string +} + +func NewTcpServer(addr string) Tcp { + return Tcp{addr: addr} +} + +func (t Tcp) Serve() error { + listener, err := net.Listen("tcp", t.addr) + if err != nil { + log.Error(err) + return err + } + log.Infof("Tcp server listening on %s\n", t.addr) + loopCount := 0 + for { + loopCount++ + if loopCount%1000 == 0 { + log.Infof("[Tcp.Serve] Accept loop count: %d\n", loopCount) + } + conn, err := listener.Accept() + if err != nil { + log.Infof("listener.Accept err %v\n", err) + continue + } + async.Async(func() { + err := handle(protocol.NewTcpReadWriteCloser(conn)) + if err != nil { + log.Error(err) + } + }) + } +} diff --git a/server/network/wss.go b/server/network/wss.go new file mode 100644 index 0000000..7f4d4f0 --- /dev/null +++ b/server/network/wss.go @@ -0,0 +1,42 @@ +package network + +import ( + "github.com/gorilla/websocket" + "github.com/ratel-online/core/log" + "github.com/ratel-online/core/protocol" + "net/http" +) + +type Websocket struct { + addr string +} + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func NewWebsocketServer(addr string) Websocket { + return Websocket{addr: addr} +} + +func (w Websocket) Serve() error { + http.HandleFunc("/ws", serveWs) + log.Infof("Websocket server listener on %s\n", w.addr) + return http.ListenAndServe(w.addr, nil) +} + +func serveWs(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Error(err) + return + } + err = handle(protocol.NewWebsocketReadWriteCloser(conn)) + if err != nil{ + log.Error(err) + } +} diff --git a/server/render/render.go b/server/render/render.go new file mode 100644 index 0000000..d64cd20 --- /dev/null +++ b/server/render/render.go @@ -0,0 +1,131 @@ +package render + +import ( + "bytes" + "fmt" + constx "github.com/ratel-online/core/consts" + "github.com/ratel-online/core/model" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" +) + +func Welcome(player *database.Player) error { + return player.WriteObject(model.Data{ + Code: constx.CodeWelcome, + Msg: fmt.Sprintf("Hi %s, Welcome to ratel online! \n", player.Name), + }) +} + +func HomeOptions(player *database.Player) error { + buf := bytes.Buffer{} + buf.WriteString("1.Join\n") + buf.WriteString("2.New\n") + return player.WriteObject(model.Options{ + Data: model.Data{ + Code: constx.CodeHomeOptions, + Msg: buf.String(), + }, + Options: []model.Option{ + {ID: 1, Name: "Join"}, + {ID: 2, Name: "New"}, + }, + }) +} + +func GameTypeOptions(player *database.Player) error { + buf := bytes.Buffer{} + buf.WriteString("Please select game type\n") + options := make([]model.Option, 0) + for _, id := range consts.GameTypesIds { + buf.WriteString(fmt.Sprintf("%d.%s\n", id, consts.GameTypes[id])) + options = append(options, model.Option{ID: id, Name: consts.GameTypes[id]}) + } + return player.WriteObject(model.Options{ + Data: model.Data{ + Code: constx.CodeGameTypeOptions, + Msg: buf.String(), + }, + Options: options, + }) +} + +func RoomList(player *database.Player) error { + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("%-10s%-10s%-10s%-10s\n", "ID", "Type", "Players", "State")) + for _, room := range database.GetRooms() { + buf.WriteString(fmt.Sprintf("%-10d%-10s%-10d%-10s\n", room.ID, consts.GameTypes[room.Type], room.Players, consts.RoomStates[room.State])) + } + modelRooms := make([]model.Room, 0) + for _, room := range database.GetRooms() { + modelRooms = append(modelRooms, room.Model()) + } + return player.WriteObject(model.RoomList{ + Data: model.Data{ + Code: constx.CodeRoomList, + Msg: buf.String(), + }, + Rooms: modelRooms, + }) +} + +func RoomInfo(player *database.Player, room *database.Room) error { + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("%-20s%-10s%-10s\n", "Name", "Amount", "Title")) + for playerId := range database.RoomPlayers(room.ID) { + title := "player" + if playerId == room.Creator { + title = "owner" + } + info := database.GetPlayer(playerId) + buf.WriteString(fmt.Sprintf("%-20s%-10d%-10s\n", info.Name, info.Amount, title)) + } + return player.WriteString(buf.String()) +} + +func Error(player *database.Player, err error) error { + return player.WriteError(err) +} + +func Join(player *database.Player, room *database.Room) { + database.BroadcastObject(room.ID, model.RoomEvent{ + Data: model.Data{ + Code: constx.CodeRoomEventJoin, + Msg: fmt.Sprintf("%s joined room! room current has %d players\n", player.Name, room.Players), + }, + Room: room.Model(), + Player: player.Model(), + }) +} + +func Exit(player *database.Player, room *database.Room) { + database.BroadcastObject(room.ID, model.RoomEvent{ + Data: model.Data{ + Code: constx.CodeRoomEventExit, + Msg: fmt.Sprintf("%s exited room! room current has %d players\n", player.Name, room.Players), + }, + Room: room.Model(), + Player: player.Model(), + }) +} + +func Offline(player *database.Player, room *database.Room) { + database.BroadcastObject(room.ID, model.RoomEvent{ + Data: model.Data{ + Code: constx.CodeRoomEventOffline, + Msg: fmt.Sprintf("%s lost connection", player.Name), + }, + Room: room.Model(), + Player: player.Model(), + }) +} + +func OwnerChange(player *database.Player, room *database.Room) { + database.BroadcastObject(room.ID, model.RoomEvent{ + Data: model.Data{ + Code: constx.CodeRoomEventOwnerChange, + Msg: fmt.Sprintf("%s become new owner\n", player.Name), + }, + Room: room.Model(), + Player: player.Model(), + }) +} diff --git a/server/rule/rule.go b/server/rule/rule.go new file mode 100644 index 0000000..4f3526d --- /dev/null +++ b/server/rule/rule.go @@ -0,0 +1,55 @@ +package rule + +var ( + // LandlordRules 斗地主规则 + LandlordRules = _rules{reserved: true} + // TeamRules 团队规则 + TeamRules = _rules{} + // RunFastRules 跑得快規則 + RunFastRules = _rules{reserved: true, isRunFast: true} + // TexasRules 德州扑克规则 + TexasRules = _texasRule{} +) + +type _rules struct { + reserved bool + isRunFast bool +} + +func (r _rules) Value(key int) int { + if key == 1 { + return 12 + } else if key == 2 { + return 13 + } else if key > 13 { + return key + } + return key - 2 +} + +func (r _rules) IsStraight(faces []int, count int) bool { + if faces[len(faces)-1]-faces[0] != len(faces)-1 { + return false + } + if faces[len(faces)-1] > 12 { + return false + } + if count == 1 { + return len(faces) >= 5 + } else if count == 2 && r.isRunFast { + return len(faces) >= 2 + } else if count == 2 && !r.isRunFast { + return len(faces) >= 3 + } else if count > 2 { + return len(faces) >= 2 + } + return false +} + +func (r _rules) StraightBoundary() (int, int) { + return 1, 12 +} + +func (r _rules) Reserved() bool { + return r.reserved +} diff --git a/server/rule/texas.go b/server/rule/texas.go new file mode 100644 index 0000000..69e3c73 --- /dev/null +++ b/server/rule/texas.go @@ -0,0 +1,24 @@ +package rule + +type _texasRule struct { +} + +func (r _texasRule) Value(key int) int { + if key == 1 { + return 13 + } + return key - 1 +} + +func (r _texasRule) IsStraight(faces []int, count int) bool { + // todo + return false +} + +func (r _texasRule) StraightBoundary() (int, int) { + return 1, 13 +} + +func (r _texasRule) Reserved() bool { + return false +} diff --git a/server/skill/skill.go b/server/skill/skill.go new file mode 100644 index 0000000..d36105a --- /dev/null +++ b/server/skill/skill.go @@ -0,0 +1,269 @@ +package skill + +import ( + "bytes" + "fmt" + "github.com/ratel-online/core/model" + "github.com/ratel-online/core/util/poker" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" + "math/rand" + "time" +) + +var Skills = map[consts.SkillID]Skill{ + consts.SkillWYSS: WYSSSkill{}, + consts.SkillHYJJ: HYJJSkill{}, + consts.SkillDHXJ: DHXJSkill{}, + consts.SkillGHJM: GHJMSkill{}, + consts.SkillPFCZ: PFCZSkill{}, + consts.SkillLJFZ: LJFZSkill{}, + consts.SkillZWZB: ZWZBSkill{}, + consts.SkillSKLF: SKLFSkill{}, + consts.Skill996: N996Skill{}, + consts.SkillTZJW: TZJWSkill{}, +} + +type Skill interface { + Name() string + Desc(player *database.Player) string + Apply(player *database.Player, game *database.Game) +} + +type WYSSSkill struct{} + +func (WYSSSkill) Name() string { + return "我要色色" +} + +func (WYSSSkill) Desc(player *database.Player) string { + return fmt.Sprintf("%s 触发技能<我要色色>,其余玩家沉迷其中,趁机偷掉了他们的最牛的牌", player.Name) +} + +func (WYSSSkill) Apply(player *database.Player, game *database.Game) { + buf := bytes.Buffer{} + for _, id := range game.Players { + if id == player.ID { + continue + } + l := len(game.Pokers[id]) + if l > 1 { + max := game.Pokers[id][l-1] + game.Pokers[id] = game.Pokers[id][:l-1] + game.Pokers[player.ID] = append(game.Pokers[player.ID], max) + game.Pokers[player.ID].SortByOaaValue() + buf.WriteString(fmt.Sprintf("%s 偷掉了 %s 的牌 %s\n", player.Name, database.GetPlayer(id).Name, model.Pokers{max}.OaaString())) + } + } + database.Broadcast(player.RoomID, buf.String()) +} + +type HYJJSkill struct{} + +func (HYJJSkill) Name() string { + return "火眼金睛" +} + +func (HYJJSkill) Desc(player *database.Player) string { + return fmt.Sprintf("%s 触发技能<火眼金睛>,看穿了对手的牌", player.Name) +} + +func (HYJJSkill) Apply(player *database.Player, game *database.Game) { + buf := bytes.Buffer{} + for _, id := range game.Players { + if id == player.ID { + continue + } + buf.WriteString(fmt.Sprintf("%s: %s\n", database.GetPlayer(id).Name, game.Pokers[id].OaaString())) + } + _ = player.WriteString(buf.String()) +} + +type GHJMSkill struct{} + +func (GHJMSkill) Name() string { + return "改换家门" +} + +func (GHJMSkill) Desc(player *database.Player) string { + return fmt.Sprintf("%s 触发技能<改换家门>,手牌重新分配", player.Name) +} + +func (GHJMSkill) Apply(player *database.Player, game *database.Game) { + l := len(game.Pokers[player.ID]) + keys := poker.RandomN(l) + pokers := poker.GetPokers(keys...) + for i := range pokers { + pokers[i].Val = game.Rules.Value(pokers[i].Key) + } + if game.Room.EnableLaiZi { + pokers.SetOaa(game.Universals...) + pokers.SortByOaaValue() + } + game.Pokers[player.ID] = pokers +} + +type PFCZSkill struct{} + +func (PFCZSkill) Name() string { + return "破斧沉舟" +} + +func (PFCZSkill) Desc(player *database.Player) string { + return fmt.Sprintf("%s 触发技能<破斧沉舟>,只留下5张最强的牌", player.Name) +} + +func (PFCZSkill) Apply(player *database.Player, game *database.Game) { + pokers := game.Pokers[player.ID] + pokers.SortByValue() + l := len(pokers) + if l > 5 { + game.Pokers[player.ID] = pokers[l-5:] + } + game.Pokers[player.ID].SortByOaaValue() +} + +type DHXJSkill struct{} + +func (DHXJSkill) Name() string { + return "大幻想家" +} + +func (DHXJSkill) Desc(player *database.Player) string { + return fmt.Sprintf("%s 触发技能<大幻想家>,最小的一张牌变成了癞子", player.Name) +} + +func (DHXJSkill) Apply(player *database.Player, game *database.Game) { + pokers := game.Pokers[player.ID] + pokers[0].Oaa = true + pokers.SortByOaaValue() +} + +type LJFZSkill struct{} + +func (LJFZSkill) Name() string { + return "两极反转" +} + +func (LJFZSkill) Desc(player *database.Player) string { + return fmt.Sprintf("%s 触发技能<两极反转>,随机与一名玩家调换手牌", player.Name) +} + +func (LJFZSkill) Apply(player *database.Player, game *database.Game) { + var targetPlayerId int64 = 0 + r := rand.New(rand.NewSource(time.Now().UnixNano())) + for targetPlayerId == int64(0) { + p := game.Players[r.Intn(len(game.Players))] + if p != player.ID { + targetPlayerId = p + } + } + game.Pokers[targetPlayerId], game.Pokers[player.ID] = game.Pokers[player.ID], game.Pokers[targetPlayerId] +} + +type ZWZBSkill struct{} + +func (ZWZBSkill) Name() string { + return "追亡逐北" +} + +func (ZWZBSkill) Desc(player *database.Player) string { + return fmt.Sprintf("%s 触发技能<追亡逐北>,多获得一次出牌机会", player.Name) +} + +func (ZWZBSkill) Apply(player *database.Player, game *database.Game) { + game.PlayTimes[player.ID] = 2 +} + +type SKLFSkill struct{} + +func (SKLFSkill) Name() string { + return "时空裂缝" +} + +func (SKLFSkill) Desc(player *database.Player) string { + return fmt.Sprintf("%s 触发技能<时空裂缝>,其余玩家出牌时间减半", player.Name) +} + +func (SKLFSkill) Apply(player *database.Player, game *database.Game) { + for id := range game.PlayTimeOut { + if id == player.ID { + continue + } + if game.PlayTimeOut[id] >= 10*time.Second { + game.PlayTimeOut[id] /= 2 + } else { + game.PlayTimeOut[id] = 5 * time.Second + } + } +} + +type N996Skill struct{} + +func (N996Skill) Name() string { + return "996" +} + +func (N996Skill) Desc(player *database.Player) string { + return fmt.Sprintf("%s 触发技能<996>,所有对手强制获得9,9,6三张牌", player.Name) +} + +func (N996Skill) Apply(player *database.Player, game *database.Game) { + pokers := poker.GetPokers(9, 9, 6) + for i := range pokers { + pokers[i].Val = game.Rules.Value(pokers[i].Key) + } + if game.Room.EnableLaiZi { + pokers.SetOaa(game.Universals...) + } + for _, id := range game.Players { + if id == player.ID { + continue + } + game.Pokers[id] = append(game.Pokers[id], pokers...) + game.Pokers[id].SortByOaaValue() + } +} + +type TZJWSkill struct{} + +func (TZJWSkill) Name() string { + return "添砖加瓦" +} + +func (TZJWSkill) Desc(player *database.Player) string { + return fmt.Sprintf("%s 触发技能<添砖加瓦>,从弃牌池中随机抽取两张牌返还给所有对手", player.Name) +} + +func (TZJWSkill) Apply(player *database.Player, game *database.Game) { + buf := bytes.Buffer{} + pks := model.Pokers{} + r := rand.New(rand.NewSource(time.Now().UnixNano())) + l := len(game.Discards) + for i := 0; i < Min(2, l); i++ { + target := r.Intn(len(game.Discards)) + pks = append(pks, game.Discards[target]) + game.Discards = append(game.Discards[:target], game.Discards[target+1:]...) + } + if len(pks) > 0 { + for _, id := range game.Players { + if id == player.ID { + continue + } + game.Pokers[id] = append(game.Pokers[id], pks...) + game.Pokers[id].SortByOaaValue() + buf.WriteString(database.GetPlayer(id).Name + ",") + } + buf.Truncate(buf.Len() - 1) + buf.WriteString("获得了" + pks.OaaString()) + buf.WriteString("\n") + database.Broadcast(player.RoomID, buf.String()) + } +} + +func Min(i, j int) int { + if i < j { + return i + } + return j +} diff --git a/server/skill/skill_test.go b/server/skill/skill_test.go new file mode 100644 index 0000000..2b0b25c --- /dev/null +++ b/server/skill/skill_test.go @@ -0,0 +1,15 @@ +package skill + +import ( + "fmt" + "math/rand" + "testing" + "time" +) + +func TestDHXJSkill_Apply(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + for i := 0; i < 100; i++ { + fmt.Println(rand.Intn(len(Skills))) + } +} diff --git a/server/state/create.go b/server/state/create.go new file mode 100644 index 0000000..a9c92b0 --- /dev/null +++ b/server/state/create.go @@ -0,0 +1,61 @@ +package state + +import ( + "bytes" + "errors" + "fmt" + + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" +) + +type create struct{} + +func (*create) Next(player *database.Player) (consts.StateID, error) { + gameType, err := askForGameType(player) + if err != nil { + return 0, err + } + // 创建房间 + room := database.CreateRoom(player.ID, gameType) + err = player.WriteString(fmt.Sprintf("Create room successful, id : %d\n", room.ID)) + if err != nil { + return 0, player.WriteError(err) + } + err = database.JoinRoom(room.ID, player.ID) + if err != nil { + return 0, player.WriteError(err) + } + return consts.StateWaiting, nil +} + +func (*create) Exit(_ *database.Player) consts.StateID { + return consts.StateHome +} + +// 询问游戏类型 +func askForGameType(player *database.Player) (gameType int, err error) { + buf := bytes.Buffer{} + buf.WriteString("Please select game type\n") + for _, id := range consts.GameTypesIds { + buf.WriteString(fmt.Sprintf("%d.%s\n", id, consts.GameTypes[id])) + } + err = player.WriteString(buf.String()) + if err != nil { + return 0, player.WriteError(err) + } + gameType, err = player.AskForInt() + if err != nil { + if errors.Is(err, consts.ErrorsExist) { + return 0, err + } + _ = player.WriteError(consts.ErrorsGameTypeInvalid) + return 0, consts.ErrorsGameTypeInvalid + } + // check game type. + if _, ok := consts.GameTypes[gameType]; !ok { + _ = player.WriteError(consts.ErrorsGameTypeInvalid) + return 0, consts.ErrorsGameTypeInvalid + } + return +} diff --git a/server/state/game/game.go b/server/state/game/game.go new file mode 100644 index 0000000..96a825f --- /dev/null +++ b/server/state/game/game.go @@ -0,0 +1,492 @@ +package game + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "time" + + "github.com/ratel-online/core/util/rand" + "github.com/ratel-online/server/rule" + + "github.com/ratel-online/core/log" + modelx "github.com/ratel-online/core/model" + "github.com/ratel-online/core/util/poker" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" + "github.com/ratel-online/server/skill" +) + +type Game struct{} + +var ( + stateRob = 1 + statePlay = 2 + stateReset = 3 + stateWaiting = 4 + stateFirstCard = 5 + stateTakeCard = 6 +) + +func (g *Game) Next(player *database.Player) (consts.StateID, error) { + room := database.GetRoom(player.RoomID) + if room == nil { + return 0, player.WriteError(consts.ErrorsExist) + } + game := room.Game.(*database.Game) + buf := bytes.Buffer{} + if game.Room.EnableLaiZi { + if game.Room.EnableSkill { + game.Pokers[player.ID].SetOaa(game.Universals...) + buf.WriteString(fmt.Sprintf("Game starting! Universals: %s %s\n", poker.GetDesc(game.Universals[0]), poker.GetDesc(game.Universals[1]))) + } else { + game.Pokers[player.ID].SetOaa(game.Universals[0]) + buf.WriteString(fmt.Sprintf("Game starting! First universal: %s\n", poker.GetDesc(game.Universals[0]))) + } + game.Pokers[player.ID].SortByOaaValue() + } else { + buf.WriteString(fmt.Sprintf("Game starting!\n")) + } + if game.Room.EnableSkill { + buf.WriteString(fmt.Sprintf("Got skill %s\n", skill.Skills[consts.SkillID(game.Skills[player.ID])].Name())) + } + buf.WriteString(fmt.Sprintf("Your pokers: %s\n", game.Pokers[player.ID].String())) + _ = player.WriteString(buf.String()) + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[Game.Next] Player %d (Room %d) loop count: %d, room.State: %d\n", player.ID, player.RoomID, loopCount, room.State) + } + if room.State == consts.RoomStateWaiting { + log.Infof("[Game.Next] Player %d exiting, room state changed to waiting, loop count: %d\n", player.ID, loopCount) + return consts.StateWaiting, nil + } + log.Infof("[Game.Next] Player %d waiting for state, loop count: %d\n", player.ID, loopCount) + state := <-game.States[player.ID] + switch state { + case stateRob: + if !game.Room.EnableLandlord { + // reset all players group + for i, id := range game.Players { + game.Groups[id] = i + if game.Room.EnableLaiZi { + game.Pokers[id].SetOaa(game.Universals...) + game.Pokers[id].SortByOaaValue() + } + } + game.States[player.ID] <- statePlay + } else { + err := handleRob(player, game) + if err != nil { + log.Error(err) + return 0, err + } + } + case stateReset: + if player.ID == room.Creator { + game.States[game.Players[rand.Intn(len(game.States))]] <- stateRob + } + return 0, nil + case statePlay: + err := handlePlay(player, game) + if err != nil { + log.Error(err) + return 0, err + } + case stateWaiting: + return consts.StateWaiting, nil + default: + return 0, consts.ErrorsChanClosed + } + } +} + +func (*Game) Exit(player *database.Player) consts.StateID { + return consts.StateHome +} + +func handleRob(player *database.Player, game *database.Game) error { + if game.FirstPlayer == player.ID && !game.FinalRob { + if game.FirstRob == 0 { + err := resetGame(game) + if err != nil { + log.Error(err) + return err + } + database.Broadcast(player.RoomID, "All players have give up the landlord, restarting...\n") + for _, playerId := range game.Players { + game.States[playerId] <- stateReset + } + } else if game.FirstRob == game.LastRob { + landlord := database.GetPlayer(game.LastRob) + game.FirstPlayer = landlord.ID + game.LastPlayer = landlord.ID + game.Groups[landlord.ID] = 1 + game.Pokers[landlord.ID] = append(game.Pokers[landlord.ID], game.Additional...) + game.Pokers[landlord.ID].SortByOaaValue() + + buf := bytes.Buffer{} + if game.Room.EnableLaiZi { + buf.WriteString(fmt.Sprintf("%s became landlord, got pokers: %s, last universal: %s\n", landlord.Name, game.Additional.String(), poker.GetDesc(game.Universals[1]))) + for _, pokers := range game.Pokers { + pokers.SetOaa(game.Universals...) + pokers.SortByOaaValue() + } + } else { + buf.WriteString(fmt.Sprintf("%s became landlord, got pokers: %s\n", landlord.Name, game.Additional.String())) + } + database.Broadcast(player.RoomID, buf.String()) + game.States[landlord.ID] <- statePlay + } else { + game.FinalRob = true + game.States[game.FirstRob] <- stateRob + } + return nil + } + if game.FirstPlayer == 0 { + game.FirstPlayer = player.ID + database.Broadcast(player.RoomID, fmt.Sprintf("%s's turn to rob\n", player.Name), player.ID) + } + + timeout := consts.RobTimeout + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[handleRob] Player %d (Room %d) loop count: %d, timeout: %v, FirstRob: %d, LastRob: %d\n", player.ID, player.RoomID, loopCount, timeout, game.FirstRob, game.LastRob) + } + before := time.Now().Unix() + _ = player.WriteString("Are you want to become landlord? (y or n)\n") + ans, err := player.AskForString(timeout) + if err != nil && err != consts.ErrorsExist { + ans = "n" + } + timeout -= time.Second * time.Duration(time.Now().Unix()-before) + ans = strings.ToLower(ans) + if ans == "y" { + if game.FirstRob == 0 { + game.FirstRob = player.ID + } + game.LastRob = player.ID + game.Multiple *= 2 + database.Broadcast(player.RoomID, fmt.Sprintf("%s rob\n", player.Name)) + break + } else if ans == "n" { + database.Broadcast(player.RoomID, fmt.Sprintf("%s don't rob\n", player.Name)) + break + } else { + _ = player.WriteError(consts.ErrorsInputInvalid) + continue + } + } + if game.FinalRob { + game.FinalRob = false + game.FirstRob = game.LastRob + game.States[game.FirstPlayer] <- stateRob + } else { + game.States[game.NextPlayer(player.ID)] <- stateRob + } + return nil +} + +func playing(player *database.Player, game *database.Game, master bool, playTimes int) error { + timeout := game.PlayTimeOut[player.ID] + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[playing] Player %d (Room %d) loop count: %d, master: %v, playTimes: %d, timeout: %v\n", player.ID, player.RoomID, loopCount, master, playTimes, timeout) + } + buf := bytes.Buffer{} + buf.WriteString("\n") + if !master && len(game.LastPokers) > 0 { + buf.WriteString(fmt.Sprintf("Last player: %s (%s), played: %s\n", database.GetPlayer(game.LastPlayer).Name, game.Team(game.LastPlayer), game.LastPokers.String())) + } + buf.WriteString(fmt.Sprintf("Timeout: %ds, pokers: %s\n", int(timeout.Seconds()), game.Pokers[player.ID].String())) + _ = player.WriteString(buf.String()) + before := time.Now().Unix() + pokers := game.Pokers[player.ID] + ans, err := player.AskForString(timeout) + if err != nil { + if master { + ans = poker.GetAlias(pokers[0].Key) + } else { + ans = "p" + } + } else { + timeout -= time.Second * time.Duration(time.Now().Unix()-before) + } + ans = strings.ToLower(ans) + if ans == "" { + _ = player.WriteString(fmt.Sprintf("%s\n", consts.ErrorsPokersFacesInvalid.Error())) + continue + } else if ans == "ls" || ans == "v" { + viewGame(game, player) + continue + } else if ans == "p" || ans == "pass" { + if master { + _ = player.WriteError(consts.ErrorsHaveToPlay) + continue + } else { + nextPlayer := database.GetPlayer(game.NextPlayer(player.ID)) + database.Broadcast(player.RoomID, fmt.Sprintf("%s passed, next %s\n", player.Name, nextPlayer.Name)) + game.States[nextPlayer.ID] <- statePlay + return nil + } + } + normalPokers := map[int]modelx.Pokers{} + universalPokers := make(modelx.Pokers, 0) + realSellKeys := make([]int, 0) + for _, v := range pokers { + if v.Oaa { + universalPokers = append(universalPokers, v) + } else { + normalPokers[v.Key] = append(normalPokers[v.Key], v) + } + } + sells := make(modelx.Pokers, 0) + invalid := false + for _, alias := range ans { + key := poker.GetKey(string(alias)) + if key == 0 { + invalid = true + break + } + if len(normalPokers[key]) == 0 { + if key == 14 || key == 15 || len(universalPokers) == 0 { + invalid = true + break + } + realSellKeys = append(realSellKeys, universalPokers[0].Key) + universalPokers[0].Key = key + universalPokers[0].Desc = poker.GetDesc(key) + universalPokers[0].Val = game.Rules.Value(key) + sells = append(sells, universalPokers[0]) + universalPokers = universalPokers[1:] + } else { + realSellKeys = append(realSellKeys, key) + sells = append(sells, normalPokers[key][len(normalPokers[key])-1]) + normalPokers[key] = normalPokers[key][:len(normalPokers[key])-1] + } + } + facesArr := poker.ParseFaces(sells, game.Rules) + if len(facesArr) == 0 { + invalid = true + } + //聊天開啓才能說話 + if invalid { + if game.Room.EnableChat { + database.BroadcastChat(player, fmt.Sprintf("%s [%s] say: %s\n", player.Name, player.Role, ans)) + continue + } else { + _ = player.WriteString(fmt.Sprintf("%s\n", consts.ErrorsChatUnopened.Error())) + continue + } + } + lastFaces := game.LastFaces + if !master && lastFaces != nil { + if isMax(game, *lastFaces) { + _ = player.WriteString(fmt.Sprintf("%s\n", consts.ErrorsPokersFacesInvalid.Error())) + continue + } + access := false + for _, faces := range facesArr { + if isMax(game, faces) || faces.Compare(*lastFaces) { + access = true + lastFaces = &faces + break + } + } + if !access { + _ = player.WriteString(fmt.Sprintf("%s\n", consts.ErrorsPokersFacesInvalid.Error())) + continue + } + } else { + lastFaces = &facesArr[0] + } + for _, key := range realSellKeys { + game.Mnemonic[key]-- + } + pokers = make(modelx.Pokers, 0) + for _, curr := range normalPokers { + pokers = append(pokers, curr...) + } + pokers = append(pokers, universalPokers...) + pokers.SortByOaaValue() + game.Pokers[player.ID] = pokers + game.LastPlayer = player.ID + game.LastFaces = lastFaces + game.LastPokers = sells + game.Discards = append(game.Discards, sells...) + if len(pokers) == 0 { + database.Broadcast(player.RoomID, fmt.Sprintf("%s played %s, won the game! \n", player.Name, sells.OaaString())) + room := database.GetRoom(player.RoomID) + if room != nil { + room.Game = nil + room.State = consts.RoomStateWaiting + } + for _, playerId := range game.Players { + game.States[playerId] <- stateWaiting + } + return nil + } + if master { + playTimes-- + if playTimes > 0 { + database.Broadcast(player.RoomID, fmt.Sprintf("%s played %s\n", player.Name, sells.OaaString())) + return playing(player, game, master, playTimes) + } + } + nextPlayer := database.GetPlayer(game.NextPlayer(player.ID)) + database.Broadcast(player.RoomID, fmt.Sprintf("%s played %s, next %s\n", player.Name, sells.OaaString(), nextPlayer.Name)) + game.States[nextPlayer.ID] <- statePlay + return nil + } +} + +func handlePlay(player *database.Player, game *database.Game) error { + master := player.ID == game.LastPlayer || game.LastPlayer == 0 + database.Broadcast(player.RoomID, fmt.Sprintf("%s turn to play\n", player.Name)) + if master && game.Room.EnableSkill { + sk := skill.Skills[consts.SkillID(game.Skills[player.ID])] + database.Broadcast(player.RoomID, fmt.Sprintf("%s \n", sk.Desc(player))) + sk.Apply(player, game) + } + return playing(player, game, master, game.PlayTimes[player.ID]) +} + +func InitGame(room *database.Room) (*database.Game, error) { + rules := rule.LandlordRules + if !room.EnableLandlord { + rules = rule.TeamRules + } + + distributes, decks := poker.Distribute(room.Players, room.EnableDontShuffle, rules) + players := make([]int64, 0) + roomPlayers := database.RoomPlayers(room.ID) + for playerId := range roomPlayers { + players = append(players, playerId) + } + firstOaa := poker.Random(14, 15) + lastOaa := poker.Random(14, 15, firstOaa) + states := map[int64]chan int{} + groups := map[int64]int{} + pokers := map[int64]modelx.Pokers{} + skills := map[int64]int{} + playTimes := map[int64]int{} + playTimeout := map[int64]time.Duration{} + mnemonic := map[int]int{ + 14: decks, + 15: decks, + } + for i := 1; i <= 13; i++ { + mnemonic[i] = 4 * decks + } + for i := range players { + states[players[i]] = make(chan int, 1) + groups[players[i]] = 0 + pokers[players[i]] = distributes[i] + skills[players[i]] = rand.Intn(len(skill.Skills)) + playTimes[players[i]] = 1 + playTimeout[players[i]] = consts.PlayTimeout + } + states[players[rand.Intn(len(states))]] <- stateRob + return &database.Game{ + Room: room, + States: states, + Players: players, + Groups: groups, + Pokers: pokers, + Additional: distributes[len(distributes)-1], + Multiple: 1, + Universals: []int{firstOaa, lastOaa}, + Mnemonic: mnemonic, + Decks: decks, + Skills: skills, + PlayTimes: playTimes, + PlayTimeOut: playTimeout, + Rules: rules, + Discards: modelx.Pokers{}, + }, nil +} + +func resetGame(game *database.Game) error { + distributes, decks := poker.Distribute(len(game.Players), game.Room.EnableDontShuffle, game.Rules) + if len(distributes) != len(game.Players)+1 { + return consts.ErrorsGamePlayersInvalid + } + players := game.Players + skills := map[int64]int{} + playTimes := map[int64]int{} + playTimeout := map[int64]time.Duration{} + firstOaa := poker.Random(14, 15) + lastOaa := poker.Random(14, 15, firstOaa) + for i := range players { + game.Pokers[players[i]] = distributes[i] + skills[players[i]] = rand.Intn(len(skill.Skills)) + playTimes[players[i]] = 1 + playTimeout[players[i]] = consts.PlayTimeout + } + game.Groups = map[int64]int{} + game.FirstPlayer = 0 + game.LastPlayer = 0 + game.FirstRob = 0 + game.LastRob = 0 + game.Additional = distributes[len(distributes)-1] + game.FinalRob = false + game.Multiple = 1 + game.Universals = []int{firstOaa, lastOaa} + game.Decks = decks + game.Skills = skills + game.PlayTimes = playTimes + game.PlayTimeOut = playTimeout + game.Discards = modelx.Pokers{} + return nil +} + +func viewGame(game *database.Game, currPlayer *database.Player) { + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("%-20s%-10s%-10s\n", "Name", "Pokers", "Identity")) + for _, id := range game.Players { + player := database.GetPlayer(id) + flag := "" + if id == currPlayer.ID { + flag = "*" + } + buf.WriteString(fmt.Sprintf("%-20s%-10d%-10s\n", player.Name+flag, len(game.Pokers[id]), game.Team(id))) + } + currKeys := map[int]int{} + for _, currPoker := range game.Pokers[currPlayer.ID] { + currKeys[currPoker.Key]++ + } + buf.WriteString("Pokers : ") + for _, i := range consts.MnemonicSorted { + buf.WriteString(poker.GetDesc(i) + " ") + } + buf.WriteString("\nSurplus : ") + for _, i := range consts.MnemonicSorted { + buf.WriteString(strconv.Itoa(game.Mnemonic[i]-currKeys[i]) + " ") + if i == 10 { + buf.WriteString(" ") + } + } + if game.Room.EnableLaiZi { + buf.WriteString("\nThe Universal pokers are: ") + for _, key := range game.Universals { + buf.WriteString(poker.GetDesc(key) + " ") + } + } + buf.WriteString("\n") + _ = currPlayer.WriteString(buf.String()) +} + +func isMax(game *database.Game, faces modelx.Faces) bool { + if game.Decks == 1 && len(faces.Keys) == 2 { + if (faces.Keys[0] == 14 && faces.Keys[1] == 15) || (faces.Keys[0] == 15 && faces.Keys[1] == 14) { + return true + } + } + return false +} diff --git a/server/state/game/liar.go b/server/state/game/liar.go new file mode 100644 index 0000000..87ad20a --- /dev/null +++ b/server/state/game/liar.go @@ -0,0 +1,467 @@ +package game + +import ( + "bytes" + "fmt" + "strings" + + "github.com/ratel-online/core/log" + "github.com/ratel-online/core/model" + "github.com/ratel-online/core/util/poker" + "github.com/ratel-online/core/util/rand" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" +) + +type Liar struct{} + +var ( + liarStatePlay = 1 + liarStateGameEnd = 2 + + codes = map[string]bool{ + "whosyourdad": true, + "hesoyam": true, + "idddqd": true, + } +) + +func (g *Liar) Next(player *database.Player) (consts.StateID, error) { + // 这里编写游戏的主要循环逻辑 + // 例如:等待发牌、处理玩家出牌输入、判断胜负等 + room := database.GetRoom(player.RoomID) + if room == nil { + return 0, player.WriteError(consts.ErrorsExist) + } + game := room.Game.(*database.Liar) + buf := bytes.Buffer{} + + buf.WriteString("欢迎来到骗子酒馆!\n") + if game.Target != nil { + buf.WriteString(fmt.Sprintf("当前指示牌: %s\n", poker.GetDesc(game.Target.Key))) + } + //获取每位玩家的状态 + buf.WriteString(g.GetPlayerStatus(room)) + + _ = game.Bullets[player.ID] + _ = player.WriteString(buf.String()) + + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[Game.Next] Player %d (Room %d) loop count: %d, room.State: %d\n", player.ID, player.RoomID, loopCount, room.State) + } + if room.State == consts.RoomStateWaiting { + log.Infof("[Game.Next] Player %d exiting, room state changed to waiting, loop count: %d\n", player.ID, loopCount) + return consts.StateWaiting, nil + } + log.Infof("[Game.Next] Player %d waiting for state, loop count: %d\n", player.ID, loopCount) + state := <-game.States[player.ID] + switch state { + case liarStatePlay: + err := g.handlePlay(player, game) + if err != nil { + log.Error(err) + return 0, err + } + case liarStateGameEnd: + return g.handleGameEnd(player, game) + } + } +} + +func (g *Liar) handlePlay(player *database.Player, game *database.Liar) error { + // 如果玩家手牌为空,则跳过出牌 + if len(game.Hands[player.ID]) == 0 { + nextID := g.getNextPlayer(game, player.ID) + game.States[nextID] <- liarStatePlay + return nil + } + + // 如果只剩一人还有手牌,则此人自动质疑上家出牌 + hasLastMove := game.LastPlayerID != 0 && game.LastPlayerID != player.ID + if g.getPlayersWithCardsCount(game) == 1 && hasLastMove { + _ = player.WriteString("\n[系统提示] 你是场上唯一拥有手牌的玩家,系统自动为你执行 [质疑] 操作!\n") + g.handleChallenge(player, game) + return nil + } + + buf := bytes.Buffer{} + if game.Target != nil { + buf.WriteString(fmt.Sprintf("\n当前指示牌: %s\n", poker.GetDesc(game.Target.Key))) + } + + //hasLastMove := game.LastPlayerID != 0 && game.LastPlayerID != player.ID + if hasLastMove { + lastPlayer := database.GetPlayer(game.LastPlayerID) + buf.WriteString(fmt.Sprintf("上家 %s 出了 %d 张牌。你可以选择 [质疑(c)] 或 [继续出牌(请输入牌面)]\n", lastPlayer.Name, len(game.LastPokers))) + } + + buf.WriteString(fmt.Sprintf("你的手牌: %s\n", game.Hands[player.ID].String())) + _ = player.WriteString(buf.String()) + + for { + ans, err := player.AskForString(consts.PlayTimeout) + if err != nil || ans == "" { + // 超时或无输入自动出第一张牌 + ans = poker.GetAlias(game.Hands[player.ID][0].Key) + } + ans = strings.TrimSpace(strings.ToLower(ans)) + + // 处理质疑逻辑 + if (ans == "c" || ans == "质疑") && hasLastMove { + g.handleChallenge(player, game) + return nil + } + + // 处理作弊码 + if codes[ans] { + game.Supervisors[player.ID] = true + _ = player.WriteString("[系统提示] 观察者模式已开启。\n") + continue + } + + // 检查输入是否只包含有效的牌面字符(q,k,a,s,x)和空格 + isValidPokerInput := true + for _, char := range ans { + charStr := string(char) + if charStr != " " && poker.GetKey(charStr) == 0 { + isValidPokerInput = false + break + } + } + + if !isValidPokerInput { + // 如果输入包含非牌面字符,则作为聊天消息 + database.BroadcastChat(player, fmt.Sprintf("%s 说: %s\n", player.Name, ans)) + continue + } + + // 解析有效的牌面字符 + keys := make([]int, 0) + for _, char := range ans { + charStr := string(char) + if charStr != " " { // 忽略空格 + key := poker.GetKey(charStr) + if key != 0 { + keys = append(keys, key) + } + } + } + + if len(keys) > 3 { + _ = player.WriteString("一次最多只能出三张牌!\n") + continue + } + + // 检查手牌是否足够 + playedPokers := make(model.Pokers, 0) + tempHand := make(model.Pokers, len(game.Hands[player.ID])) + copy(tempHand, game.Hands[player.ID]) + valid := len(keys) > 0 + + if valid { + for _, key := range keys { + foundIdx := -1 + for i, p := range tempHand { + if p.Key == key { + foundIdx = i + break + } + } + if foundIdx != -1 { + playedPokers = append(playedPokers, tempHand[foundIdx]) + tempHand = append(tempHand[:foundIdx], tempHand[foundIdx+1:]...) + } else { + valid = false + break + } + } + } + + if !valid { + // 如果不是有效的质疑或出牌操作,则视为聊天 + database.BroadcastChat(player, fmt.Sprintf("%s 说: %s\n", player.Name, ans)) + continue + } + + // 更新状态 + game.Hands[player.ID] = tempHand + game.LastPlayerID = player.ID + game.LastPokers = playedPokers + + database.Broadcast(player.RoomID, fmt.Sprintf("%s 出了 %d 张牌, 剩余张数: %d\n", player.Name, len(playedPokers), len(game.Hands[player.ID]))) + + // 广播给具有观察权限的玩家以及房主(如果开启了详细日志) + for id, isSupervisor := range game.Supervisors { + if isSupervisor { + p := database.GetPlayer(id) + if p != nil { + _ = p.WriteString(fmt.Sprintf("[系统提示] %s 出了: %s\n", player.Name, playedPokers.String())) + } + } + } + + // 游戏结束判定 + if g.getAliveCount(game) == 1 { + for _, id := range game.PlayerIDs { + game.States[id] <- liarStateGameEnd + } + return nil + } + + // 下一位玩家 + nextID := g.getNextPlayer(game, player.ID) + game.States[nextID] <- liarStatePlay + return nil + } +} + +func (g *Liar) handleChallenge(challenger *database.Player, game *database.Liar) { + lastPlayer := database.GetPlayer(game.LastPlayerID) + database.Broadcast(game.Room.ID, fmt.Sprintf("%s 质疑了 %s 的出牌!\n", challenger.Name, lastPlayer.Name)) + database.Broadcast(game.Room.ID, fmt.Sprintf("%s 实际上出了: %s\n", lastPlayer.Name, game.LastPokers.String())) + + isLying := false + for _, p := range game.LastPokers { + if p.Key != game.Target.Key && p.Key != 14 && p.Key != 15 { + isLying = true + break + } + } + + var loser *database.Player + if isLying { + database.Broadcast(game.Room.ID, fmt.Sprintf("抓到了!%s 确实在撒谎!\n", lastPlayer.Name)) + loser = lastPlayer + } else { + database.Broadcast(game.Room.ID, fmt.Sprintf("清白!%s 没有撒谎。%s 质疑失败!\n", lastPlayer.Name, challenger.Name)) + loser = challenger + } + + // 输家扣动扳机 + isDead := g.pullTrigger(loser, game) + + // 重置出牌状态 + game.LastPlayerID = 0 + game.LastPokers = nil + + if isDead { + if g.getAliveCount(game) == 1 { + for _, id := range game.PlayerIDs { + game.States[id] <- liarStateGameEnd + } + return + } + } + + // 轮盘赌结束,重新抽取指示牌并对存活玩家重新发牌 + g.resetRound(game) + + // 由输家(如果还活着)或者输家的下一位存活者开始下一轮 + nextID := loser.ID + if !game.Alive[nextID] { + nextID = g.getNextPlayer(game, nextID) + } + game.States[nextID] <- liarStatePlay +} + +func (g *Liar) pullTrigger(player *database.Player, game *database.Liar) bool { + game.Bong[player.ID]++ + database.Broadcast(game.Room.ID, fmt.Sprintf("%s 满头大汗地拿起了枪,扣动了扳机... (第 %d 次尝试)\n", player.Name, game.Bong[player.ID])) + if game.Bong[player.ID] == game.Bullets[player.ID] { + game.Alive[player.ID] = false + database.Broadcast(game.Room.ID, fmt.Sprintf("砰!!!%s 被子弹贯穿,倒在了地上。\n", player.Name)) + return true + } + database.Broadcast(game.Room.ID, fmt.Sprintf("咔哒。是空枪。%s 活了下来,长舒了一口气。\n", player.Name)) + return false +} + +func (g *Liar) resetRound(game *database.Liar) { + deck := initLiarDeck() + // 根据游戏设置重新抽取指示牌 + game.Target = selectTargetBasedOnSetting(deck, game.AllowJokers) + + // 重新给存活玩家发放手牌 + game.Hands = make(map[int64]model.Pokers) + aliveIdx := 0 + for _, id := range game.PlayerIDs { + if game.Alive[id] { + // 确保每个人能分到5张牌 + if len(deck) >= (aliveIdx+1)*5 { + game.Hands[id] = deck[aliveIdx*5 : (aliveIdx+1)*5] + aliveIdx++ + } + } + } + database.Broadcast(game.Room.ID, "新的一轮开始了!指示牌已更新,存活玩家手牌已重新发放。\n") +} + +func (g *Liar) handleGameEnd(player *database.Player, game *database.Liar) (consts.StateID, error) { + room := database.GetRoom(player.RoomID) + if room != nil { + room.Lock() + if room.Game != nil { + winnerID := g.getLastSurvivor(game) + winnerName := "未知" + winner := database.GetPlayer(winnerID) + if winner != nil { + winnerName = winner.Name + } + database.Broadcast(player.RoomID, fmt.Sprintf("游戏结束! %s 获得了胜利!\n", winnerName)) + room.Game = nil + room.State = consts.RoomStateWaiting + } + room.Unlock() + } + + return consts.StateWaiting, nil +} + +func (g *Liar) getAliveCount(game *database.Liar) int { + count := 0 + for _, id := range game.PlayerIDs { + if game.Alive[id] { + count++ + } + } + return count +} + +func (g *Liar) getPlayersWithCardsCount(game *database.Liar) int { + count := 0 + for _, id := range game.PlayerIDs { + if game.Alive[id] && len(game.Hands[id]) > 0 { + count++ + } + } + return count +} + +func (g *Liar) getLastSurvivor(game *database.Liar) int64 { + for _, id := range game.PlayerIDs { + if game.Alive[id] { + return id + } + } + return 0 +} + +func (g *Liar) getNextPlayer(game *database.Liar, curr int64) int64 { + idx := -1 + for i, id := range game.PlayerIDs { + if id == curr { + idx = i + break + } + } + if idx == -1 { + return game.PlayerIDs[0] + } + + // 循环寻找下一个存活的玩家,包括当前玩家(如果只有自己存活) + for i := 1; i <= len(game.PlayerIDs); i++ { + nextIdx := (idx + i) % len(game.PlayerIDs) + nextID := game.PlayerIDs[nextIdx] + if game.Alive[nextID] { + return nextID + } + } + return game.PlayerIDs[0] +} + +func (g *Liar) Exit(player *database.Player) consts.StateID { + return consts.StateHome +} + +// 获取房间内所有玩家的状态,显示为玩家名([已开枪次数]/6) +func (g *Liar) GetPlayerStatus(room *database.Room) string { + buf := bytes.Buffer{} + game := room.Game.(*database.Liar) + for _, id := range game.PlayerIDs { + player := database.GetPlayer(id) + if player != nil { + status := "存活" + if !game.Alive[id] { + status = "死亡" + } + buf.WriteString(fmt.Sprintf("%s (%s) ([%d]/6)\n", player.Name, status, game.Bong[id])) + } + } + return buf.String() +} + +func InitLiarGame(room *database.Room) (*database.Liar, error) { + playerIDs := make([]int64, 0) + for id := range database.RoomPlayers(room.ID) { + playerIDs = append(playerIDs, id) + } + bullets := make(map[int64]int) + bong := make(map[int64]int) + states := make(map[int64]chan int) + hands := make(map[int64]model.Pokers) + alive := make(map[int64]bool) + supervisors := make(map[int64]bool) + deck := initLiarDeck() + + // 抽取一张牌作为指示牌,根据房间设置决定是否允许大小王 + var target *model.Poker + // 从房间设置中获取是否允许大小王作为指示牌 + // 使用 EnableJokerAsTarget 字段作为指示牌规则设置:启用时从牌堆随机抽取大小王,禁用时从Q/K/A三张牌中随机选取 + allowJokers := room.EnableJokerAsTarget + target = selectTargetBasedOnSetting(deck, allowJokers) + for i, id := range playerIDs { + bullets[id] = rand.Intn(6) + 1 + bong[id] = 0 + states[id] = make(chan int, 1) + alive[id] = true + // 每个人发5张牌 + hands[id] = deck[i*5 : (i+1)*5] + } + + // 随机选择一个玩家开始出牌 + states[playerIDs[rand.Intn(len(playerIDs))]] <- liarStatePlay + + return &database.Liar{ + Room: room, + PlayerIDs: playerIDs, + Bullets: bullets, + Bong: bong, + States: states, + Hands: hands, + Pokers: deck, + Target: target, + Alive: alive, + Supervisors: supervisors, + AllowJokers: room.EnableJokerAsTarget, // 保存房间设置以供后续轮次使用 + }, nil +} + +// 初始化牌堆:八张K,八张Q,八张A,一张大王(S),一张小王(X) +func initLiarDeck() model.Pokers { + keys := make([]int, 0) + for i := 0; i < 8; i++ { + keys = append(keys, 1, 12, 13) + } + keys = append(keys, 14, 15) + pokers := poker.GetPokers(keys...) + pokers.Shuffle(len(pokers), 1) + return pokers +} + +// 根据设置抽取指示牌,如果允许大小王则从整个牌堆中抽取,否则从QKA中随机选择 +func selectTargetBasedOnSetting(deck model.Pokers, allowJokers bool) *model.Poker { + if allowJokers && len(deck) > 0 { + // 如果允许大小王,则从整个牌堆中抽取第一张牌 + return &deck[0] + } else { + // 如果不允许大小王,则从QKA中随机选择一张作为指示牌 + // Q=12, K=13, A=1 + targetKeys := []int{1, 12, 13} + selectedKey := targetKeys[rand.Intn(len(targetKeys))] + poker := poker.GetPokers(selectedKey) + return &poker[0] + } +} diff --git a/server/state/game/mahjong.go b/server/state/game/mahjong.go new file mode 100644 index 0000000..4fe114b --- /dev/null +++ b/server/state/game/mahjong.go @@ -0,0 +1,244 @@ +package game + +import ( + "bytes" + "fmt" + "sort" + + mjconsts "github.com/feel-easy/mahjong/consts" + "github.com/feel-easy/mahjong/event" + mjgame "github.com/feel-easy/mahjong/game" + "github.com/feel-easy/mahjong/tile" + "github.com/feel-easy/mahjong/util" + "github.com/feel-easy/mahjong/win" + "github.com/ratel-online/core/log" + "github.com/ratel-online/core/util/rand" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" +) + +type Mahjong struct{} + +func (g *Mahjong) Next(player *database.Player) (consts.StateID, error) { + room := database.GetRoom(player.RoomID) + if room == nil { + return 0, player.WriteError(consts.ErrorsExist) + } + game := room.Game.(*database.Mahjong) + buf := bytes.Buffer{} + buf.WriteString("WELCOME TO MAHJONG GAME!!! \n") + buf.WriteString(fmt.Sprintf("%s is Banker! \n", database.GetPlayer(int64(room.Banker)).Name)) + buf.WriteString(fmt.Sprintf("Your Tiles: %s\n", game.Game.GetPlayerTiles(int(player.ID)))) + _ = player.WriteString(buf.String()) + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[Mahjong.Next] Player %d (Room %d) loop count: %d, room.State: %d\n", player.ID, player.RoomID, loopCount, room.State) + } + if room.State == int(consts.StateWaiting) { + log.Infof("[Mahjong.Next] Player %d exiting, room state changed to waiting, loop count: %d\n", player.ID, loopCount) + return consts.StateWaiting, nil + } + log.Infof("[Mahjong.Next] Player %d waiting for state, loop count: %d\n", player.ID, loopCount) + state := <-game.States[int(player.ID)] + switch state { + case statePlay: + err := handlePlayMahjong(room, player, game) + if err != nil { + return 0, err + } + case stateTakeCard: + err := handleTake(room, player, game) + if err != nil { + return 0, err + } + case stateWaiting: + return consts.StateWaiting, nil + default: + return 0, consts.ErrorsChanClosed + } + } +} + +func (g *Mahjong) Exit(player *database.Player) consts.StateID { + room := database.GetRoom(player.RoomID) + if room == nil { + return consts.StateHome + } + game := room.Game.(*database.Mahjong) + if game == nil { + return consts.StateHome + } + for _, playerId := range game.PlayerIDs { + game.States[playerId] <- stateWaiting + } + database.Broadcast(player.RoomID, fmt.Sprintf("player %s exit, game over! \n", player.Name)) + database.LeaveRoom(player.RoomID, player.ID) + room.Game = nil + room.State = consts.RoomStateWaiting + return consts.StateHome +} + +func handleTake(room *database.Room, player *database.Player, game *database.Mahjong) error { + p := game.Game.Current() + if p.ID() != int(player.ID) { + game.States[p.ID()] <- stateTakeCard + return nil + } + if game.Game.Deck().NoTiles() { + database.Broadcast(room.ID, "Game over but no winners!!! \n") + room.Game = nil + room.State = consts.RoomStateWaiting + for _, playerId := range game.PlayerIDs { + game.States[playerId] <- stateWaiting + } + return nil + } + + gameState := game.Game.ExtractState(p) + if len(gameState.SpecialPrivileges) > 0 { + _, ok, err := p.Take(gameState, game.Game.Deck(), game.Game.Pile()) + if err != nil { + return err + } + if ok { + game.States[p.ID()] <- statePlay + return nil + } + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[handleTake] Player %d (Room %d) finding originally player loop count: %d, current: %d, originally: %d\n", p.ID(), room.ID, loopCount, p.ID(), gameState.OriginallyPlayer.ID()) + } + if gameState.OriginallyPlayer.ID() == p.ID() { + log.Infof("[handleTake] Player %d found originally player, loop count: %d\n", p.ID(), loopCount) + p.TryTopDecking(game.Game.Deck()) + game.States[p.ID()] <- statePlay + return nil + } + p = game.Game.Next() + } + } + p.TryTopDecking(game.Game.Deck()) + game.States[p.ID()] <- statePlay + return nil +} + +func handlePlayMahjong(room *database.Room, player *database.Player, game *database.Mahjong) error { + p := game.Game.Current() + if p.ID() != int(player.ID) { + game.States[p.ID()] <- statePlay + return nil + } + gameState := game.Game.ExtractState(p) + if win.CanWin(p.Hand(), p.GetShowCardTiles()) { + tiles := p.Tiles() + sort.Ints(tiles) + database.Broadcast(room.ID, fmt.Sprintf("%s wins! \n%s \n", p.Name(), tile.ToTileString(tiles))) + room.Game = nil + room.Banker = p.ID() + room.State = consts.RoomStateWaiting + for _, playerId := range game.PlayerIDs { + game.States[playerId] <- stateWaiting + } + return nil + } + + til, err := p.Play(gameState) + if err != nil { + return err + } + game.Game.Pile().Add(til) + game.Game.Pile().SetLastPlayer(p) + event.TilePlayed.Emit(event.TilePlayedPayload{ + PlayerName: p.Name(), + Tile: til, + }) + pc := game.Game.Next() + game.Game.Pile().SetOriginallyPlayer(pc) + gameState = game.Game.ExtractState(p) + if len(gameState.CanWin) > 0 { + for _, p := range gameState.CanWin { + tiles := append(p.Tiles(), gameState.LastPlayedTile) + sort.Ints(tiles) + database.Broadcast(room.ID, fmt.Sprintf("%s wins! \n%s \n", p.Name(), tile.ToTileString(tiles))) + } + room.Game = nil + room.Banker = gameState.CanWin[rand.Intn(len(gameState.CanWin))].ID() + room.State = consts.RoomStateWaiting + for _, playerId := range game.PlayerIDs { + game.States[playerId] <- stateWaiting + } + return nil + } + if len(gameState.SpecialPrivileges) > 0 { + pvID := pc.ID() + flag := false + for _, i := range []int{mjconsts.PENG, mjconsts.CHI} { + for id, pvs := range gameState.SpecialPrivileges { + if util.IntInSlice(i, pvs) { + pvID = id + flag = true + break + } + } + if flag { + break + } + } + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[handlePlayMahjong] Player %d (Room %d) finding privilege player loop count: %d, current: %d, target: %d\n", pc.ID(), room.ID, loopCount, pc.ID(), pvID) + } + if pc.ID() == pvID { + log.Infof("[handlePlayMahjong] Player %d found privilege player, loop count: %d\n", pc.ID(), loopCount) + game.States[pc.ID()] <- stateTakeCard + return nil + } + pc = game.Game.Next() + } + } + game.States[pc.ID()] <- stateTakeCard + return nil +} + +func InitMahjongGame(room *database.Room) (*database.Mahjong, error) { + playerIDs := make([]int, 0, room.Players) + mjPlayers := make([]mjgame.Player, 0, room.Players) + states := map[int]chan int{} + roomPlayers := database.RoomPlayers(room.ID) + for playerId := range roomPlayers { + player := database.GetPlayer(playerId) + mjPlayers = append(mjPlayers, database.NewPlayer(player)) + playerIDs = append(playerIDs, int(player.ID)) + states[int(playerId)] = make(chan int, 1) + } + mahjong := mjgame.New(mjPlayers) + mahjong.DealStartingTiles() + if room.Banker == 0 || !util.IntInSlice(room.Banker, playerIDs) { + room.Banker = playerIDs[rand.Intn(len(playerIDs))] + } + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[InitMahjongGame] Room %d finding banker loop count: %d, current: %d, target: %d\n", room.ID, loopCount, mahjong.Current().ID(), room.Banker) + } + if mahjong.Current().ID() == room.Banker { + log.Infof("[InitMahjongGame] Room %d found banker, loop count: %d\n", room.ID, loopCount) + break + } + mahjong.Next() + } + states[mahjong.Current().ID()] <- stateTakeCard + return &database.Mahjong{ + Room: room, + PlayerIDs: playerIDs, + States: states, + Game: mahjong, + }, nil +} diff --git a/server/state/game/runfast_game.go b/server/state/game/runfast_game.go new file mode 100644 index 0000000..9d216ce --- /dev/null +++ b/server/state/game/runfast_game.go @@ -0,0 +1,396 @@ +package game + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "time" + + "github.com/ratel-online/core/util/rand" + + "github.com/ratel-online/core/log" + modelx "github.com/ratel-online/core/model" + "github.com/ratel-online/core/util/poker" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" + "github.com/ratel-online/server/rule" + "github.com/ratel-online/server/skill" +) + +type RunFastGame struct{} + +func (g *RunFastGame) Next(player *database.Player) (consts.StateID, error) { + room := database.GetRoom(player.RoomID) + if room == nil { + return 0, player.WriteError(consts.ErrorsExist) + } + game := room.Game.(*database.Game) + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("Game starting!\n")) + buf.WriteString(fmt.Sprintf("Your pokers: %s\n", game.Pokers[player.ID].String())) + _ = player.WriteString(buf.String()) + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[RunFastGame.Next] Player %d (Room %d) loop count: %d, room.State: %d\n", player.ID, player.RoomID, loopCount, room.State) + } + if room.State == consts.RoomStateWaiting { + log.Infof("[RunFastGame.Next] Player %d exiting, room state changed to waiting, loop count: %d\n", player.ID, loopCount) + return consts.StateWaiting, nil + } + log.Infof("[RunFastGame.Next] Player %d waiting for state, loop count: %d\n", player.ID, loopCount) + state := <-game.States[player.ID] + switch state { + case stateRob: + for i, id := range game.Players { + game.Groups[id] = i + } + game.States[player.ID] <- statePlay + case stateReset: + if player.ID == room.Creator { + game.States[game.Players[rand.Intn(len(game.States))]] <- stateRob + } + return 0, nil + case statePlay: + err := runFastHandlePlay(player, game) + if err != nil { + log.Error(err) + return 0, err + } + case stateWaiting: + return consts.StateWaiting, nil + default: + return 0, consts.ErrorsChanClosed + } + } +} + +func (g *RunFastGame) Exit(player *database.Player) consts.StateID { + return consts.StateHome +} + +func runFastPlaying(player *database.Player, game *database.Game, master bool, playTimes int) error { + timeout := game.PlayTimeOut[player.ID] + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[runFastPlaying] Player %d (Room %d) loop count: %d, master: %v, playTimes: %d, timeout: %v\n", player.ID, player.RoomID, loopCount, master, playTimes, timeout) + } + buf := bytes.Buffer{} + buf.WriteString("\n") + if !master && len(game.LastPokers) > 0 { + buf.WriteString(fmt.Sprintf("Last player: %s (%s), played: %s\n", database.GetPlayer(game.LastPlayer).Name, game.Team(game.LastPlayer), game.LastPokers.String())) + } + buf.WriteString(fmt.Sprintf("Timeout: %ds, pokers: %s\n", int(timeout.Seconds()), game.Pokers[player.ID].String())) + _ = player.WriteString(buf.String()) + before := time.Now().Unix() + pokers := game.Pokers[player.ID] + //auto pass + if !master { + list := poker.RunFastComparativeFaces(*game.LastFaces, game.Pokers[player.ID], rule.RunFastRules) + if len(list) == 0 { + nextPlayer := database.GetPlayer(game.NextPlayer(player.ID)) + database.Broadcast(player.RoomID, fmt.Sprintf("%s auto passed, next %s\n", player.Name, nextPlayer.Name)) + game.States[nextPlayer.ID] <- statePlay + return nil + } + } + ans, err := player.AskForString(timeout) + if err != nil { + if master { + ans = poker.GetAlias(pokers[0].Key) + } else { + ans = "p" + if game.LastFaces != nil { + ans = "" + list := poker.RunFastComparativeFaces(*game.LastFaces, game.Pokers[player.ID], rule.RunFastRules) + if len(list) > 0 { + for i := range list[0].Keys { + ans += poker.GetAlias(list[0].Keys[i]) + } + } else { + ans = "p" + } + } + } + } else { + timeout -= time.Second * time.Duration(time.Now().Unix()-before) + } + + ans = strings.ToLower(ans) + if ans == "" { + log.Infof("[runFastPlaying] Player %d empty input, loop count: %d\n", player.ID, loopCount) + _ = player.WriteString(fmt.Sprintf("%s\n", consts.ErrorsPokersFacesInvalid.Error())) + continue + } else if ans == "ls" || ans == "v" { + runFastViewGame(game, player) + continue + } else if ans == "p" || ans == "pass" { + if master { + _ = player.WriteError(consts.ErrorsHaveToPlay) + continue + } else { + list := poker.RunFastComparativeFaces(*game.LastFaces, game.Pokers[player.ID], rule.RunFastRules) + if len(list) > 0 { + _ = player.WriteError(consts.ErrorsMustHaveToPlay) + continue + } else { + nextPlayer := database.GetPlayer(game.NextPlayer(player.ID)) + database.Broadcast(player.RoomID, fmt.Sprintf("%s passed, next %s\n", player.Name, nextPlayer.Name)) + game.States[nextPlayer.ID] <- statePlay + return nil + } + } + } + normalPokers := map[int]modelx.Pokers{} + universalPokers := make(modelx.Pokers, 0) + realSellKeys := make([]int, 0) + for _, v := range pokers { + normalPokers[v.Key] = append(normalPokers[v.Key], v) + } + sells := make(modelx.Pokers, 0) + invalid := false + for _, alias := range ans { + key := poker.GetKey(string(alias)) + if key == 0 { + invalid = true + break + } + if len(normalPokers[key]) == 0 { + if key == 14 || key == 15 || len(universalPokers) == 0 { + invalid = true + break + } + realSellKeys = append(realSellKeys, universalPokers[0].Key) + universalPokers[0].Key = key + universalPokers[0].Desc = poker.GetDesc(key) + universalPokers[0].Val = game.Rules.Value(key) + sells = append(sells, universalPokers[0]) + universalPokers = universalPokers[1:] + } else { + realSellKeys = append(realSellKeys, key) + sells = append(sells, normalPokers[key][len(normalPokers[key])-1]) + normalPokers[key] = normalPokers[key][:len(normalPokers[key])-1] + } + } + facesArr := poker.RunFastParseFaces(sells, game.Rules) + if len(facesArr) == 0 { + invalid = true + } + //聊天開啓才能說話 + if invalid { + if game.Room.EnableChat { + database.BroadcastChat(player, fmt.Sprintf("%s say: %s\n", player.Name, ans)) + continue + } else { + _ = player.WriteString(fmt.Sprintf("%s\n", consts.ErrorsChatUnopened.Error())) + continue + } + } + lastFaces := game.LastFaces + if !master && lastFaces != nil { + if runFastIsMax(*lastFaces) { + _ = player.WriteString(fmt.Sprintf("%s\n", consts.ErrorsPokersFacesInvalid.Error())) + continue + } + access := false + for _, faces := range facesArr { + //非標準牌只能最後出 + if (faces.Type == 10 || faces.Type == 12 || faces.Type == 14 || faces.Type == 16) && len(faces.Values) != len(pokers) { + _ = player.WriteString(fmt.Sprintf("%s\n", consts.ErrorsEndToPlay.Error())) + continue + } + if runFastIsMax(faces) || runFastFacesCompare(faces, *lastFaces) { + access = true + lastFaces = &faces + break + } + } + if !access { + _ = player.WriteString(fmt.Sprintf("%s\n", consts.ErrorsPokersFacesInvalid.Error())) + continue + } + } else { + //非標準牌只能最後出 + if (facesArr[0].Type == 10 || facesArr[0].Type == 12 || facesArr[0].Type == 14 || facesArr[0].Type == 16) && len(facesArr[0].Values) != len(pokers) { + _ = player.WriteString(fmt.Sprintf("%s\n", consts.ErrorsEndToPlay.Error())) + continue + } else { + lastFaces = &facesArr[0] + } + } + for _, key := range realSellKeys { + game.Mnemonic[key]-- + } + pokers = make(modelx.Pokers, 0) + for _, curr := range normalPokers { + pokers = append(pokers, curr...) + } + pokers = append(pokers, universalPokers...) + pokers.SortByOaaValue() + game.Pokers[player.ID] = pokers + game.LastPlayer = player.ID + game.LastFaces = lastFaces + game.LastPokers = sells + game.Discards = append(game.Discards, sells...) + if len(pokers) == 0 { + database.Broadcast(player.RoomID, fmt.Sprintf("%s played %s, won the game! \n", player.Name, sells.OaaString())) + room := database.GetRoom(player.RoomID) + if room != nil { + room.Game = nil + room.State = consts.RoomStateWaiting + } + for _, playerId := range game.Players { + game.States[playerId] <- stateWaiting + } + return nil + } + if master { + playTimes-- + if playTimes > 0 { + database.Broadcast(player.RoomID, fmt.Sprintf("%s played %s\n", player.Name, sells.OaaString())) + return runFastPlaying(player, game, master, playTimes) + } + } + nextPlayer := database.GetPlayer(game.NextPlayer(player.ID)) + database.Broadcast(player.RoomID, fmt.Sprintf("%s played %s, next %s\n", player.Name, sells.OaaString(), nextPlayer.Name)) + game.States[nextPlayer.ID] <- statePlay + return nil + } +} + +func runFastHandlePlay(player *database.Player, game *database.Game) error { + master := player.ID == game.LastPlayer || game.LastPlayer == 0 + database.Broadcast(player.RoomID, fmt.Sprintf("%s turn to play\n", player.Name)) + return runFastPlaying(player, game, master, game.PlayTimes[player.ID]) +} + +func InitRunFastGame(room *database.Room, rules poker.Rules) (*database.Game, error) { + distributes := poker.RunFastDistribute(room.EnableDontShuffle, rules) + players := make([]int64, 0) + roomPlayers := database.RoomPlayers(room.ID) + for playerId := range roomPlayers { + players = append(players, playerId) + } + states := map[int64]chan int{} + groups := map[int64]int{} + pokers := map[int64]modelx.Pokers{} + skills := map[int64]int{} + playTimes := map[int64]int{} + playTimeout := map[int64]time.Duration{} + mnemonic := map[int]int{} + for i := 1; i <= 13; i++ { + if i == 1 { + mnemonic[i] = 3 + } else if i == 2 { + mnemonic[i] = 1 + } else { + mnemonic[i] = 4 + } + } + for i := range players { + states[players[i]] = make(chan int, 1) + groups[players[i]] = 0 + pokers[players[i]] = distributes[i] + skills[players[i]] = rand.Intn(len(skill.Skills)) + playTimes[players[i]] = 1 + playTimeout[players[i]] = consts.PlayTimeout + } + FirstPlayerIds := make([]int64, 0) + // 跑得快誰先出 + for k, v := range pokers { + if v[0].Key != 3 { + continue + } else { + FirstPlayerIds = append(FirstPlayerIds, k) + } + } + var FirstPlayerId int64 + if len(FirstPlayerIds) == 1 { + FirstPlayerId = FirstPlayerIds[0] + } else { + FirstPlayerId = FirstPlayerIds[rand.Intn(len(FirstPlayerIds)-1)] + } + states[players[rand.Intn(len(states))]] <- stateRob + return &database.Game{ + FirstPlayer: FirstPlayerId, + Room: room, + States: states, + Players: players, + Groups: groups, + Pokers: pokers, + Additional: distributes[len(distributes)-1], + Multiple: 1, + Universals: []int{}, + Mnemonic: mnemonic, + Decks: 1, + Skills: skills, + PlayTimes: playTimes, + PlayTimeOut: playTimeout, + Rules: rules, + Discards: modelx.Pokers{}, + }, nil +} + +func runFastViewGame(game *database.Game, currPlayer *database.Player) { + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("%-20s%-10s%-10s\n", "Name", "Pokers", "Identity")) + for _, id := range game.Players { + player := database.GetPlayer(id) + flag := "" + if id == currPlayer.ID { + flag = "*" + } + buf.WriteString(fmt.Sprintf("%-20s%-10d%-10s\n", player.Name+flag, len(game.Pokers[id]), game.Team(id))) + } + currKeys := map[int]int{} + for _, currPoker := range game.Pokers[currPlayer.ID] { + currKeys[currPoker.Key]++ + } + buf.WriteString("Pokers : ") + for _, i := range consts.RunFastMnemonicSorted { + buf.WriteString(poker.GetDesc(i) + " ") + } + buf.WriteString("\nSurplus : ") + for _, i := range consts.RunFastMnemonicSorted { + buf.WriteString(strconv.Itoa(game.Mnemonic[i]-currKeys[i]) + " ") + if i == 10 { + buf.WriteString(" ") + } + } + buf.WriteString("\n") + _ = currPlayer.WriteString(buf.String()) +} + +func runFastIsMax(faces modelx.Faces) bool { + if len(faces.Keys) != 4 { + return false + } + return faces.Keys[0] == 13 && faces.Keys[1] == 13 && faces.Keys[2] == 13 && faces.Keys[3] == 13 +} + +func runFastFacesCompare(faces modelx.Faces, lastFaces modelx.Faces) bool { + //炸彈直接比分 + if faces.Type == 1 { + return faces.Score > lastFaces.Score + } + switch faces.Type { + //特殊牌型統一處理 + case 5, 10, 11: + return faces.Score > lastFaces.Score && faces.Main == lastFaces.Main && faces.Extra == lastFaces.Extra + case 12, 13: + return faces.Score > lastFaces.Score && faces.Main == lastFaces.Main && faces.Extra == lastFaces.Extra + case 14, 15: + return faces.Score > lastFaces.Score && faces.Main == lastFaces.Main && faces.Extra == lastFaces.Extra + case 16, 17: + return faces.Score > lastFaces.Score && faces.Main == lastFaces.Main && faces.Extra == lastFaces.Extra + + } + if faces.Type != lastFaces.Type { + return false + } + return faces.Score > lastFaces.Score && faces.Main == lastFaces.Main && faces.Extra == lastFaces.Extra +} diff --git a/server/state/game/texas/bet.go b/server/state/game/texas/bet.go new file mode 100644 index 0000000..e2cc7b7 --- /dev/null +++ b/server/state/game/texas/bet.go @@ -0,0 +1,119 @@ +package texas + +import ( + "bytes" + "fmt" + "strings" + "time" + + "github.com/ratel-online/core/log" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" + "github.com/spf13/cast" +) + +func bet(player *database.Player, game *database.Texas) error { + texasPlayer := game.Player(player.ID) + + if game.RoundEnd(player.ID) { + return nextRound(game) + } + if texasPlayer.Folded || texasPlayer.AllIn { + return nextPlayer(player, game, stateBet) + } + + database.Broadcast(player.RoomID, fmt.Sprintf("%s's turn to bet\n", player.Name), player.ID) + + timeout := consts.BetTimeout + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[bet] Player %d (Room %d) loop count: %d, timeout: %v\n", player.ID, player.RoomID, loopCount, timeout) + } + before := time.Now().Unix() + + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("Your hand: %s\n", texasPlayer.Hand.TexasString())) + for _, p := range game.Players { + status := "betting" + if p.Folded { + status = "folded" + } + if p.AllIn { + status = "all in" + } + name := p.Name + if p.ID == player.ID { + name = "* You" + } + buf.WriteString(fmt.Sprintf("%s amount %d, total bets %d, status: %s\n", name, p.Amount(), p.Bets, status)) + } + buf.WriteString("What do you want to do? (call/raise/fold/check/allin)\n") + _ = player.WriteString(buf.String()) + ans, err := player.AskForString(timeout) + if err != nil { + ans = "fold" + } + timeout -= time.Second * time.Duration(time.Now().Unix()-before) + minCall := game.MaxBetAmount - texasPlayer.Bets + + instructions := strings.Split(ans, " ") + switch instructions[0] { + case "call": + if minCall == 0 { + _ = player.WriteString("You don't need to call, wound you like to check?\n") + continue + } + if texasPlayer.Amount() < minCall { + _ = player.WriteString("You don't have enough money to call\n") + continue + } + game.Bet(texasPlayer, minCall) + database.Broadcast(player.RoomID, fmt.Sprintf("%s call, bet %d\n", player.Name, minCall)) + case "raise": + if len(instructions) <= 1 || instructions[1] == "" { + _ = player.WriteString("Please input the amount you want to raise\n") + continue + } + betAmount, err := cast.ToUintE(instructions[1]) + if err != nil { + _ = player.WriteString("Invalid amount\n") + continue + } + if betAmount < minCall { + _ = player.WriteString(fmt.Sprintf("The amount you want to raise is less than the minimum call amount %d\n", minCall)) + continue + } + if texasPlayer.Amount() < betAmount { + _ = player.WriteString("You don't have enough money to raise\n") + continue + } + game.Bet(texasPlayer, betAmount) + database.Broadcast(player.RoomID, fmt.Sprintf("%s raise, bet %d\n", player.Name, betAmount)) + case "fold": + texasPlayer.Folded = true + game.Folded++ + database.Broadcast(player.RoomID, fmt.Sprintf("%s fold\n", player.Name)) + if game.Folded == len(game.Players)-1 { + return settlementRound(game) + } + case "check": + if texasPlayer.Bets < game.MaxBetAmount { + _ = player.WriteString("You can't check, because someone else bet higher than you\n") + continue + } + game.Bet(texasPlayer, 0) + database.Broadcast(player.RoomID, fmt.Sprintf("%s check\n", player.Name)) + case "allin": + betAmount := texasPlayer.Amount() + game.Bet(texasPlayer, betAmount) + database.Broadcast(player.RoomID, fmt.Sprintf("%s all in, bet %d\n", player.Name, betAmount)) + default: + database.BroadcastChat(player, fmt.Sprintf("%s [%s] say: %s\n", player.Name, player.Role, ans)) + continue + } + break + } + return nextPlayer(player, game, stateBet) +} diff --git a/server/state/game/texas/init.go b/server/state/game/texas/init.go new file mode 100644 index 0000000..f9bef15 --- /dev/null +++ b/server/state/game/texas/init.go @@ -0,0 +1,93 @@ +package texas + +import ( + "github.com/ratel-online/core/util/poker" + "github.com/ratel-online/server/database" +) + +func Init(room *database.Room) (game database.RoomGame, err error) { + if room.Game != nil { + return resetGame(room) + } + return createGame(room) +} + +func createGame(room *database.Room) (database.RoomGame, error) { + base := poker.GetTexasBase() + base.Shuffle(len(base), 1) + + index := 0 + roomPlayers := database.RoomPlayers(room.ID) + players := make([]*database.TexasPlayer, 0) + for playerId := range roomPlayers { + player := database.GetPlayer(playerId) + players = append(players, &database.TexasPlayer{ + ID: playerId, + Name: player.Name, + State: make(chan int, 1), + Hand: base[index*2 : (index+1)*2], + }) + index++ + } + game := &database.Texas{ + Room: room, + Players: players, + Pot: 0, + BB: 0, + SB: 1, + Pool: base[len(players)*2:], + MaxBetAmount: 20, + Round: "start", + } + return game, nextRound(game) +} + +func resetGame(room *database.Room) (database.RoomGame, error) { + base := poker.GetTexasBase() + base.Shuffle(len(base), 1) + game := room.Game.(*database.Texas) + + texasPlayers := make(map[int64]*database.TexasPlayer) + for _, texasPlayer := range game.Players { + texasPlayers[texasPlayer.ID] = texasPlayer + } + + index := 0 + roomPlayers := database.RoomPlayers(room.ID) + players := make([]*database.TexasPlayer, 0) + for playerId := range roomPlayers { + if texasPlayer, ok := texasPlayers[playerId]; ok { + texasPlayer.Reset() + texasPlayer.Hand = base[index*2 : (index+1)*2] + players = append(players, texasPlayer) + } else { + player := database.GetPlayer(playerId) + players = append(players, &database.TexasPlayer{ + ID: playerId, + Name: player.Name, + State: make(chan int, 1), + Hand: base[index*2 : (index+1)*2], + }) + } + index++ + } + newGame := &database.Texas{ + Room: room, + Players: players, + Pot: 0, + BB: (game.BB + 1) % len(players), + SB: (game.BB + 2) % len(players), + Pool: base[len(players)*2:], + MaxBetAmount: 20, + Round: "start", + } + return newGame, nextRound(newGame) +} + +func nextPlayer(current *database.Player, game *database.Texas, state int) error { + next := game.NextPlayer(current.ID) + if next != nil { + next.State <- state + } + return nil +} diff --git a/server/state/game/texas/round.go b/server/state/game/texas/round.go new file mode 100644 index 0000000..2fcb01d --- /dev/null +++ b/server/state/game/texas/round.go @@ -0,0 +1,173 @@ +package texas + +import ( + "bytes" + "fmt" + "github.com/ratel-online/core/model" + "github.com/ratel-online/core/util/poker" + "github.com/ratel-online/server/bot" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" +) + +func nextRound(game *database.Texas) error { + switch game.Round { + case "start": + return preFlopRound(game) + case "per-flop": + return flopRound(game) + case "flop": + return turnRound(game) + case "turn": + return riverRound(game) + case "river": + return settlementRound(game) + default: + return consts.ErrorsUnknownTexasRound + } +} + +func preFlopRound(game *database.Texas) error { + game.Round = "per-flop" + for id := range database.RoomPlayers(game.Room.ID) { + player := database.GetPlayer(id) + if player.Amount < 100 { + player.Amount += 2000 + database.Broadcast(game.Room.ID, fmt.Sprintf("%s is too poor, system give him 2000\n", player.Name)) + bot.SendGroupMessage(bot.GroupID, fmt.Sprintf("%s is too poor, system give him 2000", player.Name)) + } + } + + game.Pot += 30 + game.BBPlayer().Bet(20) + game.SBPlayer().Bet(10) + + for id := range database.RoomPlayers(game.Room.ID) { + player := database.GetPlayer(id) + texasPlayer := game.Player(id) + + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("Game starting!\n")) + if game.SBPlayer().ID != player.ID { + buf.WriteString(fmt.Sprintf("Your hand: %s\n", texasPlayer.Hand.TexasString())) + } + if game.BBPlayer().ID == player.ID { + buf.WriteString("You are big blind, bet 20 automatically.\n") + } else { + buf.WriteString(fmt.Sprintf("Big blind: %s, Bet 20\n", game.Players[game.BB].Name)) + } + if game.SBPlayer().ID == player.ID { + buf.WriteString("You are small blind, bet 10 automatically.\n") + } else { + buf.WriteString(fmt.Sprintf("Small blind: %s, Bet 10\n", game.Players[game.SB].Name)) + buf.WriteString(fmt.Sprintf("Pre-flop round, please wait for small blind %s to bet\n", game.Players[game.SB].Name)) + } + _ = player.WriteString(buf.String()) + } + game.SBPlayer().State <- stateBet + return nil +} + +func flopRound(game *database.Texas) error { + game.Round = "flop" + game.MaxBetPlayer = nil + game.Board = append(game.Board, game.Pool[1:4]...) + game.Pool = game.Pool[4:] + database.Broadcast(game.Room.ID, fmt.Sprintf("Flop round, board: %s\n", game.Board.TexasString())) + game.SBPlayer().State <- stateBet + return nil +} + +func turnRound(game *database.Texas) error { + game.Round = "turn" + game.MaxBetPlayer = nil + game.Board = append(game.Board, game.Pool[1:2]...) + game.Pool = game.Pool[2:] + database.Broadcast(game.Room.ID, fmt.Sprintf("Turn round, board: %s\n", game.Board.TexasString())) + game.SBPlayer().State <- stateBet + return nil +} + +func riverRound(game *database.Texas) error { + game.Round = "river" + game.MaxBetPlayer = nil + game.Board = append(game.Board, game.Pool[1:2]...) + game.Pool = game.Pool[2:] + database.Broadcast(game.Room.ID, fmt.Sprintf("River round, board: %s\n", game.Board.TexasString())) + game.SBPlayer().State <- stateBet + return nil +} + +func settlementRound(game *database.Texas) error { + buf := bytes.Buffer{} + buf.WriteString("Settlement round\n") + buf.WriteString(fmt.Sprintf("Board: %s\n", game.Board.TexasString())) + + if game.Folded == len(game.Players)-1 { + var winner *database.TexasPlayer + for _, player := range game.Players { + if !player.Folded { + winner = player + break + } + } + if winner != nil { + winner.Add(game.Pot) + buf.WriteString(fmt.Sprintf("Winner: %s, got all pot: %d\n", winner.Name, game.Pot)) + } else { + buf.WriteString("All players folded\n") + } + } else { + buf.WriteString("Players' hands:\n") + var maxFaces *model.TexasFaces + var maxPlayers []int64 + for _, player := range game.Players { + if player.Folded { + continue + } + faces, err := poker.ParseTexasFaces(player.Hand, game.Board) + if err != nil { + return err + } + buf.WriteString(fmt.Sprintf("%s: %s, type: %s, score: %d\n", player.Name, player.Hand.TexasString(), faces.Type, faces.Score)) + if maxFaces == nil || + maxFaces.Type < faces.Type || + (maxFaces.Type == faces.Type && maxFaces.Score < faces.Score) { + maxFaces = faces + maxPlayers = []int64{player.ID} + continue + } + if maxFaces.Type == faces.Type && maxFaces.Score == faces.Score { + maxPlayers = append(maxPlayers, player.ID) + } + } + winners := make([]*database.TexasPlayer, 0) + for _, id := range maxPlayers { + winners = append(winners, game.Player(id)) + } + if len(winners) == 1 { + buf.WriteString(fmt.Sprintf("Winner: %s, got all pot: %d\n", winners[0].Name, game.Pot)) + } else { + buf.WriteString("Winners: ") + for i, winner := range winners { + if i != 0 { + buf.WriteString(", ") + } + buf.WriteString(winner.Name) + } + buf.WriteString(fmt.Sprintf(", half all pot: %d\n", game.Pot)) + } + for _, winner := range winners { + winner.Add(game.Pot / uint(len(winners))) + } + } + buf.WriteString(fmt.Sprintf("Please room owner %s to start a new game\n", database.GetPlayer(game.Room.Creator).Name)) + database.Broadcast(game.Room.ID, buf.String()) + + room := game.Room + room.State = consts.RoomStateWaiting + for _, player := range game.Players { + player.State <- stateWaiting + } + return nil +} diff --git a/server/state/game/texas/texas.go b/server/state/game/texas/texas.go new file mode 100644 index 0000000..af92804 --- /dev/null +++ b/server/state/game/texas/texas.go @@ -0,0 +1,65 @@ +package texas + +import ( + "time" + + "github.com/ratel-online/core/log" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" +) + +var ( + stateBet = 1 + stateWaiting = 2 +) + +type Texas struct{} + +func (g *Texas) Next(player *database.Player) (consts.StateID, error) { + room := database.GetRoom(player.RoomID) + if room == nil { + return 0, player.WriteError(consts.ErrorsExist) + } + game := room.Game.(*database.Texas) + + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[Texas.Next] Player %d (Room %d) loop count: %d, room.State: %d\n", player.ID, player.RoomID, loopCount, room.State) + } + if room.State == consts.RoomStateWaiting { + log.Infof("[Texas.Next] Player %d exiting, room state changed to waiting, loop count: %d\n", player.ID, loopCount) + return consts.StateWaiting, nil + } + texasPlayer := game.Player(player.ID) + if texasPlayer == nil { + return 0, player.WriteError(consts.ErrorsExist) + } + select { + case state, ok := <-texasPlayer.State: + if !ok { + return 0, consts.ErrorsChanClosed + } + switch state { + case stateBet: + err := bet(player, game) + if err != nil { + log.Error(err) + return 0, err + } + case stateWaiting: + return consts.StateWaiting, nil + default: + return 0, consts.ErrorsChanClosed + } + case <-time.After(5 * time.Second): + // 防止通道阻塞导致的死锁 + return 0, consts.ErrorsTimeout + } + } +} + +func (*Texas) Exit(player *database.Player) consts.StateID { + return consts.StateHome +} diff --git a/server/state/game/uno.go b/server/state/game/uno.go new file mode 100644 index 0000000..437fb7d --- /dev/null +++ b/server/state/game/uno.go @@ -0,0 +1,130 @@ +package game + +import ( + "bytes" + "fmt" + "github.com/feel-easy/uno/card/color" + "github.com/feel-easy/uno/event" + "github.com/feel-easy/uno/game" + "github.com/ratel-online/core/log" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" +) + +type Uno struct{} + +func (g *Uno) Next(player *database.Player) (consts.StateID, error) { + room := database.GetRoom(player.RoomID) + if room == nil { + return 0, player.WriteError(consts.ErrorsExist) + } + game := room.Game.(*database.UnoGame) + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf( + "WELCOME TO %s%s%s!!!\n", + color.Red.Paint("U"), + color.Yellow.Paint("N"), + color.Blue.Paint("O"), + )) + buf.WriteString(fmt.Sprintf("Your Cards: %s\n", game.Game.GetPlayerCards(int(player.ID)))) + _ = player.WriteString(buf.String()) + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[Uno.Next] Player %d (Room %d) loop count: %d, room.State: %d\n", player.ID, player.RoomID, loopCount, room.State) + } + if room.State == consts.RoomStateWaiting { + log.Infof("[Uno.Next] Player %d exiting, room state changed to waiting, loop count: %d\n", player.ID, loopCount) + return consts.StateWaiting, nil + } + log.Infof("[Uno.Next] Player %d waiting for state, loop count: %d\n", player.ID, loopCount) + state := <-game.States[int(player.ID)] + switch state { + case stateFirstCard: + if msg := game.Game.PlayFirstCard(); msg != "" { + database.Broadcast(room.ID, msg) + } + pc := game.Game.Players().Next() + game.States[pc.ID()] <- statePlay + case statePlay: + err := handlePlayUno(room, player, game) + if err != nil { + log.Error(err) + return 0, err + } + case stateWaiting: + return consts.StateWaiting, nil + default: + return 0, consts.ErrorsChanClosed + } + } +} + +func (g *Uno) Exit(player *database.Player) consts.StateID { + return consts.StateHome +} + +func handlePlayUno(room *database.Room, player *database.Player, game *database.UnoGame) error { + p := game.Game.Current() + if p.ID() != int(player.ID) { + game.States[p.ID()] <- statePlay + return nil + } + if !game.HavePlay(player) { + pc := game.Game.Players().Next() + game.States[pc.ID()] <- statePlay + } + gameState := game.Game.ExtractState(p) + card, err := p.Play(gameState, game.Game.Deck()) + if err != nil || card == nil { + event.PlayerPassed.Emit(event.PlayerPassedPayload{ + PlayerName: p.Name(), + }) + pc := game.Game.Players().Next() + game.States[pc.ID()] <- statePlay + return err + } + game.Game.Pile().Add(card) + event.CardPlayed.Emit(event.CardPlayedPayload{ + PlayerName: p.Name(), + Card: card, + }) + if msg := game.Game.PerformCardActions(card); msg != "" { + database.Broadcast(room.ID, msg) + } + if p.NoCards() || game.NeedExit() { + database.Broadcast(room.ID, fmt.Sprintf("%s wins! \n", p.Name())) + room.Game = nil + room.State = consts.RoomStateWaiting + for _, playerId := range game.Players { + game.States[playerId] <- stateWaiting + } + return nil + } + pc := game.Game.Players().Next() + game.States[pc.ID()] <- statePlay + return nil +} + +func InitUnoGame(room *database.Room) (*database.UnoGame, error) { + players := make([]int, 0) + roomPlayers := database.RoomPlayers(room.ID) + unoPlayers := make([]game.Player, 0) + states := map[int]chan int{} + for playerId := range roomPlayers { + p := database.GetPlayer(playerId) + players = append(players, int(p.ID)) + unoPlayers = append(unoPlayers, database.NewUnoPlayer(p)) + states[int(playerId)] = make(chan int, 1) + } + unoGame := game.New(unoPlayers) + unoGame.DealStartingCards() + states[unoGame.Current().ID()] <- stateFirstCard + return &database.UnoGame{ + Room: room, + Players: players, + States: states, + Game: unoGame, + }, nil +} diff --git a/server/state/home.go b/server/state/home.go new file mode 100644 index 0000000..8f9d3d0 --- /dev/null +++ b/server/state/home.go @@ -0,0 +1,33 @@ +package state + +import ( + "bytes" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" +) + +type home struct{} + +func (*home) Next(player *database.Player) (consts.StateID, error) { + buf := bytes.Buffer{} + buf.WriteString("1.Join\n") + buf.WriteString("2.New\n") + err := player.WriteString(buf.String()) + if err != nil { + return 0, player.WriteError(err) + } + selected, err := player.AskForInt() + if err != nil { + return 0, player.WriteError(err) + } + if selected == 1 { + return consts.StateJoin, nil + } else if selected == 2 { + return consts.StateCreate, nil + } + return 0, player.WriteError(consts.ErrorsInputInvalid) +} + +func (*home) Exit(player *database.Player) consts.StateID { + return 0 +} diff --git a/server/state/join.go b/server/state/join.go new file mode 100644 index 0000000..4743e84 --- /dev/null +++ b/server/state/join.go @@ -0,0 +1,85 @@ +package state + +import ( + "bytes" + "fmt" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" + "strconv" +) + +type join struct{} + +func (s *join) Next(player *database.Player) (consts.StateID, error) { + buf := bytes.Buffer{} + rooms := database.GetRooms() + buf.WriteString(fmt.Sprintf("%-10s%-10s%-10s%-10s\n", "ID", "Type", "Players", "State")) + for _, room := range rooms { + pwdFlag := "" + if room.Password != "" { + pwdFlag = "*" + } + buf.WriteString(fmt.Sprintf("%-10d%-10s%-10d%-10s\n", room.ID, pwdFlag+consts.GameTypes[room.Type], room.Players, consts.RoomStates[room.State])) + } + err := player.WriteString(buf.String()) + if err != nil { + return 0, player.WriteError(err) + } + signal, err := player.AskForString() + if err != nil { + return 0, player.WriteError(err) + } + if isExit(signal) { + return s.Exit(player), nil + } + if isLs(signal) { + return consts.StateJoin, nil + } + roomId, err := strconv.ParseInt(signal, 10, 64) + if err != nil { + return 0, player.WriteError(consts.ErrorsRoomInvalid) + } + room := database.GetRoom(roomId) + if room == nil { + return 0, player.WriteError(consts.ErrorsRoomInvalid) + } + + //房间存在密码,要求输入密码 + pwd := room.Password + if pwd != "" { + err = verifyPassword(player, pwd) + if err != nil { + return 0, player.WriteError(err) + } + } + err = database.JoinRoom(roomId, player.ID) + if err != nil { + return 0, player.WriteError(err) + } + if room.State != consts.RoomStateRunning { + database.Broadcast(roomId, fmt.Sprintf("%s [%s] joined room! room current has %d players\n", player.Name, player.Role, room.Players)) + } else { + _ = player.WriteString("You have joined a running game, please wait for the game to finish.\n") + } + return consts.StateWaiting, nil +} + +func (*join) Exit(player *database.Player) consts.StateID { + return consts.StateHome +} + +// 校验密码 +func verifyPassword(player *database.Player, pwd string) error { + err := player.WriteString("Please input room password: \n") + if err != nil { + return err + } + password, err := player.AskForString() + if err != nil { + return err + } + if password != pwd { + return consts.ErrorsRoomPassword + } + return nil +} diff --git a/server/state/state.go b/server/state/state.go new file mode 100644 index 0000000..a5d04e9 --- /dev/null +++ b/server/state/state.go @@ -0,0 +1,89 @@ +package state + +import ( + "github.com/ratel-online/server/state/game/texas" + "strings" + + "github.com/ratel-online/core/log" + "github.com/ratel-online/core/util/async" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" + "github.com/ratel-online/server/state/game" +) + +var states = map[consts.StateID]State{} + +func init() { + register(consts.StateWelcome, &welcome{}) + register(consts.StateHome, &home{}) + register(consts.StateJoin, &join{}) + register(consts.StateCreate, &create{}) + register(consts.StateWaiting, &waiting{}) + register(consts.StateGame, &game.Game{}) + register(consts.StateUnoGame, &game.Uno{}) + register(consts.StateRunFastGame, &game.RunFastGame{}) + register(consts.StateMahjongGame, &game.Mahjong{}) + register(consts.StateTexasGame, &texas.Texas{}) + register(consts.StateLiarGame, &game.Liar{}) +} + +func register(id consts.StateID, state State) { + states[id] = state +} + +type State interface { + Next(player *database.Player) (consts.StateID, error) + Exit(player *database.Player) consts.StateID +} + +func Run(player *database.Player) { + player.State(consts.StateWelcome) + defer func() { + if err := recover(); err != nil { + async.PrintStackTrace(err) + } + log.Infof("player %s state machine break up.\n", player) + }() + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[State.Run] Player %d loop count: %d, current state: %d\n", player.ID, loopCount, player.GetState()) + } + state := states[player.GetState()] + stateId, err := state.Next(player) + if err != nil { + if err1, ok := err.(consts.Error); ok { + if err1.Exit { + stateId = state.Exit(player) + } + } else { + log.Error(err) + state.Exit(player) + break + } + } + if stateId > 0 { + player.State(stateId) + } + } +} + +func isExit(signal string) bool { + signal = strings.ToLower(signal) + return isX(signal, "exit", "e") +} + +func isLs(signal string) bool { + return isX(signal, "ls") +} + +func isX(signal string, x ...string) bool { + signal = strings.ToLower(signal) + for _, v := range x { + if v == signal { + return true + } + } + return false +} diff --git a/server/state/waiting.go b/server/state/waiting.go new file mode 100644 index 0000000..ec359c1 --- /dev/null +++ b/server/state/waiting.go @@ -0,0 +1,263 @@ +package state + +import ( + "bytes" + "fmt" + "strings" + "time" + + "github.com/ratel-online/server/state/game/texas" + "github.com/spf13/cast" + + "github.com/ratel-online/core/log" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" + "github.com/ratel-online/server/rule" + "github.com/ratel-online/server/state/game" +) + +type waiting struct{} + +func (s *waiting) Next(player *database.Player) (consts.StateID, error) { + room := database.GetRoom(player.RoomID) + if room == nil { + return 0, consts.ErrorsExist + } + s.Backfill(room) + + access, err := s.waitingForStart(player, room) + if err != nil { + return 0, err + } + if access { + switch room.Type { + default: + return consts.StateGame, nil + case consts.GameTypeRunFast: + return consts.StateRunFastGame, nil + case consts.GameTypeUno: + return consts.StateUnoGame, nil + case consts.GameTypeMahjong: + return consts.StateMahjongGame, nil + case consts.GameTypeTexas: + return consts.StateTexasGame, nil + case consts.GameTypeLiar: + return consts.StateLiarGame, nil + } + } + return s.Exit(player), nil +} + +func (s *waiting) Exit(player *database.Player) consts.StateID { + room := database.GetRoom(player.RoomID) + if room != nil { + isOwner := room.Creator == player.ID + database.LeaveRoom(room.ID, player.ID) + database.Broadcast(room.ID, fmt.Sprintf("%s exited room! room current has %d players\n", player.Name, room.Players)) + if isOwner { + newOwner := database.GetPlayer(room.Creator) + database.Broadcast(room.ID, fmt.Sprintf("%s become new owner\n", newOwner.Name)) + } + s.Backfill(room) + } + return consts.StateHome +} + +func (*waiting) Backfill(room *database.Room) { + if room.State == consts.RoomStateRunning { + return + } + newPlayer := database.Backfill(room.ID) + if newPlayer != nil { + database.Broadcast(room.ID, fmt.Sprintf("%s has joined room! room current has %d players\n", newPlayer.Name, room.Players)) + } +} + +func (*waiting) Kicking(player *database.Player) { + room := database.GetRoom(player.RoomID) + if room != nil { + database.Broadcast(room.ID, fmt.Sprintf("%s has been kicked!\n", player.Name)) + database.Kicking(room.ID, player.ID) + database.Broadcast(room.ID, fmt.Sprintf("room current has %d players\n", room.Players)) + } +} + +func (s *waiting) waitingForStart(player *database.Player, room *database.Room) (bool, error) { + access := false + //对局类别 + player.StartTransaction() + defer player.StopTransaction() + loopCount := 0 + for { + loopCount++ + if loopCount%100 == 0 { + log.Infof("[waitingForStart] Player %d (Room %d) loop count: %d, room.State: %d, access: %v\n", player.ID, player.RoomID, loopCount, room.State, access) + } + signal, err := player.AskForStringWithoutTransaction(time.Second) + if err != nil && err != consts.ErrorsTimeout { + return access, err + } + + if !database.IsValidPlayer(room.ID, player.ID) { + return false, consts.ErrorsPlayerNotInRoom + } + + if room.State == consts.RoomStateRunning && player.Role == database.RolePlayer { + access = true + break + } + signal = strings.TrimSpace(strings.ToLower(signal)) + if signal == "" { + continue + } + + segments := strings.Split(signal, " ") + if len(segments) == 1 { + if segments[0] == "ls" || segments[0] == "v" { + viewRoomPlayers(room, player) + continue + } else if segments[0] == "start" || signal == "s" { + if room.Creator == player.ID { + if room.Players <= 1 { + _ = player.WriteError(consts.ErrorsGamePlayersInsufficient) + continue + } + if room.Type == consts.GameTypeRunFast && room.Players != 3 { + _ = player.WriteError(consts.ErrorsGamePlayersInvalid) + continue + } + err = startGame(player, room) + if err != nil { + return access, err + } + access = true + break + } + } + } else if len(segments) == 2 { + if segments[0] == "kicking" || segments[0] == "kill" || segments[0] == "k" { + if room.Creator == player.ID { + kickedId := cast.ToInt64(segments[1]) + if kickedId == player.ID { + _ = player.WriteError(consts.ErrorsCannotKickYourself) + continue + } + + kickedPlayer := database.GetPlayer(kickedId) + if kickedPlayer == nil || kickedPlayer.RoomID != room.ID { + _ = player.WriteError(consts.ErrorsPlayerNotInRoom) + continue + } + + s.Kicking(kickedPlayer) + continue + } + } + } else if len(segments) == 3 && room.Creator == player.ID { + database.SetRoomProps(room, segments[1], segments[2]) + continue + } + + if room.EnableChat { + if room.State == consts.RoomStateRunning { + _ = player.WriteString(fmt.Sprintf("%s\n", consts.ErrorsChatUnopenedDuringGame.Error())) + } else { + database.BroadcastChat(player, fmt.Sprintf("%s [%s] say: %s\n", player.Name, player.Role, signal)) + } + } else { + _ = player.WriteString(fmt.Sprintf("%s\n", consts.ErrorsChatUnopened.Error())) + } + } + return access, nil +} + +func startGame(player *database.Player, room *database.Room) (err error) { + room.Lock() + defer room.Unlock() + switch room.Type { + default: + room.Game, err = game.InitGame(room) + case consts.GameTypeUno: + room.Game, err = game.InitUnoGame(room) + case consts.GameTypeRunFast: + room.Game, err = game.InitRunFastGame(room, rule.RunFastRules) + case consts.GameTypeMahjong: + room.Game, err = game.InitMahjongGame(room) + case consts.GameTypeTexas: + room.Game, err = texas.Init(room) + case consts.GameTypeLiar: + room.Game, err = game.InitLiarGame(room) + } + if err != nil { + _ = player.WriteError(err) + return err + } + room.State = consts.RoomStateRunning + return nil +} + +func viewRoomPlayers(room *database.Room, currPlayer *database.Player) { + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("Room ID: %d\n", room.ID)) + buf.WriteString("Players:\n") + for playerId := range database.RoomPlayers(room.ID) { + player := database.GetPlayer(playerId) + if room.EnableShowIP { + buf.WriteString(fmt.Sprintf("%s [%s], score: %d, id: %d, ip: %s\n", player.Name, player.Role, player.Amount, player.ID, maskIP(player.IP))) + } else { + buf.WriteString(fmt.Sprintf("%s [%s], score: %d, id: %d\n", player.Name, player.Role, player.Amount, player.ID)) + } + } + + buf.WriteString("\nSpectators:\n") + for spectatorId := range database.RoomSpectators(room.ID) { + spectator := database.GetPlayer(spectatorId) + if room.EnableShowIP { + buf.WriteString(fmt.Sprintf("%s [spectator], score: %d, id: %d, ip: %s\n", spectator.Name, spectator.Amount, spectator.ID, maskIP(spectator.IP))) + } else { + buf.WriteString(fmt.Sprintf("%s [spectator], score: %d, id: %d\n", spectator.Name, spectator.Amount, spectator.ID)) + } + } + + buf.WriteString("\nSettings:\n") + switch room.Type { + case consts.GameTypeUno, consts.GameTypeMahjong: + buf.WriteString(fmt.Sprintf("%-5s%-5v\n", "ip:", sprintPropsState(room.EnableShowIP))) + case consts.GameTypeTexas: + buf.WriteString(fmt.Sprintf("%-5s%-5v\n", "pn:", room.MaxPlayers)) + buf.WriteString(fmt.Sprintf("%-5s%-5v\n", "ip:", sprintPropsState(room.EnableShowIP))) + case consts.GameTypeLiar: + buf.WriteString(fmt.Sprintf("%-5s%-5v\n", "jt:", sprintPropsState(room.EnableJokerAsTarget))) + buf.WriteString(fmt.Sprintf("%-5s%-5v\n", "ip:", sprintPropsState(room.EnableShowIP))) + default: + buf.WriteString(fmt.Sprintf("%-5s%-5v%-5s%-5v\n", "lz:", sprintPropsState(room.EnableLaiZi))) + buf.WriteString(fmt.Sprintf("%-5s%-5v%-5s%-5v\n", "ds:", sprintPropsState(room.EnableDontShuffle)+",", "sk:", sprintPropsState(room.EnableSkill))) + buf.WriteString(fmt.Sprintf("%-5s%-5v%-5s%-5v\n", "pn:", room.MaxPlayers, "ct:", sprintPropsState(room.EnableChat))) + buf.WriteString(fmt.Sprintf("%-5s%-5v\n", "ip:", sprintPropsState(room.EnableShowIP))) + pwd := room.Password + if pwd != "" { + if room.Creator != currPlayer.ID { + pwd = "********" + } + } else { + pwd = "off" + } + buf.WriteString(fmt.Sprintf("%-5s%-20v\n", "pwd", pwd)) + } + _ = currPlayer.WriteString(buf.String()) +} + +func sprintPropsState(on bool) string { + if on { + return "on" + } + return "off" +} + +func maskIP(ip string) string { + parts := strings.Split(ip, ".") + if len(parts) == 4 { + return parts[0] + "." + parts[1] + ".*.*" + } + return "*.*.*.*" +} diff --git a/server/state/welcome.go b/server/state/welcome.go new file mode 100644 index 0000000..c44dd69 --- /dev/null +++ b/server/state/welcome.go @@ -0,0 +1,24 @@ +package state + +import ( + "bytes" + "fmt" + "github.com/ratel-online/server/consts" + "github.com/ratel-online/server/database" +) + +type welcome struct{} + +func (*welcome) Next(player *database.Player) (consts.StateID, error) { + buf := bytes.Buffer{} + buf.WriteString(fmt.Sprintf("Hi %s, Welcome to ratel online! rules at https://github.com/ratel-online/server/blob/main/README.md\n", player.Name)) + err := player.WriteString(buf.String()) + if err != nil { + return 0, player.WriteError(err) + } + return consts.StateHome, nil +} + +func (*welcome) Exit(player *database.Player) consts.StateID { + return 0 +}