project init

This commit is contained in:
zhl 2020-11-30 14:45:21 +08:00
commit 215d09c776
30 changed files with 3384 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
.idea

28
README.md Normal file
View File

@ -0,0 +1,28 @@
# Welcome to Colyseus!
This project has been created using [⚔️ `create-colyseus-app`](https://github.com/colyseus/create-colyseus-app/) - an npm init template for kick starting a Colyseus project in TypeScript.
[Documentation](http://docs.colyseus.io/)
## :crossed_swords: Usage
```
npm start
```
## Structure
- `index.ts`: main entry point, register an empty room handler and attach [`@colyseus/monitor`](https://github.com/colyseus/colyseus-monitor)
- `src/rooms/MyRoom.ts`: an empty room handler for you to implement your logic
- `src/rooms/schema/MyRoomState.ts`: an empty schema used on your room's state.
- `loadtest/example.ts`: scriptable client for the loadtest tool (see `npm run loadtest`)
- `package.json`:
- `scripts`:
- `npm start`: runs `ts-node-dev index.ts`
- `npm run loadtest`: runs the [`@colyseus/loadtest`](https://github.com/colyseus/colyseus-loadtest/) tool for testing the connection, using the `loadtest/example.ts` script.
- `tsconfig.json`: TypeScript configuration file
## License
MIT

25
loadtest/example.ts Normal file
View File

@ -0,0 +1,25 @@
import { Room, Client } from "colyseus.js";
export function requestJoinOptions (this: Client, i: number) {
return { requestNumber: i };
}
export function onJoin(this: Room) {
console.log(this.sessionId, "joined.");
this.onMessage("*", (type, message) => {
console.log(this.sessionId, "received:", type, message);
});
}
export function onLeave(this: Room) {
console.log(this.sessionId, "left.");
}
export function onError(this: Room, err: any) {
console.log(this.sessionId, "!! ERROR !!", err.message);
}
export function onStateChange(this: Room, state: any) {
console.log(this.sessionId, "new state:", state);
}

2568
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@ -0,0 +1,35 @@
{
"private": true,
"name": "my-app",
"version": "1.0.0",
"description": "npm init template for bootstrapping an empty Colyseus project",
"main": "lib/index.js",
"scripts": {
"start": "ts-node-dev src/index.ts",
"loadtest": "colyseus-loadtest loadtest/example.ts --room my_room --numClients 3",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "UNLICENSED",
"bugs": {
"url": "https://github.com/colyseus/create-colyseus/issues"
},
"homepage": "https://github.com/colyseus/create-colyseus#readme",
"devDependencies": {
"@colyseus/loadtest": "^0.14.0",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.1",
"ts-node": "^8.1.0",
"ts-node-dev": "^1.0.0-pre.63",
"typescript": "^3.4.5"
},
"dependencies": {
"@colyseus/command": "^0.1.6",
"@colyseus/monitor": "^0.12.2",
"@colyseus/social": "^0.10.9",
"colyseus": "^0.14.0",
"cors": "^2.8.5",
"express": "^4.16.4",
"express-jwt": "^5.3.1"
}
}

38
src/index.ts Normal file
View File

@ -0,0 +1,38 @@
import http from "http";
import express from "express";
import cors from "cors";
import { Server } from "colyseus";
import { monitor } from "@colyseus/monitor";
// import socialRoutes from "@colyseus/social/express"
import { GeneralRoom } from "./rooms/GeneralRoom";
import {MongooseDriver} from "colyseus/lib/matchmaker/drivers/MongooseDriver";
const port = Number(process.env.PORT || 2567);
const app = express()
app.use(cors());
app.use(express.json())
const server = http.createServer(app);
const gameServer = new Server({
server,
driver: new MongooseDriver(),
});
// register your room handlers
gameServer.define('general_room', GeneralRoom);
/**
* Register @colyseus/social routes
*
* - uncomment if you want to use default authentication (https://docs.colyseus.io/server/authentication/)
* - also uncomment the import statement
*/
// app.use("/", socialRoutes);
// register colyseus monitor AFTER registering your room handlers
app.use("/colyseus", monitor());
gameServer.listen(port);
console.log(`Listening on ws://localhost:${ port }`)

66
src/rooms/GeneralRoom.ts Normal file
View File

@ -0,0 +1,66 @@
import { Room, Client } from "colyseus";
import { CardGameState } from "./schema/CardGameState";
import { OnJoinCommand } from "./commands/OnJoinCommand";
import { PlayReadyCommand} from "./commands/PlayReadyCommand";
import { Dispatcher } from "@colyseus/command";
import {DiscardCommand} from "./commands/DiscardCommand";
import {NextSubCommand} from "./commands/NextSubCommand";
import {SelectPetCommand} from "./commands/SelectPetCommand";
export class GeneralRoom extends Room {
dispatcher = new Dispatcher(this);
maxClients = 4;
clientMap = new Map();
getClient(sessionId: string) {
return this.clientMap.get(sessionId);
}
onCreate (options: any) {
this.setState(new CardGameState());
this.state.gameSate = 0;
this.onMessage("play_ready", (client, message) => {
console.log('play_ready from ', client.sessionId, message);
this.dispatcher.dispatch(new PlayReadyCommand(), {client});
});
this.onMessage("discard_card", (client, message) => {
console.log('discard_card from ', client.sessionId, message);
this.dispatcher.dispatch(new DiscardCommand(), {client, cards: message.cards});
});
this.onMessage("give_up_take", (client, message) => {
console.log('give_up_take from ', client.sessionId, message);
this.dispatcher.dispatch(new NextSubCommand(), {});
});
this.onMessage("select_pet", (client, message) => {
console.log('select_pet from ', client.sessionId, message);
this.dispatcher.dispatch(new SelectPetCommand(), {client, cardId: message.cardId});
});
this.onMessage("*", (client, type, message) => {
//
// Triggers when any other type of message is sent,
// excluding "action", which has its own specific handler defined above.
//
console.log(client.sessionId, "sent", type, message);
});
}
onJoin (client: Client, options: any) {
this.dispatcher.dispatch(new OnJoinCommand(), {
client: client
});
this.clientMap.set(client.sessionId, client);
}
onLeave (client: Client, consented: boolean) {
}
onDispose() {
this.dispatcher.stop();
}
}

View File

@ -0,0 +1,9 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
import {Wait} from "./Wait";
export class AsyncSequence extends Command {
execute() {
return [new Wait().setPayload(1), new Wait().setPayload(2), new Wait().setPayload(3)];
}
}

View File

@ -0,0 +1,13 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
export class ChildAsyncCommand extends Command<CardGameState, { i: number }> {
async execute({ i }: {i: number}) {
await new Promise((resolve) => {
setTimeout(() => {
this.state.i += i;
resolve();
}, 100)
})
}
}

View File

@ -0,0 +1,8 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
export class ChildCommand extends Command<CardGameState, { i: number }> {
execute({ i }: {i: number}) {
this.state.i += i;
}
}

View File

@ -0,0 +1,32 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
export class DeepAsync extends Command<CardGameState> {
async execute() {
this.state.i = 0;
return [new DeepOneAsync(), new DeepOneAsync()];
}
}
export class DeepOneAsync extends Command<CardGameState> {
async execute() {
await this.delay(100);
this.state.i += 1;
return [new DeepTwoAsync()];
}
}
export class DeepTwoAsync extends Command<CardGameState> {
async execute() {
await this.delay(100);
this.state.i += 10;
return [new DeepThreeAsync()];
}
}
export class DeepThreeAsync extends Command<CardGameState> {
async execute() {
await this.delay(100);
this.state.i += 100;
}
}

View File

@ -0,0 +1,29 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
export class DeepSync extends Command<CardGameState> {
execute() {
this.state.i = 0;
return [new DeepOneSync(), new DeepOneSync()];
}
}
export class DeepOneSync extends Command<CardGameState> {
execute() {
this.state.i += 1;
return [new DeepTwoSync()];
}
}
export class DeepTwoSync extends Command<CardGameState> {
execute() {
this.state.i += 10;
return [new DeepThreeSync()];
}
}
export class DeepThreeSync extends Command<CardGameState> {
execute() {
this.state.i += 100;
}
}

View File

@ -0,0 +1,46 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
import gameUtil from "../../utils/game.util";
import {Client} from "colyseus";
import {NextTurnCommand} from "./NextTurnCommand";
import {NextSubCommand} from "./NextSubCommand";
/**
*
*/
export class DiscardCommand extends Command<CardGameState, { client: Client, cards: [string] }> {
validate({ client, cards } = this.payload) {
const player = this.state.players.get(client.sessionId);
return player !== undefined && gameUtil.checkCardsExists(player.cards, cards);
}
execute({ client, cards } = this.payload) {
this.state.cards.clear();
for (let id of cards) {
this.state.cards.set(id, this.state.players.get(client.sessionId).cards.get(id));
this.state.players.get(client.sessionId).cards.delete(id);
this.state.players.get(client.sessionId).cardSet.delete(id);
}
if (this.state.currentTurn == client.sessionId) {
/**
* ,
* ,
*
*/
if (cards.length === 1) {
return [new NextSubCommand()];
} else {
this.state.gameState = 4;
// return [new NextTurnCommand()];
}
} else {
/**
* id和当前轮id不一直, ,
*/
return [new NextTurnCommand()];
}
}
}

View File

@ -0,0 +1,24 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
export class DrawCommand extends Command<CardGameState, {}> {
execute() {
let sessionId = this.state.currentTurn;
let curPlayer = this.state.players.get(sessionId);
let curClient;
for (let client of this.room.clients) {
if (client.sessionId === sessionId) {
curClient = client;
break;
}
}
let cards = [];
for (let i = 0; i < 2; i ++) {
let card = this.state.cardQueue.pop();
cards.push(card);
curPlayer.cards.set(card.id, card);
curPlayer.cardSet.add(card.id);
}
curClient.send('draw_card', cards);
}
}

View File

@ -0,0 +1,12 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
import {ChildAsyncCommand} from "./ChildAsyncCommand";
export class EnqueueAsyncCommand extends Command<CardGameState, { count: number }> {
async execute({ count }: {count: number}) {
this.state.i = 0;
return [...Array(count)].map(_ =>
new ChildAsyncCommand().setPayload({ i: count }));
}
}

View File

@ -0,0 +1,13 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
import {Card} from "../schema/Card";
import {ChildCommand} from "./ChildCommand";
export class EnqueueCommand extends Command<CardGameState, { count: number }> {
execute({ count }: {count: number}) {
this.state.i = 0;
return [...Array(count)].map(_ =>
new ChildCommand().setPayload({ i: count }));
}
}

View File

@ -0,0 +1,20 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
import {NextTurnCommand} from "./NextTurnCommand";
export class NextSubCommand extends Command<CardGameState, {}> {
execute() {
this.state.gameState = 3;
const sessionIds = [...this.state.players.keys()];
let nextSubTurn = this.state.subTurn ?
sessionIds[(sessionIds.indexOf(this.state.subTurn) + 1) % sessionIds.length]
: sessionIds[(sessionIds.indexOf(this.state.currentTurn) + 1) % sessionIds.length];
if (nextSubTurn !== this.state.currentTurn) {
this.state.subTurn = nextSubTurn;
} else {
return [new NextTurnCommand()];
}
}
}

View File

@ -0,0 +1,17 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
import {DrawCommand} from "./DrawCommand";
export class NextTurnCommand extends Command<CardGameState, {}> {
execute() {
this.state.gameState = 2;
this.state.subTurn = '';
const sessionIds = [...this.state.players.keys()];
this.state.currentTurn = (this.state.currentTurn)
? sessionIds[(sessionIds.indexOf(this.state.currentTurn) + 1) % sessionIds.length]
: sessionIds[0];
return [new DrawCommand()]
}
}

View File

@ -0,0 +1,19 @@
import {Command} from "@colyseus/command";
import {CardGameState} from "../schema/CardGameState";
import {Player} from "../schema/Player";
import {Client} from "colyseus";
export class OnJoinCommand extends Command<CardGameState, {
client: Client
}> {
execute({client}: { client: Client }) {
this.state.players.set(client.sessionId, new Player());
if (this.state.players.size >= this.room.maxClients) {
this.room.lock();
this.state.gameState = 1;
}
this.room.broadcast("player_join", `${client.sessionId}`, {except: client});
}
}

View File

@ -0,0 +1,51 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
import {Client} from "colyseus";
import gameUtil from "../../utils/game.util";
import {GeneralRoom} from "../GeneralRoom";
export class PlayReadyCommand extends Command<CardGameState, {
client: Client
}> {
execute({ client } : {client: Client}) {
this.state.players.get(client.sessionId).state = 1;
this.room.broadcast("player_ready", {player: client.sessionId}, {except: client});
let readyCount = 0;
for (let [sessionId, player] of this.state.players) {
if (player.state === 1) {
readyCount ++;
}
}
// 如果所有人的状态都为已准备状态, 则开始发牌
if (readyCount >= this.room.maxClients) {
this.state.cardQueue = gameUtil.initCardQue();
for (let client of this.room.clients) {
let player = this.state.players.get(client.sessionId);
let cards = [];
for (let i = 0; i < 6; i ++) {
let card = this.state.cardQueue.pop();
cards.push(card);
player.cards.set(card.id, card);
player.cardSet.add(card.id);
}
client.send('draw_card', cards);
}
let curClient = this.room.clients[0];
this.state.currentTurn = curClient.sessionId;
this.state.subTurn = '';
let cards = [];
let curPlayer = this.state.players.get(curClient.sessionId);
for (let i = 0; i < 2; i ++) {
let card = this.state.cardQueue.pop();
cards.push(card);
curPlayer.cards.set(card.id, card);
curPlayer.cardSet.add(card.id);
}
curClient.send('draw_card', cards);
this.state.gameState = 2;
}
}
}

View File

@ -0,0 +1,29 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
import {Client} from "colyseus";
import {NextTurnCommand} from "./NextTurnCommand";
export class SelectPetCommand extends Command<CardGameState, {client: Client, cardId: string}> {
execute({client, cardId}: {client: Client, cardId: string}) {
let sessionId = this.state.currentTurn;
let player = this.state.players.get(sessionId);
let ap = 0;
let moreAp = 0;
let count = 0;
for (let card of this.state.cards.values()) {
ap += card.number;
count ++;
if (count > 2) {
moreAp += 10;
}
}
for (let [key, pet] of player.pets) {
if (pet.ap == 0 ) {
pet.ap = ap + moreAp;
break;
}
}
return [new NextTurnCommand()];
}
}

View File

@ -0,0 +1,12 @@
import { Command } from "@colyseus/command";
import { CardGameState } from "../schema/CardGameState";
export class ValidationCommand extends Command<CardGameState, number> {
validate(n = this.payload) {
return n === 1;
}
execute() {
throw new Error("This should never execute!")
}
}

View File

@ -0,0 +1,7 @@
import { Command } from "@colyseus/command";
export class Wait extends Command<any, number> {
async execute(number: number) {
await this.delay(100);
}
}

38
src/rooms/schema/Card.ts Normal file
View File

@ -0,0 +1,38 @@
import {Schema, type, filter} from "@colyseus/schema";
import { Client } from "colyseus";
export class Card extends Schema {
constructor(number: number, type: string, id: string) {
super();
this.number = number;
this.type = type;
this.id = id;
this.played = false;
}
/**
*
*/
// @filter(function(
// this: Card, // the instance of the class `@filter` has been defined (instance of `Card`)
// client: Client, // the Room's `client` instance which this data is going to be filtered to
// value: Card['number'], // the value of the field to be filtered. (value of `number` field)
// root: Schema // the root state Schema instance
// ) {
// return !this.played || this.owner === client.sessionId;
// })
@type("number")
number: number;
@type("string")
owner: string;
@type("string")
id: string;
@type("boolean")
played: boolean = false;
/**
*
*/
@type("string")
type: string;
}

View File

@ -0,0 +1,43 @@
import { Schema, MapSchema, type } from "@colyseus/schema";
import {Player} from "./Player";
import {Card} from "./Card";
export class CardGameState extends Schema {
@type({ map: Player })
players = new MapSchema<Player>();
/**
*
* 0: 等待玩家加入
* 1: 等待玩家准备
* 2: 游戏进行中-
* 3: 游戏进行中-
* 4: 游戏中吃牌后的选随从轮
* 9: 游戏结束
*/
@type("number")
gameState: number = 0;
@type("string")
currentTurn: string;
/**
* , gameState==3
*/
@type("string")
subTurn: string;
@type("number")
i: number;
/**
*
*/
cardQueue: Card[] = [];
/**
*
*/
@type({map: Card})
cards = new MapSchema<Card>() ;
}

22
src/rooms/schema/Pet.ts Normal file
View File

@ -0,0 +1,22 @@
import {Schema, type} from "@colyseus/schema";
export class Pet extends Schema {
/**
*
*/
@type("number")
ap: number;
/**
*
*/
@type("string")
type: string;
constructor() {
super();
this.ap = 0;
this.type = '';
}
}

View File

@ -0,0 +1,49 @@
import {MapSchema, SetSchema, Schema, type} from "@colyseus/schema";
import {Card} from "./Card";
import {Pet} from "./Pet";
export class Player extends Schema {
/**
*
*/
cards = new MapSchema<Card>();
@type({ set: "string" })
cardSet = new SetSchema<string>();
/**
* hp
*/
@type("number")
hp: number;
/**
*
*/
@type("number")
ap: number;
/**
*
* 0: 正常状态
* 1: 已准备好
* 2: 死亡
* 9: 掉线
*/
@type("number")
state: number;
/**
*
*/
@type({ map: Pet })
pets = new MapSchema<Pet>();
//TODO: set hp, ap from cfg
constructor() {
super();
this.state = 0;
this.hp = 200;
this.ap = 30;
for (let i = 0; i < 6; i++) {
this.pets.set(i+'', new Pet());
}
}
}

77
src/utils/array.util.ts Normal file
View File

@ -0,0 +1,77 @@
let arrUtil = {
/**
*
* @param arr
*/
randomSort<T>(arr: Array<T>) {
for (let j, x, i = arr.length; i; j = (Math.random() * i) | 0, x = arr[--i], arr[i] = arr[j], arr[j] = x) {
;
}
},
/**
* object
* @param arr
* @param obj obj | | child字段的值 | child字段的数组
* @param child
*/
contains<T>(arr: Array<T>, obj: any, child: string): boolean {
let result = false;
if (child) {
const isArr = Array.isArray(obj);
if (isArr) {
if (obj[0].hasOwnProperty(child)) {
let set0 = new Set();
for (let s of obj) {
set0.add(s[child]);
}
let set1 = new Set(arr.filter (x => set0.has(x)));
return set0.size === set1.size;
} else {
let set0 = new Set(obj);
let set1 = new Set(arr.filter (x => set0.has(x)));
return set1.size === set0.size;
}
} else {
if (obj.hasOwnProperty(child)) {
for (let sub of arr) {
if (sub.hasOwnProperty(child)) {
// @ts-ignore
if (sub[child] === obj[child]) {
result = true;
break;
}
}
}
} else {
for (let sub of arr) {
if (sub.hasOwnProperty(child)) {
// @ts-ignore
if (sub[child] === obj) {
result = true;
break;
}
}
}
}
}
} else {
// 不指定 比较字段 的话, 只处理2种情况
// 1: obj 为数组
// 2: obj 不是数组
if (Array.isArray(obj)) {
let set0 = new Set(obj);
let set1 = new Set(arr.filter (x => set0.has(x)));
return set1.size === set0.size;
} else {
let idx = arr.indexOf(obj);
return !!(~idx);
}
}
return result;
}
}
export default arrUtil;

41
src/utils/game.util.ts Normal file
View File

@ -0,0 +1,41 @@
import {Card} from "../rooms/schema/Card";
import arrUtil from "./array.util";
let gameUtil = {
// TODO: 根据配表生成牌组
initCardQue() {
let cards: Array<Card> = [];
let nums: Array<number> = [];
let types : Array<number> = [];
for (let i = 0; i < 12; i ++) {
for (let j = 0; j < 16; j ++) {
nums.push(i);
}
}
arrUtil.randomSort(nums);
for (let i = 0; i < 8; i ++) {
for (let j = 0; j < 24; j ++) {
types.push(i);
}
}
arrUtil.randomSort(types);
for (let i = 0; i < nums.length; i++) {
cards.push(new Card(nums[i] + 1, types[i] + '', i + ''));
}
arrUtil.randomSort(cards);
return cards;
},
checkCardsExists(cardMap: Map<string, Card>, cardIds: Array<string>) {
let result = true;
for (let id of cardIds) {
if (!cardMap.has(id)) {
result = false;
break;
}
}
return result;
}
}
export default gameUtil;

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"outDir": "lib",
"target": "es6",
"module": "commonjs",
"strict": true,
"strictNullChecks": false,
"esModuleInterop": true,
"experimentalDecorators": true
}
}