在独立进程中实现机器人逻辑

This commit is contained in:
zhl 2020-12-15 19:50:38 +08:00
parent ede87e9eb7
commit b169103835
10 changed files with 522 additions and 26 deletions

8
package-lock.json generated
View File

@ -446,6 +446,14 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
},
"axios": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz",
"integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==",
"requires": {
"follow-redirects": "^1.10.0"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",

View File

@ -10,6 +10,7 @@
"dev": "DEBUG=colyseus:*,jc:* node --require ts-node/register --inspect src/index.ts",
"dev:less": "DEBUG=colyseus:command,jc:* node --require ts-node/register --inspect=0.0.0.0:9229 src/index.ts",
"dev:jc": "DEBUG=jc:* node --require ts-node/register --inspect=0.0.0.0:9229 src/index.ts",
"dev:robot": "DEBUG=jc:* node --require ts-node/register --inspect=0.0.0.0:9228 src/robot.ts",
"loadtest": "colyseus-loadtest loadtest/example.ts --room my_room --numClients 3",
"test": "echo \"Error: no test specified\" && exit 1"
},
@ -28,6 +29,7 @@
"@colyseus/monitor": "^0.12.2",
"@colyseus/proxy": "^0.12.2",
"@colyseus/social": "^0.10.9",
"axios": "^0.21.0",
"colyseus": "^0.14.0",
"cors": "^2.8.5",
"debug": "^4.3.1",

View File

@ -1 +1,2 @@
pm2 start npm --name "card" -- run "dev:jc"
pm2 start npm --name "robot" -- run "dev:robot"

22
src/robot.ts Normal file
View File

@ -0,0 +1,22 @@
import express from "express";
import cors from "cors";
import bodyParser from 'body-parser';
import mainCtrl from './robot/main.controller';
import {initData} from "./common/GConfig";
const app = express()
const port = Number(process.env.PORT || 2500);
initData();
app.use(cors());
app.use(express.json());
app.use(bodyParser.json({}));
app.use('/robot', mainCtrl);
app.listen(port, function () {
console.log(`App is listening on port ${port}!`);
});

411
src/robot/Robot.ts Normal file
View File

