UNPKG

board-game

Version:

an online board game engine

297 lines (289 loc) 11.6 kB
/** * Created by wm123 on 2017/6/20. */ const server = require('server'); const extend = require('extend'); const Player = require('./player'); const Room = require('./room'); const Role = require('./role'); const Game = require('./game'); const Board = require('./board'); const Team = require('./team'); const Group = require('./group'); const Messages = require('./messages'); const Dispatcher = require('./dispatcher'); const utils = require('./utils'); const pack = require('./pack'); const { get, socket, error } = server.router; const { file, status, send } = server.reply; const path = require('path'); const seo = require('./seo'); const random = require('./random'); const execute = execute => data => { try { execute(data) } catch (e) { e && console.error(e); data.socket.emit('err', e && (e.message || e)); } }; const notEmpty = value => { return Array.isArray(value) ? value.some(o => typeof o === 'object' ? notEmpty(o) : typeof o !== 'undefined') : (typeof value !== 'undefined' && Object.keys(value).length > 0); } const notSameArray = (value, old, diff) => { if (Array.isArray(value) && Array.isArray(old) && Array.isArray(diff)) { if (diff.every(o => typeof o !== 'undefined' && typeof o !== 'object')) { return value.length !== old.length || value.some((o, index) => o !== old[index]); } else { return true; } } else { return true; } } const diff = (data, old) => { let isArray = Array.isArray(data); let result = (isArray ? [] : { }); old = typeof old === 'undefined' ? (isArray ? [] : { }) : old; for (let name in data) { if (data.hasOwnProperty(name)) { let value = data[name]; if (typeof value === 'undefined') { if (typeof old[name] !== 'undefined') { if (isArray) { result[name] = undefined; } else { result['~' + name] = true; } } } else if (typeof value === 'object') { if (typeof old[name] === 'undefined') { result[name] = value; } else { let d = diff(value, old[name]); if (isArray || notEmpty(d)) { if (!Array.isArray(d) || notSameArray(value, old[name], d)) { result[name] = d; } } } } else if (isArray || value !== old[name]) { result[name] = value; } } } if (typeof data !== 'undefined') { for (let name in old) { if (old.hasOwnProperty(name)) { if (!(name in data) && !name.startsWith('~')) { result['~' + name] = true; } } } } return result; }; const emit = (socket, event, data, key) => { if (key === false) { socket.emit(event, data); } else { let emits = socket.emits || { }; let prop = typeof key !== 'undefined' ? event + '.' + data[key] : event; let send = diff(data, emits[prop]); if (notEmpty(send)) { socket.emit(event, send); emits[prop] = data; socket.emits = emits; } } }; const getRemoteAddress = socket => socket.client.conn.request.headers['x-real-ip'] || socket.client.conn.remoteAddress; const proxy = socket => new Proxy({ }, { get(target, property, receiver) { if (property in target) { return Reflect.get(target, property, receiver); } else { return data => emit(socket, property.replace(/^[_$]/i, ''), data, false); } } }); const last = data => { if (Array.isArray(data)) { return [ data[data.length - 1] ]; } else { if ('last' in data) { let last = data.last; if (typeof last === 'function') { return last.call(data); } else { return last; } } else if ('data' in data) { return last(data.data); } } }; const apply = (execute, data, socket, server) => { if (execute) { if (Array.isArray(execute)) { execute.forEach(exec => apply(exec, data, socket, server)); } else { execute({ data, server, socket, emit: proxy(socket), player: socket.player, room: socket.player && socket.player.room, role: socket.player && socket.player.room && socket.player.role, game: socket.player && socket.player.room && socket.player.room.game, watch: (dispatcher, event, getter, key) => dispatcher && dispatcher.on(dispatcher => emit(socket, event, getter ? getter(dispatcher) : dispatcher.data, key), socket.player), append: (dispatcher, event, getter) => dispatcher && dispatcher.on(dispatcher => { let data = last(getter ? getter(dispatcher) : dispatcher); if (data) emit(socket, event, data, false); }, socket.player, true) }); } } }; const watchRole = (watch, role) => watch(role, 'role'); const watchPlayer = (watch, player) => watch(player, 'player'); const watchRoom = (watch, room) => watch(room, 'room'); const watchGame = (watch, game) => watch(game, 'game', game => game.summary); const watchGroups = (watch, game) => game && game.groups.forEach(group => watch(group, 'group', group => group.summary, 'index')); const watchBoard = (watch, game) => game && watch(game.board, 'board'); const appendRoomMessage = (append, room) => room && append(room.messages, 'message'); const appendGameMessage = (append, game) => game && append(game.messages, 'message'); const defaultMappings = { connection: () => { }, login: [ ({ data, socket, server }) => socket.player = Player.login(server, data.id, data.name || '', data.password), ({ watch, append, player, role, room, game }) => { watchPlayer(watch, player); watchRole(watch, role); watchRoom(watch, room); watchGame(watch, game); watchGroups(watch, game); watchBoard(watch, game); appendRoomMessage(append, room); appendGameMessage(append, game); } ], info: ({ data, socket, server }) => socket.player = Player.get(server, data.id, data.name || ''), rooms: ({ data, emit, server }) => { let page = data && data.page || 0; emit.rooms({ page, list: Room.list(server, page).map(room => room.summary) }); }, settings: ({ emit, server }) => { emit.settings(extend(true, { publish: true }, server.config.room)) }, create: [ ({ data, player }) => player.createRoom(data), ({ watch, append, role, room }) => { watchRole(watch, role); watchRoom(watch, room); appendRoomMessage(append, room); } ], join: [ ({ data, player }) => player.joinRoom(data.roomId), ({ watch, append, role, room }) => { watchRole(watch, role); watchRoom(watch, room); appendRoomMessage(append, room); } ], exit: ({ role }) => role.exit(), move: ({ data, role }) => role.move(data), disable: ({ data, role }) => role.disable(data), enable: ({ data, role }) => role.enable(data), kick: ({ data, role }) => role.kick(data), start: ({ room, role }) => room.start(role), ready: [ ({ role }) => role.ready(), ({ watch, append, game }) => { watchGame(watch, game); watchGroups(watch, game); watchBoard(watch, game); appendGameMessage(append, game); } ], confirm: ({ role }) => role.confirm(), message: ({ role, data }) => role.message(data), disconnect: ({ socket }) => { socket.player && socket.player.leave(); socket.emits = { }; } }; class GameServer { /** * * @param mappings a list of functions, each function accept one param: <object> context. * context.role: role in session * context.data: the data received from client * context.room: room of role in session * context.server: the server object * context.socket: the socket object which is connect to client * context.game: game of role in session * @param config */ constructor(mappings, ...config) { this.mappings = mappings; this.config = extend(true, { }, { Player, Room, Role, Game, Board, Team, Group, assets: './assets', compress: true, watch: false, devtool: 'source-map', stats: 'error-only', presets: [ require.resolve('babel-preset-es2015'), require.resolve('babel-preset-stage-3') ], mvvm: 'vue', css: 'less', random: random.car }, { teams: [ { color: 'white' } ], team: { size: 5, min: 2, regroup: false } }, ...config); this.players = new Map(); this.rooms = new Map(); this.roomIds = []; } start() { pack(this.config, () => { if (!this.server) { this.server = server({ port: this.config.port, 'public': this.config.assets, views: path.join(process.cwd(), this.config.assets), index: false }, [ get('/', ctx => { return seo.need(ctx.req) ? file(path.join(this.config.assets, 'seo.html')) : file(path.join(this.config.assets, 'entry.html')); }), get('/id', ctx => ctx.req.session.id), get('/random', async ctx => send(await this.config.random(ctx))) ] .concat(Object.keys(defaultMappings).map(key => { socket(key, execute(({ data, socket }) => { apply(defaultMappings[key], data, socket, this); apply(this.mappings[key], data, socket, this); })) })) .concat(Object.keys(this.mappings).filter(key => !(key in defaultMappings)).map(key => socket(key, execute(({ data, socket }) => apply(this.mappings[key], data, socket, this))))) .concat([ error(ctx => status(500).send(ctx.error.message)) ])); console.log(`${ this.config.name || '' }服务器在端口${ this.config.port }上已启动`); } }) } static start(mappings, ...config) { return new GameServer(mappings, ...config).start(); } } module.exports = { GameServer, Player, Room, Role, Game, Board, Team, Group, Messages, Dispatcher, utils, random };