add client

This commit is contained in:
wzdwc
2020-04-14 22:20:02 +08:00
parent 32dd3302f8
commit d98223a6c6
80 changed files with 8461 additions and 20 deletions
+12
View File
@@ -0,0 +1,12 @@
sudo: false
language: node_js
node_js:
- '10'
before_install:
- npm i npminstall -g
install:
- npminstall
script:
- npm run ci
after_script:
- npminstall codecov && codecov
+58
View File
@@ -0,0 +1,58 @@
# node-loan-center
node服务中心
## 快速入门
详细文档,参见 [egg 文档][eggjs], [midway 文档][midway]。
### 安装 启动
```bash
$ yarn
$ yarn dev
$ open http://localhost:7001/
```
### 部署
```bash
$ npm start
$ npm stop
```
### 单元测试
```
yarn test
```
- 具体参见 [midway 文档 - 单元测试](https://eggjs.org/zh-cn/core/unittest)。
### 目录结构
```
├─dist
├─logs
│ ├─ELKLog //上报kibana日志系统log
| | ├─info.log
| | └─error.log
│ └─node-loan-center // 系统日志
├─node_modules
├─src //开发目录
│ ├─app
│ │ ├─controller
│ │ ├─extend
│ │ ├─helper
│ │ ├─middleware
│ │ └─public // 静态目录
│ ├─config // 配置文件目录,包含testprodlocal等config文件
│ ├─interface // ts接口目录
│ ├─lib // 工具类目录,如基类
│ └─service
└─test // 单元测试用例目录
└─app
└─controller
```
[midway]: https://midwayjs.org
[git-rules]: https://confluence.sui.work/pages/viewpage.action?pageId=51120607
[eggjs]: https://eggjs.org/zh-cn/
+14
View File
@@ -0,0 +1,14 @@
environment:
matrix:
- nodejs_version: '10'
install:
- ps: Install-Product node $env:nodejs_version
- npm i npminstall && node_modules\.bin\npminstall
test_script:
- node --version
- npm --version
- npm run test
build: off
View File
View File
+12
View File
@@ -0,0 +1,12 @@
2020-04-14 22:10:45,972 INFO 36442 [egg:logger] init all loggers with options: {"dir":"/Users/jorky/code/TexasPokerGame/server/logs/game-node-center","encoding":"utf8","env":"local","level":"INFO","consoleLevel":"INFO","disableConsoleAfterReady":false,"outputJSON":false,"buffer":true,"appLogName":"app.log","coreLogName":"core.log","agentLogName":"agent.log","errorLogName":"error.log","coreLogger":{"consoleLevel":"WARN"},"allowDebugAtProd":false,"type":"agent"}
2020-04-14 22:10:45,983 INFO 36442 [egg:core] dump config after load, 6ms
2020-04-14 22:10:46,034 INFO 36442 [egg-watcher] Start watching: ["/Users/jorky/code/TexasPokerGame/server/src/app","/Users/jorky/code/TexasPokerGame/server/src/lib","/Users/jorky/code/TexasPokerGame/server/src/service","/Users/jorky/code/TexasPokerGame/server/src/config","/Users/jorky/code/TexasPokerGame/server/src/app.ts","/Users/jorky/code/TexasPokerGame/server/src/agent.ts","/Users/jorky/code/TexasPokerGame/server/src/interface.ts"]
2020-04-14 22:10:46,034 INFO 36442 [egg-watcher] Start watching: "/Users/jorky/code/TexasPokerGame/server/src/app"
2020-04-14 22:10:46,035 INFO 36442 [egg-watcher] Start watching: "/Users/jorky/code/TexasPokerGame/server/src/lib"
2020-04-14 22:10:46,035 INFO 36442 [egg-watcher] Start watching: "/Users/jorky/code/TexasPokerGame/server/src/service"
2020-04-14 22:10:46,035 INFO 36442 [egg-watcher] Start watching: "/Users/jorky/code/TexasPokerGame/server/src/config"
2020-04-14 22:10:46,035 INFO 36442 [egg-watcher] Start watching: "/Users/jorky/code/TexasPokerGame/server/src/app.ts"
2020-04-14 22:10:46,036 INFO 36442 [egg-watcher] Start watching: "/Users/jorky/code/TexasPokerGame/server/src/agent.ts"
2020-04-14 22:10:46,036 INFO 36442 [egg-watcher] Start watching: "/Users/jorky/code/TexasPokerGame/server/src/interface.ts"
2020-04-14 22:10:46,037 INFO 36442 [egg-watcher:agent] watcher start success
2020-04-14 22:10:46,045 INFO 36442 [egg:core] dump config after ready, 5ms
View File
+49
View File
@@ -0,0 +1,49 @@
2020-04-14 22:10:47,071 INFO 36443 [egg:logger] init all loggers with options: {"dir":"/Users/jorky/code/TexasPokerGame/server/logs/game-node-center","encoding":"utf8","env":"local","level":"INFO","consoleLevel":"INFO","disableConsoleAfterReady":false,"outputJSON":false,"buffer":true,"appLogName":"app.log","coreLogName":"core.log","agentLogName":"agent.log","errorLogName":"error.log","coreLogger":{"consoleLevel":"WARN"},"allowDebugAtProd":false,"type":"application"}
2020-04-14 22:10:47,096 INFO 36443 [egg-multipart] stream mode enable
2020-04-14 22:10:47,154 INFO 36443 [egg-redis] server connecting redis://:***@127.0.0.1:6379/0
2020-04-14 22:10:47,176 INFO 36443 [egg-mysql] connecting root@127.0.0.1:3306/poker
2020-04-14 22:10:47,342 INFO 36443 [egg-static] starting static serve /public/ -> /Users/jorky/code/TexasPokerGame/server/src/app/public
2020-04-14 22:10:47,343 INFO 36443 [egg-security] use noopen middleware
2020-04-14 22:10:47,344 INFO 36443 [egg-security] use nosniff middleware
2020-04-14 22:10:47,345 INFO 36443 [egg-security] use xssProtection middleware
2020-04-14 22:10:47,345 INFO 36443 [egg-security] use xframe middleware
2020-04-14 22:10:47,346 INFO 36443 [egg-security] use dta middleware
2020-04-14 22:10:47,346 INFO 36443 [egg-security] compose 5 middlewares into one security middleware
2020-04-14 22:10:47,355 INFO 36443 [egg:core] dump config after load, 6ms
2020-04-14 22:10:47,370 INFO 36443 [egg-redis] client connect success
2020-04-14 22:10:47,380 ERROR 36443 nodejs.ReplyError: Ready check failed: NOAUTH Authentication required.
at parseError (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:193:12)
at parseType (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:303:14)
command: "INFO"
code: "NOAUTH"
pid: 36443
hostname: 192.168.0.100
2020-04-14 22:10:47,380 ERROR 36443 nodejs.ReplyError: Ready check failed: NOAUTH Authentication required.
at parseError (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:193:12)
at parseType (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:303:14)
command: "INFO"
code: "NOAUTH"
pid: 36443
hostname: 192.168.0.100
2020-04-14 22:10:47,390 ERROR 36443 nodejs.ReplyError: Ready check failed: NOAUTH Authentication required.
at parseError (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:193:12)
at parseType (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:303:14)
command: "INFO"
code: "NOAUTH"
pid: 36443
hostname: 192.168.0.100
2020-04-14 22:10:47,391 ERROR 36443 nodejs.ReplyError: Ready check failed: NOAUTH Authentication required.
at parseError (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:193:12)
at parseType (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:303:14)
command: "INFO"
code: "NOAUTH"
pid: 36443
hostname: 192.168.0.100
2020-04-14 22:10:47,394 INFO 36443 [egg-redis] instance[0] status OK, client ready
2020-04-14 22:10:47,400 INFO 36443 [egg-watcher:application] watcher start success
2020-04-14 22:10:47,434 INFO 36443 [egg-mysql] instance[0] status OK, rds currentTime: Tue Apr 14 2020 22:10:47 GMT+0800 (China Standard Time)
2020-04-14 22:10:47,458 INFO 36443 [egg:core] dump config after ready, 5ms
+6
View File
@@ -0,0 +1,6 @@
2020-04-14 22:10:47,463 INFO 36442 [Timer] /Users/jorky/code/TexasPokerGame/server/node_modules/egg-multipart/app/schedule/clean_tmpdir.js next time will execute after 22752539ms at 2020-04-15 04:30:00.002
2020-04-14 22:10:47,463 INFO 36442 [Timer] /Users/jorky/code/TexasPokerGame/server/node_modules/egg-logrotator/app/schedule/clean_log.js next time will execute after 6552537ms at 2020-04-15 00:00:00.000
2020-04-14 22:10:47,463 INFO 36442 [Timer] /Users/jorky/code/TexasPokerGame/server/node_modules/egg-logrotator/app/schedule/rotate_by_file.js next time will execute after 6553537ms at 2020-04-15 00:00:01.000
2020-04-14 22:10:47,096 INFO 36443 [egg-schedule]: register schedule /Users/jorky/code/TexasPokerGame/server/node_modules/egg-multipart/app/schedule/clean_tmpdir.js
2020-04-14 22:10:47,096 INFO 36443 [egg-schedule]: register schedule /Users/jorky/code/TexasPokerGame/server/node_modules/egg-logrotator/app/schedule/clean_log.js
2020-04-14 22:10:47,096 INFO 36443 [egg-schedule]: register schedule /Users/jorky/code/TexasPokerGame/server/node_modules/egg-logrotator/app/schedule/rotate_by_file.js
+32
View File
@@ -0,0 +1,32 @@
2020-04-14 22:10:47,379 ERROR 36443 nodejs.ReplyError: Ready check failed: NOAUTH Authentication required.
at parseError (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:193:12)
at parseType (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:303:14)
command: "INFO"
code: "NOAUTH"
pid: 36443
hostname: 192.168.0.100
2020-04-14 22:10:47,380 ERROR 36443 nodejs.ReplyError: Ready check failed: NOAUTH Authentication required.
at parseError (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:193:12)
at parseType (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:303:14)
command: "INFO"
code: "NOAUTH"
pid: 36443
hostname: 192.168.0.100
2020-04-14 22:10:47,381 ERROR 36443 nodejs.ReplyError: Ready check failed: NOAUTH Authentication required.
at parseError (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:193:12)
at parseType (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:303:14)
command: "INFO"
code: "NOAUTH"
pid: 36443
hostname: 192.168.0.100
2020-04-14 22:10:47,390 ERROR 36443 nodejs.ReplyError: Ready check failed: NOAUTH Authentication required.
at parseError (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:193:12)
at parseType (/Users/jorky/code/TexasPokerGame/server/node_modules/redis/node_modules/redis-parser/lib/parser.js:303:14)
command: "INFO"
code: "NOAUTH"
pid: 36443
hostname: 192.168.0.100
+59
View File
@@ -0,0 +1,59 @@
{
"name": "game-node-center",
"version": "1.0.0",
"description": "node服务中心",
"private": true,
"dependencies": {
"@types/socket.io": "^2.1.4",
"chai": "^4.2.0",
"egg-cors": "^2.2.3",
"egg-jwt": "^3.1.7",
"egg-mysql": "^3.0.0",
"egg-redis": "^2.4.0",
"egg-scripts": "^2.10.0",
"egg-socket.io": "^4.1.6",
"midway": "^1.0.0"
},
"devDependencies": {
"@types/mocha": "^5.2.7",
"@types/node": "^10.5.5",
"cross-env": "^6.0.0",
"egg-ci": "^1.8.0",
"midway-bin": "1",
"midway-mock": "1",
"ts-node": "^8.3.0",
"tslib": "^1.8.1",
"tslint": "^5.11.0",
"tslint-config-egg": "^1.0.0",
"typescript": "^3.5.0"
},
"engines": {
"node": ">=10.16.0"
},
"scripts": {
"start": "egg-scripts start --daemon --title=midway-server-node-loan-center --framework=midway --ts",
"stop": "egg-scripts stop --title=midway-server-node-loan-center",
"start_build": "npm run build && cross-env NODE_ENV=development midway-bin dev",
"clean": "midway-bin clean",
"dev": "cross-env NODE_ENV=local midway-bin dev --ts",
"debug": "cross-env NODE_ENV=local midway-bin debug --ts",
"test": "npm run lint && midway-bin test --ts",
"cov": "midway-bin cov --ts",
"lint": "tslint --fix -p tsconfig.json -t stylish",
"ci": "npm run cov",
"build": "midway-bin build -c"
},
"ci": {
"version": "10"
},
"midway-bin-clean": [
".vscode/.tsbuildinfo",
"dist"
],
"repository": {
"type": "git",
"url": ""
},
"author": "Cai",
"license": "MIT"
}
+31
View File
@@ -0,0 +1,31 @@
import { Application } from 'egg';
import * as path from 'path';
import ElkTransport from './app/helper/logTransport';
export default (app: Application) => {
app.beforeStart(async () => {
});
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
if (err.name === 'UnauthorizedError') {
ctx.status = 401;
ctx.body = {
code: '999999',
data: {},
message: 'invalid token...',
};
ctx.logger.error('invalid token...', err);
}
}
});
app.logger.set('remoteInfo', new ElkTransport({
level: 'INFO',
file: path.join(app.baseDir, '../logs/ELKLog/info.log'),
}));
app.logger.set('remoteError', new ElkTransport({
level: 'ERROR',
file: path.join(app.baseDir, '../logs/ELKLog/error.log'),
}));
};
+40
View File
@@ -0,0 +1,40 @@
import BaseController from '../../lib/baseController';
import { controller, inject, post, provide } from 'midway';
import { IAccountService } from '../../interface/IAccountService';
import { IAccountInfo } from '../../interface/IAccountInfo';
@provide()
@controller('/node/user/')
export class Account extends BaseController {
@inject('AccountService')
service: IAccountService;
@post('/login')
async login() {
try {
const { body } = this.getRequestBody();
const { userAccount, password } = body;
const accountInfo: IAccountInfo = { userAccount, password };
const result = await this.service.login(accountInfo);
this.success(result);
} catch (e) {
this.ctx.logger.error('login-----:', e);
this.fail(e);
}
}
@post('/register')
async register() {
try {
const { body } = this.getRequestBody();
const { userAccount, password, nickName } = body;
const accountInfo: IAccountInfo = { userAccount, password, nickName };
const result = await this.service.register(accountInfo);
this.success(result);
} catch (e) {
this.ctx.logger.error('login-----:', e);
this.fail(e);
}
}
}
+39
View File
@@ -0,0 +1,39 @@
import { Context, inject, controller, post, provide } from 'midway';
import BaseController from '../../lib/baseController';
import { IRoomService } from '../../interface/IRoom';
@provide()
@controller('/node/game/room')
export class RoomController extends BaseController {
@inject()
ctx: Context;
@inject('RoomService')
roomService: IRoomService;
/**
*
*/
@post('/')
async index() {
try {
const result = await this.roomService.add();
this.success(result);
} catch (e) {
this.fail('create room error');
console.log(e);
}
}
@post('/find')
async find() {
try {
const { body } = this.getRequestBody();
const result = await this.roomService.findByRoomNumber(body.roomNumber);
this.success({ hasRoom: result });
} catch (e) {
this.fail('create room error');
console.log(e);
}
}
}
+32
View File
@@ -0,0 +1,32 @@
import { Context, inject, controller, post, provide, plugin } from 'midway';
import BaseController from '../../lib/baseController';
import { IUserService } from '../../interface/IUserService';
@provide()
@controller('/node/user')
export class UserController extends BaseController {
@inject()
ctx: Context;
@plugin()
jwt: any;
@inject('UserService')
user: IUserService;
/**
* 处理ocr数据转发
*/
@post('/')
async index() {
try {
const token: string = this.ctx.get('Authorization') || '';
const userInfo = await this.jwt.verify(token);
const user = this.user.findByAccount(userInfo.userAccount);
this.success(user);
} catch (e) {
console.log(e);
this.fail('server error');
}
}
}
+129
View File
@@ -0,0 +1,129 @@
export interface IPlayer {
counter: number;
position?: number;
userId: string;
}
export enum ECommand {
SMALL_BLIND = 'small_blind',
BIG_BLIND = 'big_blind',
STRADDLE = 'straddle',
CALL = 'call',
ALL_IN = 'allin',
RAISE = 'raise',
CHECK = 'check',
FOLD = 'fold',
}
export enum EPlayerType {
DEFAULT = 'default',
DEALER = 'dealer',
BIG_BLIND = 'big_blind',
SMALL_BLIND = 'small_blind',
}
export class Player {
handCard: string[] = [];
position: number = 0;
counter: number = 0;
userId: string = '';
actionSize: number = 0;
type: string = EPlayerType.DEFAULT;
evPot: number = Infinity;
pokeStyle: string = '';
// commandRecord: Array<string> = [];
constructor(config: IPlayer = { counter: 0, position: 0, userId: '' }) {
this.counter = config.counter;
this.position = config.position || 0;
this.userId = config.userId;
if (this.position === 0) {
this.type = EPlayerType.DEALER;
}
if (this.position === 1) {
this.type = EPlayerType.SMALL_BLIND;
}
if (this.position === 2) {
this.type = EPlayerType.BIG_BLIND;
}
}
setHandCard(card: string) {
this.handCard.push(card);
}
getHandCard() {
return this.handCard;
}
/**
* player action
* @param {string} commandString
* @param {number} prevSize
* @example action('command:raise:10')
*/
action(commandString: string, prevSize: number = 0) {
const commandArr = commandString.split(':');
const command = commandArr[0];
const raiseSize = Number(commandArr[1]);
let size = 0;
if (command !== ECommand.ALL_IN
&& (prevSize > (this.counter + this.actionSize) || raiseSize > this.counter)) {
throw 'error action, overflow action size';
}
// BLIND
if (command === ECommand.SMALL_BLIND || command === ECommand.BIG_BLIND) {
size = raiseSize;
}
// todo STRADDLE
if (command === ECommand.STRADDLE) {
// position 0 is dealer
if (this.position === 3) {
size = raiseSize;
} else {
throw 'error action STRADDLE';
}
}
// player raise,get the raise size
if (command === ECommand.RAISE) {
// raise must double to prevSize
if ((raiseSize + this.actionSize) >= prevSize * 2) {
size = raiseSize + this.actionSize;
} else {
throw 'error action: raise size too small';
}
}
if (command === ECommand.ALL_IN) {
size = this.counter;
}
if (command === ECommand.CALL) {
size = prevSize - this.actionSize;
}
if (command === ECommand.CHECK) {
size = -1;
}
if (command === ECommand.FOLD) {
size = 0;
}
if (size > 0) {
this.counter -= size;
}
this.actionSize += size;
return size;
}
clearActionSize() {
this.actionSize = 0;
}
income(size: number) {
this.counter += size;
}
}
+54
View File
@@ -0,0 +1,54 @@
export interface IPoker {
init(): void;
getCard(): string;
getRandom(number: number): number;
}
/**
* Created by jorky on 2020/2/23.
*/
export class Poker implements IPoker {
private pokers: string [] = [];
constructor() {
this.init();
}
init(): void {
const size = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm' ];
const color = [ 1, 2, 3, 4 ];
for (const i of size) {
for (const j of color) {
this.pokers.push(`${i}${j}`);
}
}
}
getCard(): string {
if (this.pokers.length === 0) return 'done';
const currCardIndex = this.getRandom(this.pokers.length);
const currCard = this.pokers[currCardIndex];
this.pokers.splice(currCardIndex, 1);
return currCard;
}
getRandom(number: number): number {
const maxNumber = Math.ceil(number);
return Math.floor(Math.random() * maxNumber);
}
}
+407
View File
@@ -0,0 +1,407 @@
/**
* Created by jorky on 2020/2/23.
*/
import { Poker } from './Poker';
import { ECommand, EPlayerType, IPlayer, Player } from './Player';
import { PokerStyle } from './PokerStyle';
import { ILinkNode, Link } from '../../utils/Link';
interface IPokerGame {
users: IPlayer[];
smallBlind: number;
}
export enum EGameStatus {
GAME_READY,
GAME_START,
GAME_FLOP,
GAME_TURN,
GAME_RIVER,
GAME_ACTION,
GAME_SHOWDOWN,
GAME_OVER,
}
export class PokerGame {
commonCard: string[] = [];
fireCards: string[] = [];
playerLink: Link<Player>;
allPlayer: Player[] = [];
poker = new Poker();
pot: number;
status: EGameStatus;
currPlayer: ILinkNode<Player>;
smallBlind: number;
playerSize: number;
prevSize: number;
prevPot: number;
allInPlayers: Player[] = [];
currActionAllinPlayer: Player[] = [];
hasStraddle = false;
winner: Player[][] = [];
constructor(config: IPokerGame) {
this.smallBlind = config.smallBlind;
this.init(config.users);
}
init(users: IPlayer[]) {
if (this.playerSize < 2) {
throw 'player Inadequate';
}
this.status = EGameStatus.GAME_READY;
// init playerLink
this.playerLink = this.setPlayer(users);
this.playerSize = users.length;
// set SB, BB,Straddle
this.getBlind();
// utg
this.currPlayer = this.playerLink.getNode(3);
}
getBlind() {
// sb blind
const SBPlayerNode = this.playerLink.getNode(1);
const SBPlayer = SBPlayerNode.node;
if (SBPlayerNode.next) {
// big blind
const BBPlayerNode: ILinkNode<Player> = SBPlayerNode.next;
const BBPlayer = BBPlayerNode.node;
SBPlayer.action(`small_blind:${this.smallBlind}`);
BBPlayer.action(`big_blind:${this.smallBlind * 2}`);
this.prevSize = this.smallBlind * 2;
// add counter to pot
// this.pots.push(this.smallBlind * 3);
this.pot = this.smallBlind * 3;
// todo straddle
} else {
throw 'player Inadequate';
}
}
play() {
this.status = EGameStatus.GAME_START;
this.sendCard();
}
action(commandString: string) {
if (this.status === EGameStatus.GAME_ACTION && this.currPlayer.next) {
const commandArr = commandString.split(':');
const command = commandArr[0];
let size = Number(commandArr[1]);
if (command === ECommand.ALL_IN) {
size = this.currPlayer.node.counter > this.prevSize ?
this.currPlayer.node.counter : this.prevSize;
this.allIn();
this.pot += this.currPlayer.node.counter;
// other pot
// only one playeror none playergame is over
}
if (command === ECommand.CALL) {
size = this.prevSize;
this.pot += size - this.currPlayer.node.actionSize;
}
if (command === ECommand.FOLD) {
size = this.prevSize;
this.removePlayer(this.currPlayer.node);
// only one playeror none playergame is over
}
if (command === ECommand.CHECK) {
console.log(this.currPlayer.node.type === EPlayerType.BIG_BLIND
&& this.prevSize === this.smallBlind * 2, 'big blind', this.currPlayer);
// prev player must be check
if (!(this.prevSize === 0 ||
(this.currPlayer.node.type === EPlayerType.BIG_BLIND
&& this.prevSize === this.smallBlind * 2))) {
throw 'incorrect action: check';
}
}
if (command === ECommand.RAISE) {
this.pot += size;
// 3 bet
size += this.currPlayer.node.actionSize;
if ((size + this.currPlayer.node.actionSize) < this.prevSize * 2) {
throw 'incorrect action: raise';
}
}
this.currPlayer.node.action(commandString, this.prevSize);
this.prevSize = size;
const nextPlayer = this.currPlayer.next.node;
// all check actionSize === -1
// all player allin
// only 2 player, curr player fold, next player already action
// only one player ,one player fold,other player allin
// pre flop big blind check and other player call
// console.log('this.currPlayer----------', this.currPlayer, nextPlayer, command);
if (this.playerSize === 0
|| (command === ECommand.FOLD && this.currPlayer.next.node.actionSize !== 0)
|| (nextPlayer.actionSize === this.currPlayer.node.actionSize && command !== ECommand.FOLD)
|| (this.commonCard.length === 0
&& this.currPlayer.node.type === EPlayerType.BIG_BLIND
&& command === ECommand.CHECK)) {
// console.log('ccc------', this.currPlayer, nextPlayer, command, this.playerSize);
this.actionComplete();
return;
}
this.currPlayer = this.currPlayer.next;
} else {
throw 'incorrect action flow';
}
}
allIn() {
this.currActionAllinPlayer.push(this.currPlayer.node);
this.removePlayer(this.currPlayer.node);
}
private actionComplete() {
// pre flop
if (this.commonCard.length === 0
&& this.currPlayer.node.actionSize === this.smallBlind * 2
&& this.currPlayer.node.type !== EPlayerType.BIG_BLIND) {
if (this.currPlayer.next) {
this.currPlayer = this.currPlayer.next;
} else {
throw 'currPlayer.next is null';
}
return;
}
// action has allin, sum the allin player ev_pot
if (this.currActionAllinPlayer.length !== 0) {
let currAllinPlayerPot = 0;
this.currActionAllinPlayer.forEach(allinPlayer => {
this.allPlayer.forEach(p => {
if (p.actionSize < allinPlayer.actionSize) {
currAllinPlayerPot += p.actionSize;
} else {
currAllinPlayerPot += allinPlayer.actionSize;
}
});
allinPlayer.evPot = this.prevPot + currAllinPlayerPot;
currAllinPlayerPot = 0;
});
this.allInPlayers = [ ...this.allInPlayers, ...this.currActionAllinPlayer ];
}
// action complete clear player actionSize = 0
this.clearPlayerAction();
this.currActionAllinPlayer = [];
this.prevSize = 0;
this.prevPot = this.pot;
// new action ring first action is sb
this.currPlayer = this.playerLink.getNode(1);
this.setSate();
console.log(this.playerSize, 'playerS-------', this.status);
if (this.status === EGameStatus.GAME_SHOWDOWN || this.playerSize <= 1) {
this.gameOver();
}
}
setSate() {
if (this.status === EGameStatus.GAME_ACTION) {
if (this.commonCard.length === 0) {
this.status = EGameStatus.GAME_FLOP;
}
if (this.commonCard.length === 3) {
this.status = EGameStatus.GAME_TURN;
}
if (this.commonCard.length === 4) {
this.status = EGameStatus.GAME_RIVER;
}
if (this.commonCard.length === 5) {
this.status = EGameStatus.GAME_SHOWDOWN;
}
} else {
this.status = EGameStatus.GAME_ACTION;
}
}
sendCard() {
if (this.status === EGameStatus.GAME_START) {
this.setHandCard();
this.setSate();
return;
}
if (this.status === EGameStatus.GAME_FLOP) {
this.fireCards.push(this.poker.getCard());
this.flop();
this.setSate();
return;
}
if (this.status === EGameStatus.GAME_TURN || this.status === EGameStatus.GAME_RIVER) {
this.fireCards.push(this.poker.getCard());
this.commonCard.push(this.poker.getCard());
this.setSate();
return;
}
throw 'error flow sendCard';
}
flop() {
for (let i = 0; i < 3; i++) {
this.commonCard.push(this.poker.getCard());
}
}
compareCard(player: Player, otherPlayer: Player) {
const firstWeight = player.pokeStyle;
const lastWeight = otherPlayer.pokeStyle;
let flag = -1;
if (firstWeight > lastWeight) {
flag = 1;
}
if (firstWeight === lastWeight) {
flag = 0;
}
if (firstWeight < lastWeight) {
flag = -1;
}
return flag;
}
setPlayer(users: IPlayer[]) {
users.forEach((u, position) => {
const player = new Player({
...u,
position,
});
this.allPlayer.push(player);
});
return new Link<Player>(this.allPlayer);
}
getPlayers(type= 'all', withoutPlayers?: Player[]) {
let players = [];
let nextPlayer: ILinkNode<Player> = this.playerLink.link;
let i = 0;
while (i < this.playerSize) {
players.push(nextPlayer.node);
if (nextPlayer.next) {
nextPlayer = nextPlayer.next;
}
i++;
}
players = type === 'all' ? [ ...players, ...this.allInPlayers ] : players;
return withoutPlayers ? players.filter(p => {
const isNotPlayer = withoutPlayers.filter(withoutPlayer => withoutPlayer.userId === p.userId
|| withoutPlayer.evPot >= p.evPot);
return isNotPlayer.length === 0;
}) : players;
}
private clearPlayerAction() {
this.allPlayer.forEach(player => {
player.clearActionSize();
});
}
removePlayer(currPlayer: Player) {
let playerLink = this.playerLink.link;
let player: Player;
while (playerLink.next) {
player = playerLink.next.node;
if (currPlayer.userId === player.userId) {
playerLink.next = playerLink.next.next;
this.playerSize--;
return;
}
playerLink = playerLink.next;
}
}
getWinner() {
if (this.allInPlayers.length === 0 && this.playerSize === 1) {
this.winner.push([ this.currPlayer.node ]);
return;
}
while (this.status !== EGameStatus.GAME_SHOWDOWN) {
this.sendCard();
this.setSate();
}
this.getPlayerPokeStyle();
const getOtherWinner = (withoutPlayers: Player[]) => {
// all player allin, winner can't get all pot
const allPlayer = this.getPlayers('all', withoutPlayers);
const maxLastPlayer = this.getMaxPlayers(allPlayer);
this.winner.push(maxLastPlayer);
if (this.getLeftoverPot() > 0) {
getOtherWinner([ ...withoutPlayers, ...maxLastPlayer ]);
}
};
getOtherWinner([]);
// // compare allin player and last player
// const allPlayers: Player [] = this.getPlayers()
// const maxPlayers = this.getMaxPlayers(allPlayers);
// this.winner.push(maxPlayers);
// // max player can't winner all pot
// const hasSecondWinner = maxPlayers[0].evPot < this.pot;
// // all of winner is all in, must get max curr player
// if (hasSecondWinner) {
// // all player allin, winner can't get all pot
// getOtherWinner(maxPlayers);
// }
}
getPlayerPokeStyle() {
this.allPlayer.map(p => {
p.pokeStyle = new PokerStyle([ ...p.getHandCard(), ...this.commonCard ]).getPokerWeight();
return p;
});
}
getLeftoverPot() {
if (this.winner.length === 0) {
return this.pot;
}
return this.pot - this.winner[this.winner.length - 1][0].evPot;
}
getMaxPlayers(lastPlayers: Player[]) {
const _maxPlayers: Player[] = [];
const maxPlayer = lastPlayers.reduce((acc, cur) => {
return this.compareCard(acc, cur) === 1 ? acc : cur;
});
// has many winner ,equal max player
lastPlayers.forEach(p => {
if (this.compareCard(p, maxPlayer) === 0) {
_maxPlayers.push(p);
}
});
return _maxPlayers;
}
setHandCard() {
// send card start by small blind
let playerLink = this.playerLink.link;
let player: Player;
for (let i = 0; i < 2; i++) {
let j = 0;
while (j < this.playerSize) {
player = playerLink.node;
player.handCard.push(this.poker.getCard());
if (playerLink.next) {
playerLink = playerLink.next;
}
j++;
}
}
}
counting() {
let prevEvPot = 0;
this.winner.forEach((winnerList, key) => {
if (key !== 0) {
prevEvPot = this.winner[key - 1][0].evPot;
}
winnerList.forEach(winner => {
winner.income((winner.evPot - prevEvPot) / winnerList.length);
});
});
}
gameOver() {
// only one player,other fold
this.getWinner();
// todo counting
this.counting();
}
}
+197
View File
@@ -0,0 +1,197 @@
const POKER_STR = 'abcdefghijklm';
function sort(cards: string []): string[] {
let temp = '';
// 排序
for (let i = 0; i < cards.length; i++) {
for (let j = i + 1; j < cards.length; j++) {
if (cards[i] > cards[j]) {
temp = cards[i];
cards[i] = cards[j];
cards[j] = temp;
}
}
}
return cards;
}
interface IPokerStyle {
init(): void;
isStraight(str?: string []): string;
}
export class PokerStyle implements IPokerStyle {
cards: string[] = [];
flushObj = {
1: [] as string[],
2: [] as string[],
3: [] as string[],
4: [] as string[],
};
straightArr: string[] = [];
pokerStyle: string[] = [ '0', '0', '0', '0', '0', '0', '0', '0', '0', '0' ];
numObj: Map<string, number> = new Map(
POKER_STR.split('').map(m => [ m, 0 ]));
constructor(cards: string[]) {
this.cards = sort(cards);
this.init();
}
init() {
let i = 0;
const isTwo = [];
const isThree = [];
let isFour = '0';
let isFullHouse = '0';
let isStraightFlush = '0';
let isFlush = [];
let isRoyalFlush = '0';
let isThreeKind = '';
let isTowPair = '';
let isPair = '';
const highCard = [];
while (i < this.cards.length) {
const color = this.cards[i][1];
const num = this.cards[i][0];
this.straightArr.push(this.cards[i][0]);
this.flushObj[color].push(num);
let value = this.numObj.get(num) || 0;
value++;
this.numObj.set(num, value);
i++;
}
// find flush
for (const f in this.flushObj) {
if (this.flushObj[f].length >= 5) {
// flush is order,so flush[length - 1] is max flush card
isFlush = this.flushObj[f];
}
}
// find two,three,four
for (const [ key, value ] of this.numObj) {
// high card
if (value === 1) {
highCard.unshift(key);
}
// pairs max count 3, source is small to large
if (value >= 2) {
isTwo.unshift(key);
}
// three of kind max count 2
if (value === 3) {
isThree.unshift(key);
}
// four of kind only one
if (value === 4) {
isFour = key;
}
}
// straight flush
if (isFlush.length !== 0 && this.isStraight(isFlush) !== '0') {
if (this.isStraight(isFlush) === 'i') {
isRoyalFlush = '1';
this.pokerStyle[0] = isRoyalFlush;
return;
}
isStraightFlush = this.isStraight(isFlush);
this.pokerStyle[1] = isStraightFlush;
return;
}
// four of kind
if (isFour !== '0') {
isFour += highCard[0];
this.pokerStyle[2] = isFour;
return;
}
// full house
if (isThree.length > 0 && isTwo.length > isThree.length || isThree.length === 2) {
const maxTwoCard = isThree.length === 2 ? isThree[1] : isThree[0] === isTwo[0] ? isTwo[1] : isTwo[0];
const maxThree = isThree[0];
isFullHouse = maxThree + maxTwoCard;
this.pokerStyle[3] = isFullHouse;
return;
}
// flush
if (isFlush.length !== 0) {
this.pokerStyle[4] = isFlush;
return;
}
// straight
if (this.isStraight() !== '0') {
this.pokerStyle[5] = `${this.isStraight()}`;
return;
}
// three of kind
if (isThree.length > 0) {
isThreeKind = isThree.join('');
isThreeKind += highCard[0] + highCard[1];
this.pokerStyle[6] = isThreeKind;
return;
}
// tow pair
if (isTwo.length >= 2) {
isTowPair = isTwo.join('');
// third tow pair card big then high card
const highCardForTowPair = isTwo[3] && isTwo[3] > highCard[0] ? isTwo[3] : highCard[0];
isTowPair += highCardForTowPair;
this.pokerStyle[7] = isTowPair;
return;
}
// pair
if (isTwo.length === 1) {
isPair = isTwo.join('');
isPair += highCard[0] + highCard[1] + highCard[2];
this.pokerStyle[8] = isPair;
return;
}
// High card
highCard.length = 5;
this.pokerStyle[9] = highCard.join('');
}
isStraight(str?: string []): string {
const straightStr = str && str.join('') || [ ...new Set(this.straightArr) ].join('');
let first = -1;
let second = -1;
let three = -1;
function indexOf(str: string): number {
return POKER_STR.indexOf(str);
}
if (straightStr.length === 5 && indexOf(straightStr) > -1) {
return POKER_STR.charAt(indexOf(straightStr));
}
if (straightStr.length === 6) {
first = indexOf(straightStr.slice(0, 5));
second = indexOf(straightStr.slice(1, 6));
if (Math.max(first, second) > -1) {
return POKER_STR.charAt(Math.max(first, second));
}
}
if (straightStr.length === 7) {
first = indexOf(straightStr.slice(0, 5));
second = indexOf(straightStr.slice(1, 6));
three = indexOf(straightStr.slice(2, 7));
if (Math.max(first, second, three) > -1) {
return POKER_STR.charAt(Math.max(first, second, three));
}
}
// special straight "A2345",'m' -> A
if (straightStr.indexOf('m') > -1 && straightStr.indexOf('abcd') > -1) {
return POKER_STR.charAt(12);
}
return '0';
}
getPokerWeight() {
return this.pokerStyle.join('');
}
}
+34
View File
@@ -0,0 +1,34 @@
import { Context } from 'egg';
const LOG_COLLECTION = Symbol('Context#logCollection');
export default {
context: this,
/**
* 获取收集的日志信息
* @returns {any}
*/
getLogs(this: Context) {
const result: any = {};
for (const [ k, val ] of this.logCollection) {
result[k] = val;
}
return result;
},
/**
* 收集日志信息
* @param {string} key
* @param {string} value
*/
setLogCollection(this: Context, key: string, value: string) {
this.logCollection.set(key, value);
},
get logCollection(this: Context) {
if (!this.context[LOG_COLLECTION]) {
this.context[LOG_COLLECTION] = new Map();
}
return this.context[LOG_COLLECTION];
},
set logCollection(this: Context, value) {
this.context[LOG_COLLECTION] = value;
},
};
+15
View File
@@ -0,0 +1,15 @@
module.exports = {
parseMsg(action: any, payload = {}, metadata = {}) {
const meta = Object.assign({}, {
timestamp: Date.now(),
}, metadata);
return {
meta,
data: {
action,
payload,
},
};
},
};
+95
View File
@@ -0,0 +1,95 @@
import { LoggerLevel, Context } from 'egg';
import { FileBufferTransport } from 'egg-logger';
import * as iconv from 'iconv-lite';
import * as moment from 'moment';
import * as os from 'os';
import { OSLogField, BusinessLogField } from '../../interface/Ilog';
const { uid, username } = os.userInfo();
/**
* 日志格式化
*/
class LogFormat extends Map {
logField: BusinessLogField;
osField: OSLogField;
constructor() {
super();
this.osField = {
pid: process.pid,
nodeVersion: process.version,
launchTime: moment().format('YYYY-MM-DD HH:mm:ss'),
osUser: username,
osUid: uid,
};
}
/**
* Log日志进行utf-8编码
*/
protected toBuffer() {
const fields = { ...this.osField, ...this.logField };
const str = JSON.stringify(fields) + '\n';
return iconv.encode(str, 'utf8');
}
/**
* 格式化Log上下文
* @param logInfo
* @param level 日志级别
*/
public formatFetchInfoMsg(logInfo: any[], level: LoggerLevel) {
const collect = logInfo[0];
this.logField = {
fetchConsumeTime: 0,
level: '',
message: '',
requestTime: '',
stack: '',
status: '',
timestamp: '',
total: 0,
requestBody: {},
method: '',
url: '',
};
if (typeof collect === 'string') {
this.logField.message = collect;
this.logField.requestTime = moment(Date.now()).format('YYYY-MM-DD HH:mm:ss');
this.logField.level = level;
this.logField.stack = level === 'ERROR' ? logInfo[1] : '';
} else {
this.logField.timestamp = collect.startTime;
this.logField.requestTime = moment(collect.startTime).format('YYYY-MM-DD HH:mm:ss');
this.logField.status = collect.status;
this.logField.message = collect.message;
this.logField.stack = collect.stack;
this.logField.level = collect.level || level;
this.logField.url = collect.url;
this.logField.total = collect.end - collect.start;
if (collect.fetchStart && collect.fetchEnd) {
this.logField.requestBody = collect.requestBody;
this.logField.method = collect.method;
this.logField.fetchConsumeTime = collect.fetchEnd - collect.fetchStart;
}
}
return this.toBuffer();
}
}
export default class ElkTransport extends FileBufferTransport {
log(this: Context, level: LoggerLevel, args: any) {
const logFormat = new LogFormat();
let buf;
if (!this._stream) {
const err = new Error(`${this.options.file} log stream had been closed`);
console.error(err.stack);
return;
}
buf = logFormat.formatFetchInfoMsg(args, level);
if (buf.length) {
this._write(buf);
}
}
}
+15
View File
@@ -0,0 +1,15 @@
module.exports = {
parseMsg(action: any, payload = {}, metadata = {}) {
const meta = Object.assign({}, {
timestamp: Date.now(),
}, metadata);
return {
meta,
data: {
action,
payload,
},
};
},
};
+29
View File
@@ -0,0 +1,29 @@
'use strict';
import { Controller } from 'egg';
class GameController extends Controller {
async play() {
const { ctx } = this;
const socket = ctx.socket as any;
const app = ctx.app as any;
const nsp = app.io.of('/socket');
const { room } = socket.handshake.query;
console.log(room);
try {
nsp.adapter.clients([ room ], (err: any, clients: any) => {
// 广播信息
nsp.to(room).emit('game', {
clients,
action: 'broadcast',
target: 'participator',
message: '',
});
});
} catch (error) {
app.logger.error(error);
}
}
}
module.exports = GameController;
+48
View File
@@ -0,0 +1,48 @@
'use strict';
import { Controller } from 'egg';
class NspController extends Controller {
async exchange() {
const { ctx } = this;
const socket = ctx.socket as any;
const app = ctx.app as any;
const nsp = app.io.of('/socket');
const message = ctx.args[0] || {};
const client = socket.id;
try {
const { target, payload } = message;
if (!target) return;
const msg = ctx.helper.parseMsg('exchange', payload, { client, target });
nsp.emit(target, msg);
} catch (error) {
app.logger.error(error);
}
}
async broadcast() {
const { ctx } = this;
const socket = ctx.socket as any;
const app = ctx.app as any;
const nsp = app.io.of('/socket');
const message = ctx.args[0] || {};
const { room } = socket.handshake.query;
const rooms = [ room ];
try {
const { payload } = message;
nsp.adapter.clients(rooms, (err: any, clients: any) => {
// 广播信息
nsp.to(room).emit('online', {
clients,
action: 'broadcast',
target: 'participator',
message: payload,
});
});
} catch (error) {
app.logger.error(error);
}
}
}
module.exports = NspController;
+55
View File
@@ -0,0 +1,55 @@
import { Context } from 'midway';
import { ITickMsg } from '../../../interface/ITickMsg';
export default function auth(): any {
return async (ctx: Context, next: () => Promise<any>) => {
const socket = ctx.socket as any;
const id = socket.id;
const app = ctx.app as any;
const nsp = app.io.of('/socket');
const roomService = await app.applicationContext.getAsync('RoomService');
const query = socket.handshake.query;
// 用户信息
const { room, userName } = query;
// 检查房间是否存在,不存在则踢出用户
// 备注:此处 app.redis 与插件无关,可用其他存储代替
const hasRoom = await roomService.findByRoomNumber(room);
function tick(id: number, msg: ITickMsg, nsp: any, socket: any) {
// 踢出用户前发送消息
socket.emit(id, ctx.helper.parseMsg('deny', msg));
// 调用 adapter 方法踢出用户,客户端触发 disconnect 事件
nsp.adapter.remoteDisconnect(id, true, (err: any) => {
ctx.logger.error('room service tick', err);
});
}
function join(roomNumber: string, userName: string, nsp: any, socket: any) {
socket.join(roomNumber);
updatePlayer(roomNumber, `User(${userName}) joined.`, nsp);
}
function updatePlayer(roomNumber: string, message: string, nsp: any) {
// 在线列表
nsp.adapter.clients([ roomNumber ], (err: any, clients: any) => {
// 更新在线用户列表
nsp.to(roomNumber).emit('online', {
clients,
action: 'join',
target: 'participator',
message,
});
});
}
if (!hasRoom) {
tick(id, {
type: 'deleted',
message: 'deleted, room has been deleted.',
}, nsp, socket);
return;
}
join(room, userName, nsp, socket);
await next();
updatePlayer(room, userName, nsp);
};
}
+26
View File
@@ -0,0 +1,26 @@
import { Context, EggAppConfig } from 'egg';
/**
* elk 日志拦截中间件
* @description 接口请求拦截,进行日志上报,根据config的环境配置,设置日志收集,prod默认开启
* @param {EggAppConfig["elkLogger"]} options config 配置项 elkLogger
* @returns {any}
*/
export default function elkLogger(options: EggAppConfig['elkLogger']): any {
return async (ctx: Context, next: () => Promise<any>) => {
const { match, enable } = options;
console.log('jwt', ctx.jwt);
// 是否符合配置规则
if (match(ctx) && enable) {
ctx.setLogCollection('fetchStart', Date.now());
ctx.setLogCollection('url', ctx.url);
ctx.setLogCollection('requestBody', ctx.request.body);
ctx.setLogCollection('message', `${ctx.request.method} ${ctx.url} info`);
ctx.setLogCollection('level', 'INFO');
await next();
ctx.setLogCollection('status', ctx.res.statusCode);
ctx.setLogCollection('fetchEnd', Date.now());
ctx.logger.info(ctx.getLogs());
}
};
}
+22
View File
@@ -0,0 +1,22 @@
import { Context } from 'egg';
/**
* 404状态处理
* @returns {any}
*/
export default function notFound(): any {
return async (ctx: Context, next: () => Promise<any>) => {
await next();
if (ctx.status === 404 && !ctx.body) {
if (ctx.acceptJSON) {
ctx.body = {
code: '404',
data: {},
msg: '页面坐火箭去了',
};
} else {
ctx.body = '<h1>Page Not Found</h1>';
}
}
};
}
+1
View File
@@ -0,0 +1 @@
## public static file directory!
+6
View File
@@ -0,0 +1,6 @@
import { Application } from 'midway';
export default function (app: Application) {
app.io.of('/socket').route('exchange', app.io.controller.nsp.exchange);
app.io.of('/socket').route('broadcast', app.io.controller.nsp.broadcast);
}
+122
View File
@@ -0,0 +1,122 @@
import { EggAppConfig, EggAppInfo, PowerPartial, Context } from 'midway';
export type DefaultConfig = PowerPartial<EggAppConfig>;
export default (appInfo: EggAppInfo) => {
const config = {} as DefaultConfig;
// use for cookie sign key, should change to your own and keep security
config.keys = appInfo.name + '_{{keys}}';
// elk日志中间件,404处理中间件
config.middleware = [ 'elkLogger', 'notFound' ];
// 合成配置
const bizConfig = {
sourceUrl: '',
elkLogger: {
// 请求url匹配规则
match(ctx: Context) {
const reg = /.*/;
return reg.test(ctx.url);
},
// 是否启用
enable: true,
},
};
// 安全处理
config.security = {
csrf: {
enable: false,
},
methodnoallow: {
enable: false,
},
};
// CORS 跨域处理
config.cors = {
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS',
credentials: true,
origin(ctx: Context) {
const origin: string = ctx.get('origin');
// 允许*域名访问
if (origin.indexOf('172.22.88.118') > -1) {
console.log('come in');
return origin;
} else {
return '*';
}
},
};
// 日志配置
config.logger = {
outputJSON: false,
appLogName: 'app.log',
coreLogName: 'core.log',
agentLogName: 'agent.log',
errorLogName: 'error.log',
};
// 业务接口domain
config.apiDomain = {
loanDomain: '*',
};
// jsonwebtoken 插件配置
config.jwt = {
secret: '123456',
enable: true,
match(ctx: Context) {
const reg = /login|register/;
return !reg.test(ctx.originalUrl);
},
};
// socket io setting
config.io = {
namespace: {
'/socket': {
connectionMiddleware: [ 'auth' ],
packetMiddleware: [],
},
},
redis: {
host: '127.0.0.1',
port: 6379,
},
};
config.redis = {
client: {
port: 6379,
host: '127.0.0.1',
password: '123456',
db: 0,
},
};
config.mysql = {
// 单数据库信息配置
client: {
// host
host: '127.0.0.1',
// 端口号
port: '3306',
// 用户名
user: 'root',
// 密码
password: '123456',
// 数据库名
database: 'poker',
},
// 是否加载到 app 上,默认开启
app: true,
// 是否加载到 agent 上,默认关闭
agent: false,
};
return {
...bizConfig,
...config,
};
};
+12
View File
@@ -0,0 +1,12 @@
export const development = {
watchDirs: [
'app',
'lib',
'service',
'config',
'app.ts',
'agent.ts',
'interface.ts',
],
overrideDefault: true,
};
+25
View File
@@ -0,0 +1,25 @@
import { EggAppConfig, PowerPartial } from 'egg';
import { Context } from 'midway';
export default () => {
const config: PowerPartial<EggAppConfig> = {};
// 业务接口domain
config.apiDomain = {
loan: '',
};
// CORS 跨域处理
config.cors = {
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS',
credentials: true,
origin(ctx: Context) {
const origin: string = ctx.get('origin');
// 允许*域名访问
if (origin.indexOf('*') > -1) {
return origin;
} else {
return '*';
}
},
};
return config;
};
+10
View File
@@ -0,0 +1,10 @@
import { EggAppConfig, PowerPartial } from 'egg';
export default () => {
const config: PowerPartial<EggAppConfig> = {};
// 业务接口domain
config.apiDomain = {
loan: '',
};
return config;
};
+27
View File
@@ -0,0 +1,27 @@
import { EggPlugin } from 'egg';
const plugin: EggPlugin = {
static: true,
cors: {
enable: true,
package: 'egg-cors',
},
redis: {
enable: true,
package: 'egg-redis',
},
io: {
enable: true,
package: 'egg-socket.io',
},
jwt: {
enable: true,
package: 'egg-jwt',
},
mysql: {
enable: true,
package: 'egg-mysql',
},
};
export default plugin;
+5
View File
@@ -0,0 +1,5 @@
export interface IAccountInfo {
userAccount: string;
password: string;
nickName?: string;
}
+8
View File
@@ -0,0 +1,8 @@
import { IAccountInfo } from './IAccountInfo';
import { ILoginResult } from './ILoginResult';
export interface IAccountService {
login(accountInfo: IAccountInfo): Promise<ILoginResult>;
authUser(userInfo: IAccountInfo): Promise<boolean>;
register(accountInfo: IAccountInfo): Promise<string>;
}
+7
View File
@@ -0,0 +1,7 @@
export interface ICommandRecord {
roomId: string;
gameId: string;
userId: string;
type: string;
command: string;
}
+15
View File
@@ -0,0 +1,15 @@
import { HttpMethod } from 'urllib';
/**
* @description User-Service abstractions
*/
export interface IFetchOptions {
method?: string;
url: string;
body: object;
head?: object;
timeout?: number;
headers?: object;
type?: HttpMethod;
ssjToken: string;
}
+6
View File
@@ -0,0 +1,6 @@
export interface IGame {
roomId: string;
pot: string;
status: number;
commonCard: string;
}
+7
View File
@@ -0,0 +1,7 @@
export interface IGameRecord {
roomId: string;
userId: string;
commonCard: string;
handCards: string;
status: string;
}
+3
View File
@@ -0,0 +1,3 @@
export interface ILoginResult {
token: string;
}
+5
View File
@@ -0,0 +1,5 @@
export interface IRequestBody {
head: any;
body: any;
}
+11
View File
@@ -0,0 +1,11 @@
export enum ResultCode {
SUCCESS = '000000',
FAILD = '100000',
UNAUTH = '999999',
}
export interface IResult {
code: ResultCode;
data: any;
message: string;
}
+11
View File
@@ -0,0 +1,11 @@
export interface IRoom {
roomNumber: string;
}
export interface IRoomService {
findById(uid: string): Promise<IRoom>;
findByRoomNumber(roomNumber: number): Promise<boolean>;
add(): Promise<any>;
// join(roomNumber: string, userName: string): void;
// leave(roomNumber: string, message: string): void;
}
+4
View File
@@ -0,0 +1,4 @@
export interface ITickMsg {
type: string;
message: string;
}
+5
View File
@@ -0,0 +1,5 @@
export interface IUser {
nickName: string;
account: string;
password?: string;
}
+8
View File
@@ -0,0 +1,8 @@
import { IAccountInfo } from './IAccountInfo';
import { IUser } from './IUser';
export interface IUserService {
findById(uid: string): Promise<IUser>;
findByAccount(account: string): Promise<IUser>;
addUser(accountInfo: IAccountInfo): Promise<any>;
}
+75
View File
@@ -0,0 +1,75 @@
/**
* 系统日志属性
*/
export interface OSLogField {
/**
* 进程识别号
*/
pid: number;
/**
* node 版本号
*/
nodeVersion: string;
/**
* 启动时间
*/
launchTime: string;
/**
* 系统用户名
*/
osUser: string;
/**
* 系统用户id
*/
osUid: number;
}
/**
* 业务日志属性
*/
export interface BusinessLogField {
/**
* 日志时间戳
*/
timestamp: string;
/**
* 请求时间戳
*/
requestTime: string;
/**
* 请求总数
*/
total: number;
/**
* 请求url
*/
url: string;
/**
* 请求状态码
*/
status: string;
/**
* 请求消耗时间
*/
fetchConsumeTime: number;
/**
* 请求描述
*/
message: string;
/**
* 日志级别
*/
level: string;
/**
* 请求错误栈
*/
stack: string;
/**
* 请求信息
*/
requestBody?: any;
/**
* 请求类型
*/
method: string;
}
+45
View File
@@ -0,0 +1,45 @@
import { inject, Context } from 'midway';
import { IRequestBody } from '../interface/IRequestBody';
import { IResult, ResultCode } from '../interface/IResult';
export default class BaseController {
@inject()
protected ctx: Context;
/**
* 获取请求内容
* @returns {IRequestBody}
*/
public getRequestBody(): IRequestBody {
let params: IRequestBody;
params = this.ctx.request.body.params && JSON.parse(this.ctx.request.body.params) || {};
console.log(this.ctx.request.body, 'params');
return params;
}
/**
* 失败回调封装
* @param {Object} data
*/
public success(data: any) {
const result: IResult = {
code: ResultCode.SUCCESS,
data,
message: 'successful',
};
this.ctx.body = result;
}
/**
* 错误回调封装
* @param {string} message
*/
public fail(message: string) {
const result: IResult = {
code: ResultCode.FAILD,
data: {},
message,
};
this.ctx.body = result;
}
}
+45
View File
@@ -0,0 +1,45 @@
import { Context } from 'egg';
import { IFetchOptions } from '../interface/IFetchOptions';
import { inject, config } from 'midway';
export default class BaseService {
@inject()
protected ctx: Context;
@config('apiDomain')
protected apiDomainConfig: any;
/**
* 处理请求
* @param {IFetchOptions} option
* @returns {Promise<any>}
*/
public async fetch(option: IFetchOptions) {
try {
const data = {
head: { ...option.head },
body: { ...option.body },
};
// 用户登录标识
const headers = {
Authorization: `Bearer ${option.ssjToken}`,
};
const ajaxUrl = this.apiDomainConfig.loanDomain + option.url;
// 发起服务请求
const result = await this.ctx.curl(ajaxUrl, {
data,
type: option.type || 'POST',
headers,
dataType: 'json',
});
// 接口请求失败日志上报
if (result.status !== 200) {
this.ctx.logger.error(this.ctx.getLogs());
}
return result.data;
} catch (e) {
this.ctx.logger.error(this.ctx.getLogs());
}
}
}
+80
View File
@@ -0,0 +1,80 @@
import BaseService from '../lib/baseService';
import { Context, inject, provide, plugin, config } from 'midway';
import { IAccountInfo } from '../interface/IAccountInfo';
import { IAccountService } from '../interface/IAccountService';
import { ILoginResult } from '../interface/ILoginResult';
import { IUserService } from '../interface/IUserService';
import { IUser } from '../interface/IUser';
@provide('AccountService')
export class AccountService extends BaseService implements IAccountService {
@inject()
ctx: Context;
@plugin()
jwt: any;
@inject('UserService')
user: IUserService;
@config('jwt')
protected jwtConfig: any;
public login(accountInfo: IAccountInfo): Promise<ILoginResult> {
return new Promise(async (resolve, reject) => {
try {
let token = '';
// 校验用户信息
const isAuth = await this.authUser(accountInfo);
if (isAuth) {
token = await this.getToken(accountInfo.userAccount);
}
const result: ILoginResult = { token };
resolve(result);
} catch (e) {
this.ctx.logger.error('login service error:', e);
reject(e);
}
});
}
public async register(accountInfo: IAccountInfo): Promise<string> {
return new Promise(async (resolve, reject) => {
try {
const hasUser = await this.checkHasUser(accountInfo.userAccount);
if (!hasUser) {
const result = await this.user.addUser(accountInfo);
if (result.affectedRow === 1) {
resolve('user create successful');
}
} else {
reject('User already exists');
}
} catch (e) {
this.ctx.logger.error('register service error:', e);
reject(e);
}
});
}
public async authUser(accountInfo: IAccountInfo) {
const user: IUser = await this.checkHasUser(accountInfo.userAccount);
const valid = user.password === accountInfo.password;
if (!valid) {
throw 'incorrect user account or password.';
}
return valid;
}
private async checkHasUser(userAccount: string): Promise<IUser> {
return await this.user.findByAccount(userAccount);
}
private getToken(userAccount: string) {
const token = this.jwt.sign({ userAccount },
this.jwtConfig.secret, { expiresIn: 60 * 60 });
this.ctx.logger.info(`AccountService getToken token--${token}`);
return token;
}
}
+37
View File
@@ -0,0 +1,37 @@
import BaseService from '../lib/baseService';
import { Context, inject, provide, plugin } from 'midway';
import { IRoom } from '../interface/IRoom';
@provide('RoomService')
export class RoomService extends BaseService {
@inject()
ctx: Context;
@plugin()
mysql: any;
@plugin()
redis: any;
async findById(uid: string): Promise<IRoom> {
return await this.mysql.get('room', { id: uid });
}
async findByRoomNumber(number: string): Promise<boolean> {
const roomNumber = await this.redis.get(`room:${number}`);
console.log(roomNumber, 'redis', number);
return !!roomNumber;
}
async add(expires: number = 3600) {
const number = Math.floor(Math.random() * (1000000 - 100000)) + 100000;
const result = await this.mysql.insert('room', { room_number: number });
const roomRedis = await this.redis.set(`room:${number}`, `${number}`, 'ex', expires);
if (result.affectedRows === 1 && roomRedis === 'OK') {
return { roomNumber: number };
} else {
throw 'room add error';
}
}
}
+31
View File
@@ -0,0 +1,31 @@
import { Context, inject, provide, plugin } from 'midway';
import { IUser } from '../interface/IUser';
import { IUserService } from '../interface/IUserService';
import { IAccountInfo } from '../interface/IAccountInfo';
@provide('UserService')
export class UserService implements IUserService {
@inject()
ctx: Context;
@plugin()
mysql: any;
async findById(uid: string): Promise<IUser> {
return await this.mysql.get('user', { id: uid });
}
async findByAccount(account: string) {
return await this.mysql.get('user', { account });
}
async addUser(accountInfo: IAccountInfo): Promise<any> {
return await this.mysql.insert('user', {
account: accountInfo.userAccount,
password: accountInfo.password,
nick_name: accountInfo.nickName,
});
}
}
+78
View File
@@ -0,0 +1,78 @@
export interface ILinkNode<T> {
node: T;
next: ILinkNode<T> | null;
}
// interface ILink<T> {
// getNode(position: number): T;
// setNode(position: number): void;
// removeNode(position: number): void;
export class Link<T> {
link: ILinkNode<T> = {
node: {} as T,
next: null,
};
constructor(nodes: T[], isCircular: boolean = true) {
let prevNode: ILinkNode<T> = {
node: {} as T,
next: null,
};
nodes.forEach((node, key) => {
const currNode: ILinkNode<T> = {
node,
next: null,
};
// head
if (key === 0) {
this.link = currNode;
} else {
// circular, last node next is first
if (key === nodes.length - 1 && isCircular) {
currNode.next = this.link;
}
prevNode.next = currNode;
}
prevNode = currNode;
});
}
getNode(position: number) {
let linkNode = this.link;
let i = 0;
while (linkNode.next) {
if (i === position) {
return linkNode;
}
linkNode = linkNode.next;
i++;
}
return linkNode;
}
setNode(node: T, position: number) {
let linkNode = this.link;
let i = 0;
const currNode: ILinkNode<T> = {
node,
next: null,
};
while (linkNode.next) {
if (i === position) {
currNode.next = linkNode.next;
linkNode.next = currNode;
return;
}
linkNode = linkNode.next;
i++;
}
currNode.next = linkNode.next;
linkNode.next = currNode;
}
removeNode(position: number) {
}
}
+119
View File
@@ -0,0 +1,119 @@
import { PokerGame, EGameStatus } from '../../../src/app/core/PokerGame';
// @ts-ignore
import { expect } from 'chai';
import { IPlayer } from '../../../src/app/core/Player';
describe('test/app/core/pokerGame.test.ts', () => {
const users: IPlayer[] = [
{
userId: '1',
counter: 200,
},
{
userId: '2',
counter: 200,
},
{
userId: '3',
counter: 50,
},
{
userId: '4',
counter: 400,
},
{
userId: '5',
counter: 1200,
},
];
/**
* game ready
*/
it('game init', async () => {
const game = new PokerGame({
smallBlind: 1,
users,
});
game.play();
expect(game.status).to.equal(EGameStatus.GAME_ACTION);
expect(game.currPlayer.node.actionSize).to.equal(0);
expect(game.pot).to.equal(3);
expect(game.pot).to.equal(3);
expect(game.playerLink.getNode(1).node.actionSize).to.equal(1);
});
/**
* game playing
*/
it('game play', async () => {
const game = new PokerGame({
smallBlind: 1,
users,
});
game.play();
game.action('call');
game.action('call');
game.action('call');
game.action('call');
game.action('check');
game.sendCard();
game.action('raise:10');
game.action('raise:20');
game.action('call');
game.action('call');
game.action('raise:40');
game.action('call');
game.action('call');
game.action('call');
game.action('call');
game.sendCard();
game.action('allin');
game.action('allin');
game.action('allin');
game.action('fold');
game.action('allin');
console.log('cc');
// game.action('raise:10');
console.log(game.commonCard);
console.log(game.pot);
console.log(game.getPlayers());
console.log(game.winner);
console.log(game.winner[0][0].handCard, game.commonCard);
});
// flop
// turn
// river
// chip in
// has Allin need separate pot
// many allin
/**
* game over
*/
it('game over', async () => {
// only one player
// last player, other player fold
// multiple player
// last player, has all in player
// all player all in
// one player all in
// many player all in
// winner
// one winner
// multiple winner
// bisecting pot
// allin player winner and small pot, multiple second winner bisecting pot
// allin player winner and small pot, one second winner
// all player allin, winner can't win all pot,
});
/**
* count
*/
it('count', async () => {
});
// has other pot
});
+45
View File
@@ -0,0 +1,45 @@
/* tslint:disable */
import {PokerStyle} from '../../../src/app/core/PokerStyle';
const assert = require('assert');
describe('test/app/core/pokerStyle.test.ts', () => {
it('Royal Flush', async () => {
let pokerStyle: PokerStyle = new PokerStyle(['i1','j1', 'k1', 'l1', 'm1', 'a2', 'a4'])
// console.log(pokerStyle)
assert.strictEqual(pokerStyle.getPokerWeight() , '1000000000')
});
it('straight flush', async () => {
let pokerStyle: PokerStyle = new PokerStyle(['a1','b1', 'c1', 'd1', 'e1', 'a2', 'a4'])
// console.log(pokerStyle)
assert.strictEqual(pokerStyle.getPokerWeight() , '0a00000000')
});
it('four of kind', async () => {
let pokerStyle: PokerStyle = new PokerStyle(['a1','b2', 'a3', 'b4', 'd1', 'a2', 'a4'])
assert.strictEqual(pokerStyle.getPokerWeight() , '00ad0000000')
});
//
it('full house', async () => {
let pokerStyle: PokerStyle = new PokerStyle(['a1','b2', 'c3', 'b4', 'd1', 'a2', 'b3'])
assert.strictEqual(pokerStyle.getPokerWeight(), '000ba000000');
});
it('two full house', async () => {
let pokerStyle: PokerStyle = new PokerStyle(['a1','b2', 'a3', 'b4', 'd1', 'a2', 'b3'])
assert.strictEqual(pokerStyle.getPokerWeight() , '000ba000000')
});
it('straight', async () => {
let pokerStyle: PokerStyle = new PokerStyle(['a1','c2', 'e3', 'b4', 'd1', 'g2', 'm3'])
assert.strictEqual(pokerStyle.getPokerWeight() , '00000a0000')
});
it('tow pairs to tow full house', async () => {
let pokerStyle: PokerStyle = new PokerStyle(['a1','b2', 'a3', 'b4', 'd1', 'd2', 'b3'])
assert.strictEqual(pokerStyle.getPokerWeight() , '000bd000000')
});
it('tow pairs', async () => {
let pokerStyle: PokerStyle = new PokerStyle(['a1','c2', 'd3', 'b4', 'f1', 'a2', 'c3'])
assert.strictEqual(pokerStyle.getPokerWeight() , '0000000caf00')
});
it('high card', async () => {
let pokerStyle: PokerStyle = new PokerStyle(['a1','i2', 'e3', 'b4', 'd1', 'g2', 'm3'])
assert.strictEqual(pokerStyle.getPokerWeight() , '000000000miged')
});
});
+16
View File
@@ -0,0 +1,16 @@
import { Link } from '../../src/utils/Link';
import { Player } from '../../src/app/core/Player';
describe('test/utils/link.test.ts', () => {
it('link', async () => {
const person1 = new Player({ counter: 1, position: 1, userId: '1' });
const person2 = new Player({ counter: 2, position: 2, userId: '2' });
const person3 = new Player({ counter: 2, position: 3, userId: '3' });
const person4 = new Player({ counter: 2, position: 4, userId: '4' });
// const person5 = new Player({ counter: 2, position: 5, userId: '5' });
const link = new Link<Player>([ person1, person2, person3, person4 ], false);
// console.log(link.getNode(0), 'link--------')
console.log(link);
});
});
+31
View File
@@ -0,0 +1,31 @@
{
"compileOnSave": true,
"compilerOptions": {
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"inlineSourceMap": true,
"module": "commonjs",
"newLine": "lf",
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"outDir": "dist",
"pretty": true,
"skipLibCheck": true,
"strict": true,
"strictPropertyInitialization": false,
"stripInternal": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"target": "ES2018"
},
"exclude": [
"app/public",
"app/views",
"dist",
"node_modules*",
"test",
"**/*.d.ts",
"**/*.spec.ts"
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"extends": ["tslint-config-egg"],
"rules": {
"linebreak-style": [ true, "CRLF" ]
}
}
+7976
View File
File diff suppressed because it is too large Load Diff