@ -0,0 +1,411 @@
import {Client, Room} from "colyseus.js";
import {error, robotLog as log, robotLog as debug} from "../common/Debug";
import {HeroCfg} from "../cfg/parsers/HeroCfg";
import {BaseConst} from "../constants/BaseConst";
import arrUtil from "../utils/array.util";
import {GameStateConst} from "../constants/GameStateConst";
import {Card} from "../rooms/schema/Card";
import {Player} from "../rooms/schema/Player";
import {EffectCardCfg} from "../cfg/parsers/EffectCardCfg";
import {SkillTargetType} from "../rooms/logic/skill/SkillConst";
import CfgMan from "../rooms/logic/CfgMan";
export class Robot {
host: string;
roomId: string;
room: Room;
sessionId: string;
client: Client;
myTurn: boolean = false;
player: Player;
constructor(host: string, roomId: string) {
this.host = host;
this.roomId = roomId;
this.client = new Client(host);
}
async connect() {
try {
this.room = await this.client.joinById(this.roomId);
this.addListeners();
this.sessionId = this.room.sessionId;
this.setReady();
return this.room.sessionId;
} catch (err) {
error(`error join room ${this.host}, ${this.roomId}`)
}
}
addListeners() {
let self = this;
this.room.onMessage("*", (type, data) => {
debug("[ROBOT] received message:", type, "=>", data);
switch (type) {
case 'draw_card_s2c':
// if (data.player == self.sessionId) {
// self.cards = self.cards.concat(data.cards);
// }
break;
case 'player_ready_s2c':
break;
case 'eat_card_s2c':
if (data.errcode == 0 && data.player == self.sessionId) {
self.selectPet();
}
break;
}
});
this.room.onLeave(function () {
debug("[ROBOT] LEFT ROOM", arguments);
self.room.removeAllListeners();
self.room.leave();
});
/**
* , state含有当前房间游戏的所有公共信息,
* */
this.room.onStateChange(function (state) {
self.player = state.players.get(self.sessionId);
// self.players = state.players;
});
// 也可以监听state下某个特定值的变更, 比如下面是监听 当前轮的clientid
this.room.state.listen("currentTurn", (currentValue: string) => {
self.myTurn = currentValue === this.sessionId;
if (self.myTurn) {
self.discard();
}
});
this.room.state.listen("subTurn", (currentValue: string, previousValue: string) => {
// self.mySubTurn = currentValue === self.sessionId;
// if (self.mySubTurn) {
// setTimeout(self.giveup.bind(self), self.delay);
// }
});
// 监听游戏状态的改变
this.room.state.listen("gameState", (currentValue: number, previousValue: number) => {
switch (currentValue) {
case GameStateConst.DETERMINE_TURN:
// console.log('比大小阶段');
//TODO: 随机分牌比大小阶段, 需要
break;
case GameStateConst.CHANGE_HERO:
// console.log('英雄选择阶段');
self.selectHero();
break;
case GameStateConst.STATE_CHANGE_CARD:
// console.log('开局换卡阶段');
self.changeCard();
break;
case GameStateConst.STATE_BEGIN_EAT:
if (!self.myTurn) {
self.eatOrGiveUp();
}
break;
case GameStateConst.STATE_ROUND_RESULT:
console.log('结算轮');
break;
}
});
}
private delay(max: number, min?: number) {
min = min || 0;
let milliseconds = (Math.random()*(max-min)+min) * 1000 | 0;
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
private reply(messageType: string, message: any) {
this.room.send(messageType, message);
}
private checkTriple(cardArr: Card[], card?: Card): Card[] {
if (card) cardArr.push(card);
let pointMap: Map<number, Card[]> = new Map();
let cardIdSet: Set<number> = new Set();
for (let c of cardArr) {
if (c.type !== 1) {
continue;
}
if (pointMap.has(c.number)) {
let arr = pointMap.get(c.number);
arr.push(c);
pointMap.set(c.number, arr);
} else {
pointMap.set(c.number, [c]);
}
cardIdSet.add(c.number);
}
let fetched = false;
let result:Card[] = [];
// 优先出对子
for (let [point, arr] of pointMap) {
if (card) {
if (point == card.number && arr.length >= 3) {
fetched = true;
result = arr;
break;
}
} else {
if (arr.length >= 3) {
fetched = true;
result = arr;
break;
}
}
}
if (fetched) {
return result;
}
let cardIds = [...cardIdSet];
cardIds.sort((a, b) => a - b);
let tmp = [];
for (let i = 0, length = cardIds.length; i < length; i++) {
let cur = cardIds[i];
i == 0 && tmp.push(cur);
if (i > 0) {
if (cur != tmp[i - 1] + 1) {
tmp.length = 0;
}
tmp.push(cur);
}
if (card) {
if (tmp.indexOf(card.number) >= 0 && tmp.length >=3 ){
break;
}
} else {
if (tmp.length >= 3) {
break;
}
}
}
if (tmp.length >= 3) {
let subTmp = [];
for (let i = tmp[0] - 1; i > 0 ; i -- ) {
if (cardIdSet.has(i)) {
subTmp.push(i);
} else {
break;
}
}
for (let i = tmp[tmp.length]; i < cardIdSet.size; i ++) {
if (cardIdSet.has(i)) {
subTmp.push(i);
} else {
break;
}
}
tmp = tmp.concat(subTmp);
for (let point of tmp) {
if (card && point === card.number) {
result.push(card);
} else {
result.push(pointMap.get(point)[0]);
}
}
return result;
} else {
return arrUtil.randomGet(cardArr, 1);
}
}
/**
*
* @private
*/
private getEnemyPlayer(): Player {
let enemys = [];
for (let [,player] of this.room.state.players) {
if (player.team !== this.player.team) {
enemys.push(player);
}
}
return arrUtil.randomOne(enemys);
}
/**
*
* @param player
* @private
*/
private getRandomPet(player: Player): number {
let pets = [];
for (let [, pet] of player.pets) {
if (pet.ap > 0 && pet.state == 1)
pets.push(pet);
}
let result;
if (pets.length > 0) {
result = arrUtil.randomOne(pets);
}
return result ? result.pos : -1;
}
// >>>>>>>>>>>>>>>>>> begin
/**
*
* @private
*/
private async selectHero() {
let heroMap: Map<number, HeroCfg> = global.$cfg.get(BaseConst.HERO);
let heroArr = [...heroMap.values()];
let hero = arrUtil.randomGet(heroArr, 1);
await this.delay(2);
this.reply('select_hero_c2s', {
heroId: hero[0].id
});
}
/**
*
* @private
*/
private async setReady() {
this.reply('play_ready_c2s', '');
}
/**
*
* @private
*/
private async changeCard() {
let cardIds: number[] = [];
await this.delay(2);
this.reply('change_card_c2s', {
cards: cardIds
});
}
/**
*
* @private
*/
private async discard() {
await this.delay(3);
let self = this;
let cardArr = [...self.player.cards.values()];
let cards = self.checkTriple(cardArr);
if (!cards) {
return;
}
let cardIds = cards.map(o => o.id);
self.reply('discard_card_c2s', {
cards: cardIds
});
}
/**
*
* @private
*/
private async eatOrGiveUp() {
await this.delay(2);
let targetCard = [...this.room.state.cards.values()][0];
let cardArr = [...this.player.cards.values()];
let tmpCards = this.checkTriple(cardArr, targetCard);
let next = this.giveup.bind(this);
if (tmpCards.length > 1) {
let cardIds: number[] = [];
for (let card of tmpCards) {
if (card.id !== targetCard.id) {
cardIds.push(card.id);
}
}
next = this.eatCard.bind(this, cardIds, targetCard.id);
}
next.apply(this);
}
/**
*
* @param cardIds
* @param target
* @private
*/
private eatCard(cardIds: number[], target: number) {
log(`${this.sessionId} 吃牌 ${cardIds} -> ${target}`);
this.reply('eat_card_c2s', {
cards: cardIds,
target
});
}
/**
*
* @private
*/
private giveup () {
this.reply('give_up_eat_c2s', {});
}
/**
*
* @private
*/
private async selectPet() {
await this.delay(5, 0.2);
let cards = [...this.room.state.cards.values()];
let result;
let effectMap: Map<number, EffectCardCfg> = global.$cfg.get(BaseConst.EFFECTCARD);
for (let card of cards) {
let effect = effectMap.get(card.effect);
if (effect.type_id == 1) {
result = card;
break;
}
}
if (!result) {
result = arrUtil.randomGet(cards, 1)[0];
}
let targetType: SkillTargetType = CfgMan.getTargetByCard(result.id);
let targetPlayer;
let targetPos;
switch (targetType) {
case SkillTargetType.ENEMY_PLAYER:
targetPlayer = this.getEnemyPlayer();
break;
case SkillTargetType.ENEMY_PET:
for (let [,player] of this.room.state.players) {
if (player.team !== this.player.team) {
let pos = this.getRandomPet(player);
if (pos > - 1) {
targetPlayer = player;
targetPos = pos;
break;
}
}
}
break;
case SkillTargetType.FRIEND_PET:
for (let [,player] of this.room.state.players) {
if (player.team == this.player.team) {
let pos = this.getRandomPet(player);
if (pos > - 1) {
targetPlayer = player;
targetPos = pos;
break;
}
}
}
break;
case SkillTargetType.SELF_PET:
let pos = this.getRandomPet(this.player);
if (pos > - 1) {
targetPlayer = this.player;
targetPos = pos;
break;
}
break;
}
this.reply('select_pet_c2s', {
card: result.id,
player: targetPlayer?.id,
pos: targetPos,
effCards: []
})
}
}

9
src/robot/RobotManage.ts Normal file
View File

@ -0,0 +1,9 @@
import {Robot} from "./Robot";
export class RobotManage {
async addOne({host, room}: { host: string, room: string }) {
let robot = new Robot(host, room);
let sessionId = await robot.connect();
}
}

View File

@ -0,0 +1,25 @@
import express from 'express';
import {robotLog as debug} from "../common/Debug";
import {singleton} from "../common/Singleton";
import {RobotManage} from "./RobotManage";
const router = express.Router();
router.get('/create', async (req, res, next) => {
let query = req.query;
let {host, room } = query;
host = host as string;
room = room as string;
debug(`receive create robot msg: ${host}, ${room}`);
let manage = singleton(RobotManage);
try {
await manage.addOne({host, room});
res.json({errcode: 0});
} catch (err) {
res.json({errcode: 1})
}
})
export default router;

View File

@ -19,7 +19,7 @@ import {GMCommand} from "./commands/GMCommand";
import {GameStateConst} from "../constants/GameStateConst";
import {GameRestartCommand} from "./commands/GameRestartCommand";
import {RobotClient} from "../robot/RobotClient";
import axios from 'axios';
export class GeneralRoom extends Room {
dispatcher = new Dispatcher(this);
@ -100,7 +100,7 @@ export class GeneralRoom extends Room {
}
//TODO: 掉线逻辑
async onLeave (client: Client, consented: boolean) {
if (this.state.gameState === GameStateConst.STATE_GAME_OVER) {
if (this.state.gameState === GameStateConst.STATE_GAME_OVER || this.state.gameState === GameStateConst.STATE_WAIT_JOIN) {
this.state.players.delete(client.sessionId);
this.bUserLeft(client.sessionId);
} else {
@ -210,28 +210,41 @@ export class GeneralRoom extends Room {
}
addRobot(playerId?: string) {
const sessionId = playerId || generateId();
let client = new RobotClient(sessionId, this.state, this.clock, this['onMessageHandlers']);
if (this.reservedSeatTimeouts[sessionId]) {
clearTimeout(this.reservedSeatTimeouts[sessionId]);
delete this.reservedSeatTimeouts[sessionId];
let data = {
host: 'ws://127.0.0.1:2567',
room: this.roomId,
}
// get seat reservation options and clear it
const options = this.reservedSeats[sessionId];
delete this.reservedSeats[sessionId];
this.clients.push(client);
client.ref.once('close', this['_onLeave'].bind(this, client));
// client.ref.on('message', this.onMessage.bind(this, client));
const reconnection = this.reconnections[sessionId];
if (reconnection) {
reconnection.resolve(client);
}
else {
if (this.onJoin) {
this.onJoin(client, options);
}
delete this.reservedSeats[sessionId];
}
this._events.emit('join', client);
axios.get('http://127.0.0.1:2500/robot/create', {
params: data
}).then((res) => {
debugRoom(res.status);
debugRoom(res.data);
}).catch((err) => {
error(err);
})
// const sessionId = playerId || generateId();
// let client = new RobotClient(sessionId, this.state, this.clock, this['onMessageHandlers']);
// if (this.reservedSeatTimeouts[sessionId]) {
// clearTimeout(this.reservedSeatTimeouts[sessionId]);
// delete this.reservedSeatTimeouts[sessionId];
// }
// // get seat reservation options and clear it
// const options = this.reservedSeats[sessionId];
// delete this.reservedSeats[sessionId];
// this.clients.push(client);
// client.ref.once('close', this['_onLeave'].bind(this, client));
// // client.ref.on('message', this.onMessage.bind(this, client));
// const reconnection = this.reconnections[sessionId];
// if (reconnection) {
// reconnection.resolve(client);
// }
// else {
// if (this.onJoin) {
// this.onJoin(client, options);
// }
// delete this.reservedSeats[sessionId];
// }
// this._events.emit('join', client);
}
}

View File

@ -54,6 +54,7 @@ export class DiscardCommand extends Command<CardGameState, { client: Client, car
let newCount = player.extraTime - Math.min(count, 0);
player.extraTime = Math.max(newCount, 0);
}
let tmpCard = player.cards.get(cards[0] + '');
for (let id of cards) {
this.state.cards.set(id+'', player.cards.get(id + ''));
player.cards.delete(id + '');
@ -68,7 +69,11 @@ export class DiscardCommand extends Command<CardGameState, { client: Client, car
if (cards.length === 1) {
let cardArr: Card[] = [...this.state.cards.values()];
this.room.battleMan.onCardDiscarded(player, cardArr[0])
return [new NextSubCommand()];
if (cardArr[0].type == 1) {
return [new NextSubCommand()];
} else {
return [new TurnEndCommand()];
}
} else {
let cardArr: Card[] = [...this.state.cards.values()];
this.room.battleMan.onCardLinkOver(player, cardArr);

View File

@ -40,7 +40,7 @@ let gameUtil = {
let numCfgMap: Map<number, SystemCardCfg> = global.$cfg.get(BaseConst.SYSTEMCARD);
let cfgs = [];
for (let [, cfg] of numCfgMap) {
if (cfg.type_id == 1) {
if (cfg.weight.indexOf(effectId + '') >= 0) {
cfgs.push(cfg);
}
}