diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..10aa6cb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: node_js +node_js: + - 6.6 + diff --git a/commandRunner.js b/commandRunner.js index 762e18d..35b2bad 100644 --- a/commandRunner.js +++ b/commandRunner.js @@ -1,99 +1,100 @@ -var EventEmitter = require("events").EventEmitter; -var util = require("util"); +import ComponentBase from "./componentBase"; -function CommandRunner(key) { - EventEmitter.call(this); +export default class CommandRunner extends ComponentBase { + constructor(name) { + super(); - this.key = key; - this.commands = []; - this.baseSpeed = 0; - this.angle = 0; - this.loopTimeoutId = null; - this.customTimeoutIds = {}; - this.backgroundTimeoutIds = {}; - this.commandFunctions = { - rotate: (config, turn) => { - if (config.isBuiltIn) { - this.stopLoop(); - this.clearCustomTimeoutIds(); + this.name = name; + this.commands = []; + this.baseSpeed = 0; + this.angle = 0; + this.loopTimeoutId = null; + this.customTimeoutIds = {}; + this.backgroundTimeoutIds = {}; + this.commandFunctions = { + rotate: (config, turn) => { + if (config.isBuiltIn) { + this.stopLoop(); + this.clearCustomTimeoutIds(); + } + const rotateFunction = () => { + this.angle = (this.angle + turn) % 360; + this.publish("command", name, "roll", [0, this.angle]); + this.customTimeoutIds.rotate = setTimeout(rotateFunction, 500); + }; + rotateFunction(); + }, + stop: config => { + if (config.isBuiltIn) { + this.stopLoop(); + this.clearCustomTimeoutIds(); + } + this.publish("command", name, "roll", [0, this.angle]); + }, + dash: (config, baseSpeed, dashTime) => { + if (this.backgroundTimeoutIds.dash) { + clearTimeout(this.backgroundTimeoutIds.dash); + } + this.baseSpeed = baseSpeed; + this.backgroundTimeoutIds.dash = setTimeout(() => { + this.baseSpeed = 0; + }, dashTime * 1000); + }, + roll: (config, speed, degree) => { + this.publish("command", name, "roll", [ + this.baseSpeed + speed, + (360 + degree + this.angle) % 360 + ]); } - const rotateFunction = () => { - this.angle = (this.angle + turn) % 360; - this.emit("command", "roll", [0, this.angle]); - this.customTimeoutIds.rotate = setTimeout(rotateFunction, 500); - }; - rotateFunction(); - }, - stop: config => { - if (config.isBuiltIn) { - this.stopLoop(); - this.clearCustomTimeoutIds(); - } - this.emit("command", "roll", [0, this.angle]); - }, - dash: (config, baseSpeed, dashTime) => { - if (this.backgroundTimeoutIds.dash !== null) { - clearTimeout(this.backgroundTimeoutIds.dash); - } - this.baseSpeed = baseSpeed; - this.backgroundTimeoutIds.dash = setTimeout(() => { - this.baseSpeed = 0; - }, dashTime * 1000); - }, - roll: (config, speed, degree) => { - this.emit("command", "roll", [ - this.baseSpeed + speed, - (360 + degree + this.angle) % 360 - ]); - } - }; -} - -util.inherits(CommandRunner, EventEmitter); + }; + } -CommandRunner.prototype.setCommands = function(commands) { - if (commands.length === 1 && commands[0].time === -1) { - // built-in command - this.commandFunctions[commands[0].commandName].apply(this, [{ - isBuiltIn: true - }].concat(processArguments(commands[0].args))); - } else { - this.commands = commands; - this.stopLoop(); - this.clearCustomTimeoutIds(); - this.loopMethod(0); + setCommands(commands) { + if (commands.length === 1 && commands[0].time === -1) { + // built-in command + this.commandFunctions[commands[0].commandName].apply(this, [{ + isBuiltIn: true + }].concat(processArguments(commands[0].args))); + } else { + this.commands = commands; + this.stopLoop(); + this.clearCustomTimeoutIds(); + this.loopMethod(0); + } } -}; -CommandRunner.prototype.stopLoop = function() { - if (this.loopTimeoutId !== null) { - clearTimeout(this.loopTimeoutId); + stopLoop() { + if (this.loopTimeoutId) { + clearTimeout(this.loopTimeoutId); + this.loopTimeoutId = null; + } } -}; -CommandRunner.prototype.clearCustomTimeoutIds = function() { - Object.keys(this.customTimeoutIds).forEach(timeoutIdName => { - const timeoutId = this.customTimeoutIds[timeoutIdName]; - if (timeoutId !== null) { - clearTimeout(timeoutId); + clearCustomTimeoutIds() { + for (let timeoutIdName in this.customTimeoutIds) { + const timeoutId = this.customTimeoutIds[timeoutIdName]; + if (timeoutId) { + clearTimeout(timeoutId); + delete this.customTimeoutIds[timeoutIdName]; + } } - }); -} + } -CommandRunner.prototype.loopMethod = function(index) { - if (this.commands.length === 0) { - throw new Error("実行しようとしたcommandsは空でした。: " + this.key); + loopMethod(index) { + if (this.commands.length === 0) { + throw new Error("実行しようとしたcommandsは空でした。: " + this.name); + } + this.clearCustomTimeoutIds(); + const currentCommand = this.commands[index]; + this.commandFunctions[currentCommand.commandName].apply(this, [{ + isBuiltIn: false + }].concat(processArguments(currentCommand.args))); + var nextIndex = index + 1 >= this.commands.length ? 0 : index + 1; + this.loopTimeoutId = setTimeout(() => { + this.loopMethod(nextIndex); + }, currentCommand.time * 1000); } - this.clearCustomTimeoutIds(); - const currentCommand = this.commands[index]; - this.commandFunctions[currentCommand.commandName].apply(this, [{ - isBuiltIn: false - }].concat(processArguments(currentCommand.args))); - var nextIndex = index + 1 >= this.commands.length ? 0 : index + 1; - this.loopTimeoutId = setTimeout(() => { - this.loopMethod(nextIndex); - }, currentCommand.time * 1000); -}; +} // コマンドの引数にmotionSpecialDataがあった場合、それを実際の値に変更する function processArguments(args) { @@ -111,5 +112,3 @@ function processArguments(args) { }); } -module.exports = CommandRunner; - diff --git a/componentBase.js b/componentBase.js new file mode 100644 index 0000000..b93a3b9 --- /dev/null +++ b/componentBase.js @@ -0,0 +1,24 @@ +import publisher from "./publisher"; + +export default class ComponentBase { + constructor(models = {}) { + for (let modelName in models) { + this[modelName] = models[modelName]; + } + } + publish(subjectName, ...data) { + publisher.publish(this, subjectName, ...data); + } + subscribeModel(subjectName, observeFunction) { + publisher.subscribeModel(subjectName, (author, ...data) => { + observeFunction.apply(this, data); + }); + } + subscribe(subjectName, observeFunction) { + publisher.subscribe(subjectName, (author, ...data) => { + if (author !== this) { + observeFunction.apply(this, data); + } + }); + } +} diff --git a/config.js b/config.js index afa8ad4..1ee54a6 100644 --- a/config.js +++ b/config.js @@ -13,5 +13,17 @@ export default { wsPort: 8081 }, dashboardPort: 8082, - scoreboardPort: 8083 + scoreboardPort: 8083, + defaultHp: 100, + damage: 10, + defaultColor: "white", + collision: { + meth: 0x01, + xt: 0x20, + xs: 0x20, + yt: 0x20, + ys: 0x20, + dead: 0x02 + }, + isUseNoble: false }; diff --git a/connector.js b/connector.js index 3e451ce..e74b792 100644 --- a/connector.js +++ b/connector.js @@ -1,6 +1,35 @@ -class Connector { - constructor() { +import ComponentBase from "./componentBase"; + +class Connector extends ComponentBase { + constructor(models) { + super(models); + this.rawOrbs = {}; + + this.originalError = console.error; + + console.error = (message) => { + const exec121Error = /Error: Opening (\\\\\.\\)?(.+): Unknown error code (121|1167)/.exec(message); + if (exec121Error) { + const port = exec121Error[2]; + if (this.isConnecting(port)) { + this.publish("incrementError121Count"); + if (this.appModel.error121Count < 5) { + this.publish("log", `Catched 121 error. Reconnecting... (${models.appModel.error121Count})`, "warning"); + this.reconnect(port); + } else { + this.publish("resetError121Count") + this.publish("log", "Catched 121 error. But this is 5th try. Give up.", "warning"); + this.giveUp(port); + } + } else { + this.publish("log", "Catched 121 error. But port is invalid.", "error"); + } + } else { + this.publish("log", "Catched unknown error: \n" + message.toString(), "error"); + } + this.originalError(message); + }; } connect(port, rawOrb) { if (this.isConnecting(port)) { diff --git a/controller.js b/controller.js index 949e995..06bdcdb 100644 --- a/controller.js +++ b/controller.js @@ -17,23 +17,23 @@ export default class Controller extends EventEmitter { } setHp(hp) { this.hp = hp; - if (this.client !== null) { + if (this.client) { this.client.sendCustomMessage("hp", this.hp); } this.emit("hp", this.hp); } setIsOni(isOni) { this.isOni = isOni; - if (this.client !== null) { + if (this.client) { this.client.sendCustomMessage("oni", this.isOni); } this.emit("oni", this.isOni); } setLink(orb) { // client も持っているが、それに左右されずにするため link は別に持つ必要がある - this.linkedOrb = orb !== null ? orb : null; - if (this.client !== null) { - if (orb === null) { + this.linkedOrb = orb; + if (this.client) { + if (!orb) { this.client.unlink(); } else { this.client.setLinkedOrb(orb); @@ -43,21 +43,21 @@ export default class Controller extends EventEmitter { } setClient(client) { this.client = client; - if (this.client !== null) { + if (this.client) { this.client.sendCustomMessage("hp", this.hp); this.client.sendCustomMessage("oni", this.isOni); this.client.sendCustomMessage("color", this.color); - } - if (this.linkedOrb !== null && this.client !== null) { - // HPなどの Orb -> Client への伝達で、 - // client にも linkedOrb を入れておく必要がある。 - this.client.setLinkedOrb(this.linkedOrb); + if (this.linkedOrb) { + // HPなどの Orb -> Client への伝達で、 + // client にも linkedOrb を入れておく必要がある。 + this.client.setLinkedOrb(this.linkedOrb); + } } } setColor(color) { this.color = color; updateColor.call(this); - if (this.client !== null) { + if (this.client) { this.client.sendCustomMessage("color", this.color); } } @@ -65,15 +65,15 @@ export default class Controller extends EventEmitter { return { hp: this.hp, isOni: this.isOni, - link: this.linkedOrb !== null ? this.linkedOrb.name : null, - key: this.client !== null ? this.client.key : null, + link: this.linkedOrb ? this.linkedOrb.name : null, + key: this.client ? this.client.key : null, color: this.color }; } } function updateColor() { - if (this.linkedOrb !== null) { + if (this.linkedOrb) { this.linkedOrb.command("color", [this.color]); } } diff --git a/controllerManager.js b/controllerManager.js new file mode 100644 index 0000000..a319d8f --- /dev/null +++ b/controllerManager.js @@ -0,0 +1,149 @@ +import ComponentBase from "./componentBase"; + +export default class ControllerManager extends ComponentBase { + constructor(models, defaultHp, damage) { + super(models); + + this.defaultHp = defaultHp; + this.damageHp = damage; + + this.subscribe("oni", this.changeIsOni); + this.subscribe("resetHp", this.resetHp); + this.subscribe("color", this.changeColor); + this.subscribe("addClient", this.addClient); + this.subscribe("addedUnknown", this.initializeUnknown); + this.subscribe("removeClient", this.removeClient); + this.subscribe("gameState", this.updateGameState); + this.subscribe("collision", this.damage); + this.subscribe("availableCommandsCount", this.updateAvailableCommandsCount); + this.subscribe("updateLink", this.updateLink); + this.subscribe("rankingState", this.updateRankingState); + this.subscribe("addedClient", this.initializeClient); + this.subscribe("addedController", this.initializeController); + this.subscribe("setCommands", this.setCommands); + this.subscribe("command", this.command); + } + changeIsOni(name, isEnabled) { + this.controllerModel.get(name).setIsOni(isEnabled); + } + resetHp(name) { + this.controllerModel.get(name).setHp(this.defaultHp); + } + changeColor(name, color) { + this.controllerModel.get(name).setColor(color); + } + addClient(key, client) { + this.controllerModel.addUnknownClient(key, client); + } + removeClient(key) { + if (this.controllerModel.hasInUnknownClients(key)) { + this.controllerModel.removeUnknownClient(key); + } else { + const name = this.controllerModel.toName(key); + this.controllerModel.removeClient(name); + } + } + updateGameState(state) { + for (let name in this.controllerModel.controllers) { + if (this.controllerModel.get(name).client) { + this.controllerModel.get(name).client.sendCustomMessage("gameState", state); + } + } + } + updateRankingState(state) { + for (let name in this.controllerModel.controllers) { + if (this.controllerModel.get(name).client) { + this.controllerModel.get(name).client.sendCustomMessage("rankingState", state); + } + } + }; + updateRanking(ranking) { + for (let name in this.controllerModel.controllers) { + if (this.controllerModel.get(name).client) { + this.controllerModel.get(name).client.sendCustomMessage("ranking", ranking); + } + } + } + damage(orb) { + for (let controllerName in this.controllerModel.controllers) { + const controller = this.controllerModel.get(controllerName); + if (this.appModel.gameState === "active" && !controller.isOni && + controller.client && orb.linkedClients.indexOf(controller.client.key) !== -1) { + controller.setHp(controller.hp - this.damageHp); + } + } + } + updateAvailableCommandsCount(count) { + for (let name in this.controllerModel.controllers) { + const client = this.controllerModel.get(name).client; + if (client) { + client.sendCustomMessage("availableCommandsCount", count); + } else { + console.warn("Tryed to update availableCommandsCount but client is null. name: " + name); + } + } + } + updateLink(controllerName, orbName) { + this.controllerModel.get(controllerName).setLink( + orbName ? this.orbModel.getOrbFromSpheroWS(orbName) : null); + } + initializeUnknown(key, client) { + client.on("arriveCustomMessage", (name, data, mesID) => { + // Nameが同じなら、clientKeyが別でもHPなどが引き継がれる、と実装するため、 + // requestNameとuseDefinedNameを分けている。 + // requestName ・・ 新しい名前を使う。もしその名前が既に使われていたらrejectする。 + // useDefinedName ・・既存の名前を使う。もしその名前がなければrejectする。 + if (name === "requestName") { + if (this.controllerModel.has(data)) { + client.sendCustomMessage("rejectName", null); + } else { + this.controllerModel.setName(key, data); + client.sendCustomMessage("acceptName", data); + } + } else if (name === "useDefinedName") { + if (!this.controllerModel.has(data)) { + client.sendCustomMessage("rejectName", null); + } else { + this.controllerModel.setName(key, data); + client.sendCustomMessage("acceptName", data); + } + } + }); + } + initializeClient(name) { + const controller = this.controllerModel.get(name); + const client = controller.client; + + client.sendCustomMessage("gameState", this.appModel.gameState); + client.sendCustomMessage("rankingState", this.appModel.rankingState); + client.sendCustomMessage("availableCommandsCount", this.appModel.availableCommandsCount); + client.sendCustomMessage("clientKey", client.key); + + client.on("arriveCustomMessage", (messageName, data, mesID) => { + if (messageName === "commands") { + this.publish("setCommands", name, data); + } + }); + } + initializeController(name) { + console.log(name); + const controller = this.controllerModel.get(name); + controller.on("hp", hp => { + this.publish("hp", name, hp); + }); + } + setCommands(name, commands) { + const controller = this.controllerModel.get(name); + controller.commandRunner.setCommands(commands); + } + command(controllerName, commandName, args) { + const controller = this.controllerModel.get(controllerName); + if (controller.linkedOrb) { + if (!controller.linkedOrb.hasCommand(commandName)) { + throw new Error(`command : ${commandName} is not valid.`); + } + controller.linkedOrb.command(commandName, args); + } + } +} + diff --git a/controllerModel.js b/controllerModel.js deleted file mode 100644 index 71ee855..0000000 --- a/controllerModel.js +++ /dev/null @@ -1,80 +0,0 @@ -import Controller from "./controller"; -import CommandRunner from "./commandRunner"; -import { EventEmitter } from "events"; - -class ControllerModel extends EventEmitter { - constructor() { - super(); - - // 最初、controllerはunnamedControllerに追加される。 - // name がわかると、それが controllers に移行される。 - // こうすることで、clientが異なっても、nameが同じ場合、HPなどを共有できるようになる。 - - // { key: Client, ... } - this.unnamedClients = {}; - // { name: Controller, ... } - this.controllers = {}; - } - add(key, client) { - this.unnamedClients[key] = client; - this.emit("add", key, client); - } - setName(key, name) { - if (typeof this.unnamedClients[key] === "undefined") { - throw new Error("setNameしようとしたところ、keyに対するclientが見つかりませんでした。 key: " + key); - } - let isNewName = typeof this.controllers[name] === "undefined"; - if (isNewName) { - this.controllers[name] = new Controller(name, new CommandRunner(key)); - } else if (this.controllers[name].client !== null) { - throw new Error("setNameしようとしましたが、既にclientが存在します。 name: " + name); - } - this.controllers[name].setClient(this.unnamedClients[key]); - delete this.unnamedClients[key]; - this.emit("named", key, name, isNewName); - } - removeFromUnnamedClients(key) { - if (this.hasInUnnamedClients(key)) { - delete this.unnamedClients[key]; - this.emit("removeUnnamed", key); - } - } - removeClient(name) { - if (this.has(name)) { - this.controllers[name].setClient(null); - this.emit("remove", name); - } - } - hasInUnnamedClients(key) { - return typeof this.unnamedClients[key] !== "undefined"; - } - has(name) { - return typeof this.controllers[name] !== "undefined"; - } - get(name) { - return this.controllers[name]; - } - getAllStates() { - const result = {}; - Object.keys(this.controllers).forEach(name => { - result[name] = this.controllers[name].getStates(); - }); - return result; - } - getUnnamedKeys() { - return Object.keys(this.unnamedClients); - } - toName(key) { - const nameArray = Object.keys(this.controllers).filter(name => { - return this.controllers[name].client !== null && - this.controllers[name].client.key === key; - }); - if (nameArray.length === 1) { - return nameArray[0]; - } else { - return null; - } - } -} - -export default new ControllerModel(); diff --git a/dashboard.js b/dashboard.js index fee825c..b18055d 100644 --- a/dashboard.js +++ b/dashboard.js @@ -1,216 +1,174 @@ import express from "express"; import io from "socket.io"; -import { EventEmitter } from "events"; import util from "util"; -import OrbMap from "./util/orbMap"; -import controllerModel from "./controllerModel"; - -let instance = null; - -function Dashboard(port) { - EventEmitter.call(this); - - if (instance !== null) { - return instance; +import { Server as createServer } from "http"; +import socketIO from "socket.io"; +import ComponentBase from "./componentBase"; + +const socketSubjects = [ + "gameState", + "rankingState", + "availableCommandsCount", + "addOrb", + "removeOrb", + "oni", + "checkBattery", + "resetHp", + "pingall", + "color" +]; + +export default class Dashboard extends ComponentBase { + constructor(models, port) { + super(models); + + this.app = express(); + this.server = createServer(this.app); + this.io = socketIO(this.server); + this.io.origins(`localhost:${port}`); + + this.socket = null; + + + this.app.use(express.static("dashboard")); + this.server.listen(port, () => { + console.log(`dashboard is listening on port ${port}`); + }); + + this.io.on("connection", this.initializeConnection.bind(this)); + + this.subscribe("addedUnknown", (key, client) => { + if (this.socket) { + this.socket.emit("addUnnamed", key); + } + }); + this.subscribe("addedClient", (name) => { + if (this.socket) { + const controller = this.controllerModel.get(name); + this.socket.emit("named", controller.client.key, name, controller.getStates()); + } + }); + this.subscribe("removedUnknown", key => { + if (this.socket) { + this.socket.emit("removeUnnamed", key); + } + }); + this.subscribe("removedClient", name => { + if (this.socket) { + this.socket.emit("removeClient", name); + } + }); + + this.subscribe("addedOrb", this.addOrb); + this.subscribe("removedOrb", this.removeOrb); + this.subscribe("updateBattery", this.updateBattery); + this.subscribe("replyPing", this.updatePingState); + this.subscribe("log", this.logAsClientMessage); + this.subscribe("streamed", this.streamed); + this.subscribe("updateLink", this.updateUnlinkedOrbs); + this.subscribe("addOrb", this.updateUnlinkedOrbs); + this.subscribe("hp", this.updateHp); } - - this.app = express(); - this.server = require("http").Server(this.app); - this.io = require("socket.io")(this.server); - this.io.origins(`localhost:${port}`); - - this.socket = null; - - this.gameState = "inactive"; - this.rankingState = "hide"; - this.availableCommandsCount = 1; - - this.orbMap = new OrbMap(); - - this.app.use(express.static("dashboard")); - this.server.listen(port, () => { - console.log(`dashboard is listening on port ${port}`); - }); - - this.io.on("connection", socket => { - if (this.socket !== null) { - socket.disconnect(); + initializeConnection(socket) { + if (this.socket) { + this.socket.disconnect(); console.log("a dashboard rejected."); } else { console.log("a dashboard connected."); this.socket = socket; - this.log("accepted a dashboard.", "success"); + this.logAsClientMessage("accepted a dashboard.", "success"); socket.emit( - "defaultData", - this.gameState, - this.availableCommandsCount, - controllerModel.getAllStates(), - this.orbMap.toArray(), - controllerModel.getUnnamedKeys()); - socket.on("gameState", state => { - if (/^(active|inactive)$/.test(state)) { - this.gameState = state; - this.emit("gameState", state); - } - }); - socket.on("rankingState", state => { - if (/^(show|hide)$/.test(state)) { - this.rankingState = state; - this.emit("rankingState", state); - } - }); - socket.on("availableCommandsCount", count => { - if (count >= 1 && count <= 6) { - this.availableCommandsCount = count; - this.emit("availableCommandsCount", count); - } - }); - socket.on("link", (name, orbName) => { - this.emit("updateLink", name, orbName); - }); - socket.on("addOrb", (name, port) => { - this.emit("addOrb", name, port); - }); - socket.on("removeOrb", name => { - this.emit("removeOrb", name); - }); - socket.on("orbReconnect", name => { - this.emit("reconnect", name); + "defaultData", + this.appModel.gameState, + this.appModel.availableCommandsCount, + this.controllerModel.getAllStates(), + this.orbModel.toArray(), + this.controllerModel.getUnnamedKeys()); + + socketSubjects.forEach(subjectName => { + this.socket.on(subjectName, (...data) => { + this.publish(subjectName, ...data); + }); }); - socket.on("oni", (name, enable) => { - this.emit("oni", name, enable); + + // 引数は渡さないので別の方法で結びつける + this.socket.on("pingAll", () => { + this.publishPingAll(); }); - socket.on("checkBattery", () => { - this.emit("checkBattery"); + + // link -> updateLink と名前が変わるので、別の方法で結びつける + this.socket.on("link", (controllerName, orbName) => { + this.publishUpdateLink(controllerName, orbName); }); + + socket.emit("updateOrbs", this.orbModel.toArray()); socket.on("disconnect", () => { console.log("a dashboard removed."); this.socket = null; }); - socket.on("resetHp", name => { - this.emit("resetHp", name); - }); - socket.on("pingAll", () => { - this.emit("pingAll"); - Object.keys(this.orbMap.orbs).forEach(orbName => { - this.orbMap.setPingState(orbName, "no reply"); - }); - socket.emit("updateOrbs", this.orbMap.toArray()); - }); - socket.on("color", (name, color) => { - this.emit("color", name, color); - }); - } - }); - - controllerModel.on("add", (key, client) => { - if (this.socket !== null) { - this.socket.emit("addUnnamed", key); - } - }); - controllerModel.on("named", (key, name) => { - if (this.socket !== null) { - this.socket.emit("named", key, name, controllerModel.get(name).getStates()); } - }); - controllerModel.on("removeUnnamed", key => { - if (this.socket !== null) { - this.socket.emit("removeUnnamed", key); - } - }); - controllerModel.on("remove", name => { - if (this.socket !== null) { - this.socket.emit("removeClient", name); - } - }); - - instance = this; - return this; -} - -Dashboard.prototype.addOrb = function(name, port) { - if (this.orbMap.has(name)) { - throw new Error(`追加しようとしたOrbは既に存在します。 : ${name}`); - } - this.orbMap.set(name, { - orbName: name, - port, - battery: null, - link: "unlinked", - pingState: "unchecked" - }); - if (this.socket !== null) { - this.socket.emit("updateOrbs", this.orbMap.toArray()); } -}; - -Dashboard.prototype.removeOrb = function(name) { - if (!this.orbMap.has(name)) { - throw new Error(`削除しようとしたOrbは存在しません。 : ${name}`); + publishPingAll() { + this.publish("pingAll"); } - this.orbMap.remove(name); - if (this.socket !== null) { - this.socket.emit("updateOrbs", this.orbMap.toArray()); - } -}; - -Dashboard.prototype.updateUnlinkedOrbs = function(unlinkedOrbs) { - const unlinkedOrbNames = Object.keys(unlinkedOrbs); - this.orbMap.getNames().forEach(orbName => { - this.orbMap.setLink( - orbName, - unlinkedOrbNames.indexOf(orbName) >= 0 ? "unlinked" : "linked"); - }); - if (this.socket !== null) { - this.socket.emit("updateOrbs", this.orbMap.toArray()); + addOrb(name, orb) { + if (this.socket) { + this.socket.emit("updateOrbs", this.orbModel.toArray()); + } } -}; -Dashboard.prototype.updateBattery = function(orbName, batteryState) { - const orbNameItem = this.orbMap.get(orbName); - if (typeof orbNameItem === "undefined") { - throw new Error("updateBattery しようとしましたが、orb が見つかりませんでした。 : " + orbName); - } - orbNameItem.battery = batteryState; - if (this.socket !== null) { - this.socket.emit("updateOrbs", this.orbMap.toArray()); + removeOrb(name) { + if (this.socket) { + this.socket.emit("updateOrbs", this.orbModel.toArray()); + } } -}; -Dashboard.prototype.updateHp = function(controllerKey, hp) { - if (this.socket !== null) { - this.socket.emit("hp", controllerKey, hp); + updateUnlinkedOrbs() { + const unlinkedOrbs = this.orbModel.getUnlinkedOrbs(); + this.orbModel.getNames().forEach(orbName => { + this.orbModel.setLink( + orbName, + unlinkedOrbs[orbName] ? "unlinked" : "linked"); + }); + if (this.socket) { + this.socket.emit("updateOrbs", this.orbModel.toArray()); + } } -}; -Dashboard.prototype.log = function(logText, logType) { - if (this.socket !== null) { - this.socket.emit("log", logText, logType); + updateBattery() { + if (this.socket) { + this.socket.emit("updateOrbs", this.orbModel.toArray()); + } } -}; - -Dashboard.prototype.updatePingState = function(orbName) { - if (!this.orbMap.has(orbName)) { - throw new Error("updatePingState しようとしましたが、orb が見つかりませんでした。 : " + orbName); + updateHp(name, hp) { + if (this.socket) { + this.socket.emit("hp", name, hp); + } } - this.orbMap.setPingState(orbName, "reply"); - if (this.socket !== null) { - this.socket.emit("updateOrbs", this.orbMap.toArray()); + logAsClientMessage(logText, logType) { + if (this.socket) { + this.socket.emit("log", logText, logType); + } } -}; - -Dashboard.prototype.streamed = function(orbName, time) { - if (this.socket !== null) { - this.socket.emit("streamed", orbName, time); + updatePingState(orbName) { + if (!this.orbModel.has(orbName)) { + throw new Error("updatePingState しようとしましたが、orb が見つかりませんでした。 : " + orbName); + } + if (this.socket) { + this.socket.emit("updateOrbs", this.orbModel.toArray()); + } } -}; - -Dashboard.prototype.successReconnect = function(orbName) { - if (this.socket !== null) { - this.socket.emit("successReconnect", orbName); + streamed(orbName, time) { + if (this.socket) { + this.socket.emit("streamed", orbName, this.formatTime(time)); + } } -}; - -util.inherits(Dashboard, EventEmitter); - -module.exports = Dashboard; + formatTime(time) { + return ("0" + time.getHours()).slice(-2) + ":" + + ("0" + time.getMinutes()).slice(-2) + ":" + + ("0" + time.getSeconds()).slice(-2); + } + publishUpdateLink(controllerName, orbName) { + this.publish("updateLink", controllerName, orbName); + } +} diff --git a/dashboard/index.html b/dashboard/index.html index c03ca4b..85023c0 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -59,7 +59,6 @@

Ping Status Last Streamed Disconnect - Reconnect diff --git a/dashboard/js/controller.js b/dashboard/js/controller.js index 86a11df..1372114 100644 --- a/dashboard/js/controller.js +++ b/dashboard/js/controller.js @@ -109,6 +109,6 @@ function updateOrbSelect() { item.textContent = orbName; this.orbSelectElement.appendChild(item); }); - this.orbSelectElement.value = this.linkedOrb === null ? unlinkedText : this.linkedOrb; + this.orbSelectElement.value = this.linkedOrb || unlinkedText; } diff --git a/dashboard/js/controllerManager.js b/dashboard/js/controllerManager.js index 9813d3f..b1f01f3 100644 --- a/dashboard/js/controllerManager.js +++ b/dashboard/js/controllerManager.js @@ -9,10 +9,9 @@ export default class ControllerManager { this.orbNames = []; eventPublisher.on("defaultControllers", controllers => { - Object.keys(controllers).forEach(name => { - this.addController(controllers[name].key !== null ? controllers[name].key : "no client", - name, controllers[name]); - }); + for (let name in controllers) { + this.addController(controllers[name].key || "no client", name, controllers[name]); + } }); eventPublisher.on("named", (key, name, controllerDetails) => { if (this.has(name)) { diff --git a/dashboard/js/orbManager.js b/dashboard/js/orbManager.js index 2597e6b..dcef07a 100644 --- a/dashboard/js/orbManager.js +++ b/dashboard/js/orbManager.js @@ -72,16 +72,6 @@ export default class OrbManager { eventPublisher.emit("disconnect", orbName); }); disconnectTd.appendChild(disconnectButton); - const reconnectTd = document.createElement("td"); - reconnectTd.classList.add("td-reconnect"); - trElement.appendChild(reconnectTd); - const reconnectButton = document.createElement("button"); - reconnectButton.textContent = "Reconnect"; - reconnectButton.addEventListener("click", () => { - reconnectButton.disabled = true; - eventPublisher.emit("reconnect", orbName); - }); - reconnectTd.appendChild(reconnectButton); this.updateLinkForRow(orbName); this.updateBatteryForRow(orbName); this.updatePingStateForRow(orbName); @@ -106,8 +96,7 @@ export default class OrbManager { throw new Error("updateBattery しようとした Orb は存在しませんでした。 : " + orbName); } const batteryTd = trElement.querySelector(".td-battery"); - batteryTd.textContent = - this.orbMap.get(orbName).battery === null ? "unchecked" : this.orbMap.get(orbName).battery; + batteryTd.textContent = this.orbMap.get(orbName).battery || "unchecked"; } updatePingStateForRow(orbName) { const trElement = this.getRow(orbName); @@ -125,14 +114,9 @@ export default class OrbManager { const streamTimeTd = trElement.querySelector(".td-stream-time"); streamTimeTd.textContent = time; } - enableReconnectButton(orbName) { - const trElement = this.getRow(orbName); - const reconnectButton = trElement.querySelector(".td-reconnect > button"); - reconnectButton.disabled = false; - } getRow(orbName) { const trElement = document.querySelector(`[data-row-name="${orbName}"]`); - if (trElement === null) { + if (!trElement) { throw new Error("getRow しようとした Row は存在しませんでした。 : " + orbName); } return trElement; diff --git a/dashboard/js/socketManager.js b/dashboard/js/socketManager.js index ca4f149..1cb2b81 100644 --- a/dashboard/js/socketManager.js +++ b/dashboard/js/socketManager.js @@ -2,7 +2,7 @@ import eventPublisher from "./publisher"; let instance = null; function SocketManager() { - if (instance !== null) { + if (instance) { return instance; } eventPublisher.on("gameState", this.sendGameState.bind(this)); @@ -11,7 +11,6 @@ function SocketManager() { eventPublisher.on("link", this.sendLink.bind(this)); eventPublisher.on("addOrb", this.sendAddOrb.bind(this)); eventPublisher.on("disconnect", this.sendDisconnect.bind(this)); - eventPublisher.on("reconnect", this.sendReconnect.bind(this)); eventPublisher.on("oni", this.sendOni.bind(this)); eventPublisher.on("checkBattery", this.sendCheckBattery.bind(this)); eventPublisher.on("resetHp", this.sendResetHp.bind(this)); @@ -50,9 +49,6 @@ function SocketManager() { this.socket.on("streamed", (orbName, time) => { emit.call(this, "streamed", [orbName, time]); }); - this.socket.on("successReconnect", orbName => { - emit.call(this, "successReconnect", [orbName]); - }); instance = this; } @@ -81,10 +77,6 @@ SocketManager.prototype.sendDisconnect = function(name) { this.socket.emit("removeOrb", name); }; -SocketManager.prototype.sendReconnect = function(name) { - this.socket.emit("orbReconnect", name); -}; - SocketManager.prototype.sendOni = function(controllerName, isEnabled) { this.socket.emit("oni", controllerName, isEnabled); }; diff --git a/main.js b/main.js index 31d8ab9..0b8f113 100644 --- a/main.js +++ b/main.js @@ -1,290 +1,54 @@ const originalError = console.error; -let error121Count = 0; -console.error = function(message) { - const exec121Error = /Error: Opening (\\\\\.\\)?(.+): Unknown error code (121|1167)/.exec(message); - if (exec121Error !== null) { - const port = exec121Error[2]; - if (connector.isConnecting(port)) { - error121Count++; - if (error121Count < 5) { - dashboard.log(`Catched 121 error. Reconnecting... (${error121Count})`, "warning"); - connector.reconnect(port); - } else { - error121Count = 0; - dashboard.log("Catched 121 error. But this is 5th try. Give up.", "warning"); - connector.giveUp(port); - } - } else { - dashboard.log("Catched 121 error. But port is invalid.", "error"); - } - } else { - dashboard.log("Catched unknown error: \n" + message.toString(), "error"); - } - originalError(message); -}; - import spheroWebSocket from "sphero-websocket"; import argv from "argv"; import config from "./config"; -import VirtualSphero from "sphero-ws-virtual-plugin"; import Dashboard from "./dashboard"; import Scoreboard from "./scoreboard"; import CommandRunner from "./commandRunner"; import Controller from "./controller"; -import controllerModel from "./controllerModel"; +import controllerModel from "./model/controllerModel"; import RankingMaker from "./rankingMaker"; import Connector from "./connector"; -import eventPublisher from "./publisher"; +import publisher from "./publisher"; +import SpheroServerManager from "./spheroServerManager"; +import VirtualSpheroManager from "./virtualSpheroManager"; +import ControllerManager from "./controllerManager"; +import OrbController from "./orbController"; +import OrbModel from "./model/orbModel"; +import AppModel from "./model/appModel"; +import ControllerModel from "./model/controllerModel"; +import UUIDManager from "./uuidManager"; + +const models = { + appModel: new AppModel(), + orbModel: new OrbModel(), + controllerModel: new ControllerModel() +}; const opts = [ { name: "test", type: "boolean" } ]; const isTestMode = argv.option(opts).run().options.test; +models.appModel.isTestMode = isTestMode; const spheroWS = spheroWebSocket(config.websocket, isTestMode); - -const virtualSphero = new VirtualSphero(config.virtualSphero.wsPort); - -const dashboard = new Dashboard(config.dashboardPort); -dashboard.updateUnlinkedOrbs(spheroWS.spheroServer.getUnlinkedOrbs()); - -const scoreboard = new Scoreboard(config.scoreboardPort); - -let gameState = "inactive"; -let rankingState = "hide"; -let availableCommandsCount = 1; - -const rankingMaker = new RankingMaker(); - -const connector = new Connector(); - -spheroWS.spheroServer.events.on("addClient", (key, client) => { - controllerModel.add(key, client); - client.on("arriveCustomMessage", (name, data, mesID) => { - // Nameが同じなら、clientKeyが別でもHPなどが引き継がれる、と実装するため、 - // requestNameとuseDefinedNameを分けている。 - // requestName ・・ 新しい名前を使う。もしその名前が既に使われていたらrejectする。 - // useDefinedName ・・既存の名前を使う。もしその名前がなければrejectする。 - if (name === "requestName") { - if (controllerModel.has(data)) { - client.sendCustomMessage("rejectName", null); - } else { - controllerModel.setName(key, data); - client.sendCustomMessage("acceptName", data); - } - } else if (name === "useDefinedName") { - if (!controllerModel.has(data)) { - client.sendCustomMessage("rejectName", null); - } else { - controllerModel.setName(key, data); - client.sendCustomMessage("acceptName", data); - } - } - }); -}); - -spheroWS.spheroServer.events.on("removeClient", key => { - if (controllerModel.hasInUnnamedClients(key)) { - controllerModel.removeFromUnnamedClients(key); - } else { - const name = controllerModel.toName(key); - controllerModel.removeClient(name); - virtualSphero.removeSphero(name); - } -}); - -controllerModel.on("named", (key, name, isNewName) => { - const controller = controllerModel.get(name); - const client = controller.client; - - client.sendCustomMessage("gameState", gameState); - client.sendCustomMessage("rankingState", rankingState); - client.sendCustomMessage("availableCommandsCount", availableCommandsCount); - client.sendCustomMessage("clientKey", key); - - if (isNewName) { - controller.commandRunner.on("command", (commandName, args) => { - if (controller.linkedOrb !== null) { - if (!controller.linkedOrb.hasCommand(commandName)) { - throw new Error(`command : ${commandName} is not valid.`); - } - controller.linkedOrb.command(commandName, args); - } - virtualSphero.command(name, commandName, args); - }); - controller.on("hp", hp => { - dashboard.updateHp(name, hp); - }); - } - virtualSphero.addSphero(name); - - client.on("arriveCustomMessage", (messageName, data, mesID) => { - if (messageName === "commands") { - controller.commandRunner.setCommands(data); - } - }); -}); - -const orbs = spheroWS.spheroServer.getOrb(); -Object.keys(orbs).forEach(orbName => { - dashboard.addOrb(orbName, orbs[orbName].port); -}); - -spheroWS.spheroServer.events.on("addOrb", (name, orb) => { - if (!isTestMode) { - const rawOrb = orb.instance; - rawOrb.color("white"); - rawOrb.detectCollisions(); - rawOrb.on("collision", () => { - Object.keys(controllerModel.controllers).forEach(controllerName => { - const controller = controllerModel.get(controllerName); - if (gameState === "active" && !controller.isOni && - controller.client !== null && - orb.linkedClients.indexOf(controller.client.key) !== -1) { - controller.setHp(controller.hp - 10); - eventPublisher.emit("updatedHp", controller); - } - }) - }); - } - dashboard.addOrb(name, orb.port); - dashboard.updateUnlinkedOrbs(spheroWS.spheroServer.getUnlinkedOrbs()); -}); -spheroWS.spheroServer.events.on("removeOrb", name => { - dashboard.removeOrb(name); -}); - -dashboard.on("gameState", state => { - gameState = state; - Object.keys(controllerModel.controllers).filter(key => { - return controllerModel.get(key).client !== null; - }).forEach(key => { - controllerModel.get(key).client.sendCustomMessage("gameState", gameState); - }); -}); -dashboard.on("rankingState", state => { - const controllerKeys = Object.keys(controllerModel.controllers).filter(key => { - return controllerModel.get(key).client !== null; - }); - rankingState = state; - controllerKeys.forEach(key => { - controllerModel.get(key).client.sendCustomMessage("rankingState", state); - }); - if (state === "show") { - const ranking = rankingMaker.make(controllerModel.controllers); - controllerKeys.forEach(key => { - controllerModel.get(key).client.sendCustomMessage("ranking", ranking); - }); - } -}); - -dashboard.on("availableCommandsCount", count => { - availableCommandsCount = count; - Object.keys(controllerModel.controllers).forEach(name => { - const client = controllerModel.get(name).client; - if (client !== null) { - client.sendCustomMessage("availableCommandsCount", availableCommandsCount); - } - }); -}); -dashboard.on("updateLink", (controllerName, orbName) => { - controllerModel.get(controllerName).setLink( - orbName !== null ? spheroWS.spheroServer.getOrb(orbName) : null); - dashboard.updateUnlinkedOrbs(spheroWS.spheroServer.getUnlinkedOrbs()); - eventPublisher.emit("updateLink", controllerName, orbName); -}); -dashboard.on("addOrb", (name, port) => { - const rawOrb = spheroWS.spheroServer.makeRawOrb(name, port); - if (!isTestMode) { - if (!connector.isConnecting(port)) { - error121Count = 0; - connector.connect(port, rawOrb.instance).then(() => { - dashboard.log("connected orb.", "success"); - rawOrb.instance.configureCollisions({ - meth: 0x01, - xt: 0x7A, - xs: 0xFF, - yt: 0x7A, - ys: 0xFF, - dead: 100 - }, () => { - dashboard.log("configured orb.", "success"); - spheroWS.spheroServer.addOrb(rawOrb); - rawOrb.instance.streamOdometer(); - rawOrb.instance.on("odometer", data => { - const time = new Date(); - dashboard.streamed( - name, - ("0" + time.getHours()).slice(-2) + ":" + - ("0" + time.getMinutes()).slice(-2) + ":" + - ("0" + time.getSeconds()).slice(-2)); - }); - }); - }); - } - } else { - spheroWS.spheroServer.addOrb(rawOrb); - } -}); -dashboard.on("removeOrb", name => { - spheroWS.spheroServer.removeOrb(name); -}); -dashboard.on("oni", (name, enable) => { - controllerModel.get(name).setIsOni(enable); -}); -dashboard.on("checkBattery", () => { - const orbs = spheroWS.spheroServer.getOrb(); - Object.keys(orbs).forEach(orbName => { - orbs[orbName].instance.getPowerState((error, data) => { - if (error) { - throw new Error(error); - } else { - dashboard.updateBattery(orbName, data.batteryState); - } - }); - }); -}); -dashboard.on("resetHp", name => { - const controller = controllerModel.get(name); - controller.setHp(100); - eventPublisher.emit("updatedHp", controller); -}); -dashboard.on("pingAll", () => { - const orbs = spheroWS.spheroServer.getOrb(); - Object.keys(orbs).forEach(orbName => { - orbs[orbName].instance.ping((err, data) => { - if (!err) { - dashboard.updatePingState(orbName); - } else { - dashboard.log("Ping error: \n" + err.toString(), "error"); - } - }); - }); -}); -dashboard.on("reconnect", name => { - if (!isTestMode) { - const orb = spheroWS.spheroServer.getOrb(name); - if (orb !== null) { - orb.instance.disconnect(() => { - dashboard.log("(reconnect) disconnected.", "success"); - if (!connector.isConnecting(orb.port)) { - error121Count = 0; - dashboard.log("(reconnect) wait 2 seconds.", "log"); - setTimeout(() => { - dashboard.log("(reconnect) connecting...", "log"); - connector.connect(orb.port, orb.instance).then(() => { - dashboard.log("(reconnect) connected", "success"); - dashboard.successReconnect(name); - }); - }, 2000); - } - }); - } - } -}); -dashboard.on("color", (name, color) => { - controllerModel.get(name).setColor(color); - eventPublisher.emit("color", name, color); -}); - +models.orbModel.setSpheroWS(spheroWS); + +const connector = new Connector(models); + +new Dashboard(models, config.dashboardPort); +new VirtualSpheroManager(models, config.virtualSphero.wsPort); +new Scoreboard(models, config.scoreboardPort); +new SpheroServerManager(models, spheroWS); +new ControllerManager(models, config.defaultHp, config.damage); +new RankingMaker(models); +new OrbController(models, connector, spheroWS, config.defaultColor, config.collision); + +if (config.isUseNoble) { + // noble は、非対応環境だと、import した直後にエラーが発生してしまう + // そのため、require を使ってこのタイミングで読み込むしかない + // SystemJS とか使うともっとかっこいいかも + const noble = require("noble"); + new UUIDManager(models, noble); +} diff --git a/model/appModel.js b/model/appModel.js new file mode 100644 index 0000000..81781b6 --- /dev/null +++ b/model/appModel.js @@ -0,0 +1,55 @@ +import ComponentBase from "../componentBase"; + +export default class AppModel extends ComponentBase { + constructor() { + super(); + + this.gameState = "inactive"; + this.rankingState = "hide"; + this.availableCommandsCount = 1; + this.isTestMode = false; + this.error121Count = 0; + this.ranking = null; + this.nameAndUUIDs = {}; + + this.subscribeModel("gameState", this.updateGameState); + this.subscribeModel("rankingState", this.updateRankingState); + this.subscribeModel("availableCommandsCount", this.updateAvailableCommandsCount); + this.subscribeModel("ranking", this.updateRanking); + this.subscribeModel("setNameOfUUID", this.setNameOfUUID); + this.subscribeModel("incrementError121Count", this.incrementError121Count); + this.subscribeModel("resetError121Count", this.resetError121Count); + } + updateGameState(state) { + this.gameState = state; + } + updateRankingState(state) { + this.rankingState = state; + } + updateAvailableCommandsCount(count) { + this.availableCommandsCount = count; + } + resetError121Count() { + this.error121Count = 0; + } + incrementError121Count() { + this.error121Count++; + } + updateRanking(ranking) { + this.ranking = ranking; + } + setNameOfUUID(name, uuid) { + console.log(`name: ${name}, uuid: ${uuid}`); + this.nameAndUUIDs[name] = uuid; + } + containsUUID(name) { + return typeof this.nameAndUUIDs[name] !== "undefined"; + } + getUUID(name) { + if (!this.containsUUID(name)) { + throw new Error("The name's uuid was not found. name: " + name); + } + return this.nameAndUUIDs[name]; + } +} + diff --git a/model/controllerModel.js b/model/controllerModel.js new file mode 100644 index 0000000..eefd9f9 --- /dev/null +++ b/model/controllerModel.js @@ -0,0 +1,85 @@ +import Controller from "../controller"; +import CommandRunner from "../commandRunner"; +import publisher from "../publisher"; +import ComponentBase from "../componentBase"; + +export default class ControllerModel extends ComponentBase { + constructor() { + super(); + + // 最初、controllerはunnamedControllerに追加される。 + // name がわかると、それが controllers に移行される。 + // こうすることで、clientが異なっても、nameが同じ場合、HPなどを共有できるようになる。 + + // { key: Client, ... } + this.unknownClients = {}; + // { name: Controller, ... } + this.controllers = {}; + } + addUnknownClient(key, client) { + this.unknownClients[key] = client; + this.publish("addedUnknown", key, client); + } + addClient(name, key) { + this.controllers[name].setClient(this.unknownClients[key]); + this.removeUnknownClient(key); + this.publish("addedClient", name); + } + addController(name) { + this.controllers[name] = new Controller(name, new CommandRunner(name)); + this.publish("addedController", name); + } + setName(key, name) { + if (!this.unknownClients[key]) { + throw new Error("setNameしようとしたところ、keyに対するclientが見つかりませんでした。 key: " + key); + } + const isNewName = !this.controllers[name]; + if (isNewName) { + this.addController(name); + } else if (this.controllers[name].client) { + throw new Error("setNameしようとしましたが、既にclientが存在します。 name: " + name); + } + this.addClient(name, key); + } + removeUnknownClient(key) { + if (this.hasInUnknownClients(key)) { + delete this.unknownClients[key]; + this.publish("removedUnknown", key); + } + } + removeClient(name) { + if (this.has(name)) { + this.controllers[name].setClient(null); + this.publish("removedClient", name); + } + } + hasInUnknownClients(key) { + return typeof this.unknownClients[key] !== "undefined"; + } + has(name) { + return typeof this.controllers[name] !== "undefined"; + } + get(name) { + return this.controllers[name]; + } + getAllStates() { + const result = {}; + for (let name in this.controllers) { + result[name] = this.controllers[name].getStates(); + } + return result; + } + getUnnamedKeys() { + return Object.keys(this.unknownClients); + } + toName(key) { + for (let name in this.controllers) { + if (this.controllers[name].client && + this.controllers[name].client.key === key) { + return name; + } + } + return null; + } +} + diff --git a/model/orbModel.js b/model/orbModel.js new file mode 100644 index 0000000..f69d04f --- /dev/null +++ b/model/orbModel.js @@ -0,0 +1,76 @@ +import ComponentBase from "../componentBase"; + +export default class OrbModel extends ComponentBase { + constructor() { + super(); + + this.spheroWS = null; + this.orbs = {}; + } + setSpheroWS(spheroWS) { + this.spheroWS = spheroWS; + } + add(orbName, orb) { + this.orbs[orbName] = orb; + } + setBattery(orbName, battery) { + if (this.has(orbName)) { + this.orbs[orbName].battery = battery; + } + } + setLink(orbName, link) { + if (this.has(orbName)) { + this.orbs[orbName].link = link; + } + } + setPingState(orbName, state) { + if (this.has(orbName)) { + this.orbs[orbName].pingState = state; + } + } + remove(orbName) { + if (this.has(orbName)) { + delete this.orbs[orbName]; + } + } + getNames() { + return Object.keys(this.orbs); + } + get(orbName) { + return this.orbs[orbName]; + } + getDiff(comparisonOrbMap) { + const getAddedItem = (before, after) => { + return after.filter(item => before.indexOf(item) === -1); + }; + const orbNames = this.getNames(); + const added = getAddedItem(orbNames, comparisonOrbMap.getNames()); + const removed = getAddedItem(comparisonOrbMap.getNames(), orbNames); + return { + added, + removed, + noChanged: orbNames.filter(item => comparisonOrbMap.has(item)) + }; + } + has(orbName) { + return typeof this.orbs[orbName] !== "undefined"; + } + toArray() { + return Object.keys(this.orbs).map(orbName => this.orbs[orbName]); + } + + getUnlinkedOrbs() { + if (!this.spheroWS) { + throw new Error("spheroWS is null."); + } + return this.spheroWS.spheroServer.getUnlinkedOrbs(); + } + + getOrbFromSpheroWS(orbName) { + if (!this.spheroWS) { + throw new Error("spheroWS is null."); + } + return this.spheroWS.spheroServer.getOrb(orbName); + } +} + diff --git a/orbController.js b/orbController.js new file mode 100644 index 0000000..97202bd --- /dev/null +++ b/orbController.js @@ -0,0 +1,148 @@ +import ComponentBase from "./componentBase"; + +export default class OrbController extends ComponentBase { + constructor(models, connector, spheroWS, defaultColor, collisionConfig) { + super(models); + + this.spheroWS = spheroWS; + this.connector = connector; + this.defaultColor = defaultColor; + this.collisionConfig = collisionConfig; + + this.subscribeModel("addedOrb", this.addOrbToModel); + this.subscribeModel("removedOrb", this.removeOrbFromModel); + this.subscribeModel("pingAll", this.setPingStateAll); + this.subscribeModel("updateBattery", this.updateBattery); + this.subscribeModel("replyPing", this.updatePingState); + this.subscribeModel("checkBattery", this.checkBattery); + this.subscribeModel("addOrb", this.addOrb); + this.subscribeModel("removeOrb", this.removeOrb); + this.subscribeModel("pingAll", this.pingAll); + } + + addOrbToModel(name, orb) { + if (this.orbModel.has(name)) { + throw new Error(`追加しようとしたOrbは既に存在します。 : ${name}`); + } + this.orbModel.add(name, { + orbName: name, + port: orb.port, + battery: null, + link: "unlinked", + pingState: "unchecked" + }); + this.initializeOrb(orb); + } + + removeOrbFromModel(name) { + if (!this.orbModel.has(name)) { + throw new Error(`削除しようとしたOrbは存在しません。 : ${name}`); + } + this.orbModel.remove(name); + } + + setPingStateAll() { + for (let orbName in this.orbModel.orbs) { + this.orbModel.setPingState(orbName, "no reply"); + } + } + + updateBattery(name, batteryState) { + if (!this.orbModel.has(name)) { + throw new Error("updateBattery しようとしましたが、orb が見つかりませんでした。 : " + name); + } + this.orbModel.setBattery(name, batteryState); + } + + updatePingState(name) { + if (!this.orbModel.has(name)) { + throw new Error("updatePingState しようとしましたが、orb が見つかりませんでした。 : " + name); + } + this.orbModel.setPingState(name, "reply"); + } + + checkBattery() { + const orbs = this.spheroWS.spheroServer.getOrb(); + for (let orbName in orbs) { + if (!this.appModel.isTestMode) { + orbs[orbName].instance.getPowerState((error, data) => { + if (error) { + throw new Error(error); + } else { + this.publish("updateBattery", orbName, data.batteryState); + } + }); + } else { + this.publish("updateBattery", orbName, "test-battery"); + } + } + } + + pingAll() { + const orbs = this.spheroWS.spheroServer.getOrb(); + for (let orbName in orbs) { + orbs[orbName].instance.ping((err, data) => { + if (!err) { + this.publish("replyPing", orbName); + } else { + this.publish("log", "Ping error: \n" + err.toString(), "error"); + } + }); + } + } + + initializeOrb(orb) { + if (!this.appModel.isTestMode) { + const rawOrb = orb.instance; + rawOrb.color(this.defaultColor); + rawOrb.on("collision", () => { + this.publishCollision(orb); + }); + } + } + + addOrb(name, port) { + if (this.appModel.containsUUID(name)) { + port = this.appModel.getUUID(name); + console.log("changed!", port); + } + const rawOrb = this.spheroWS.spheroServer.makeRawOrb(name, port); + if (!this.appModel.isTestMode) { + if (!this.connector.isConnecting(port)) { + this.appModel.resetError121Count(); + this.connector.connect(port, rawOrb.instance).then(() => { + rawOrb.instance.setInactivityTimeout(9999999, function(err, data) { + if (err) { + throw new Error(err); + } + console.log("data: " + data); + }); + + this.publish("log", "connected orb.", "success"); + + rawOrb.instance.configureCollisions(this.collisionConfig, () => { + this.publish("log", "configured orb.", "success"); + this.spheroWS.spheroServer.addOrb(rawOrb); + rawOrb.instance.streamOdometer(); + rawOrb.instance.on("odometer", data => { + this.publish("streamed", name, new Date()); + }); + }); + }); + } else { + console.warn("Tryed to connect but a orb is connecting."); + } + } else { + this.spheroWS.spheroServer.addOrb(rawOrb); + } + } + + removeOrb(name) { + console.log("removing... : " + name); + this.spheroWS.spheroServer.removeOrb(name); + } + + publishCollision(orb) { + this.publish("collision", orb); + } +} diff --git a/package.json b/package.json index 501ef99..37a71f5 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "build": "webpack --progress --colors", "build:watch": "webpack --progress --colors --watch", "start": "babel-node main.js", - "test": "babel-node main.js --test", + "testrun": "babel-node main.js --test", + "test": "mocha test/**/*.js --compilers js:babel-register", "debug": "babel-node --debug-brk main.js" }, "babel": { @@ -31,10 +32,12 @@ "babel-core": "^6.10.4", "babel-loader": "^6.2.4", "babel-preset-es2015": "^6.9.0", + "babel-register": "^6.22.0", "css-loader": "^0.23.1", "gulp": "^3.9.1", "minimist": "^1.2.0", - "noble": "^1.7.0", + "mocha": "^3.2.0", + "sinon": "^1.17.7", "style-loader": "^0.13.1", "webpack": "^1.13.1" }, @@ -44,7 +47,10 @@ "knockout": "^3.4.0", "socket.io": "^1.4.8", "sphero": "github:orbotix/sphero.js#master", - "sphero-websocket": "^0.5.10", "sphero-ws-virtual-plugin": "1.1.2" + }, + "optionalDependencies": { + "noble": "^1.7.0", + "sphero-websocket": "^0.5.10" } } diff --git a/publisher.js b/publisher.js index b00e517..9b608b0 100644 --- a/publisher.js +++ b/publisher.js @@ -1,2 +1,36 @@ -import { EventEmitter } from "events"; -export default new EventEmitter(); +export class EventPublisher { + constructor() { + this.observeFunctions = {}; + this.observeFunctionsInModel = {}; + } + + subscribe(subjectName, observeFunction) { + if (!this.observeFunctions[subjectName]) { + this.observeFunctions[subjectName] = []; + } + this.observeFunctions[subjectName].push(observeFunction); + } + + subscribeModel(subjectName, observeFunction) { + if (!this.observeFunctionsInModel[subjectName]) { + this.observeFunctionsInModel[subjectName] = []; + } + this.observeFunctionsInModel[subjectName].push(observeFunction); + } + + publish(author, subjectName, ...data) { + (this.observeFunctionsInModel[subjectName] || []) + .concat(this.observeFunctions[subjectName] || []) + .forEach(observeFunction => { + + observeFunction(author, ...data); + }); + } + + clearObserveFunctions() { + this.observeFunctions = {}; + this.observeFunctionsInModel = {}; + } +} + +export default new EventPublisher(); diff --git a/rankingMaker.js b/rankingMaker.js index 77696f6..62942ec 100644 --- a/rankingMaker.js +++ b/rankingMaker.js @@ -1,13 +1,29 @@ -export default class RankingMaker { - constructor() { +import ComponentBase from "./componentBase"; + +export default class RankingMaker extends ComponentBase { + constructor(models) { + super(models); + + this.subscribe("rankingState", this.updateRankingState); + this.subscribe("updatedHp", this.make); + this.subscribe("updatedLink", this.make); + this.subscribe("updatedColor", this.make); } - make(controllers) { + + updateRankingState(state) { + if (state === "show") { + this.make(); + } + } + + make() { + const controllers = this.controllerModel.controllers; const controllerNames = Object.keys(controllers); // indexが順位となっている // [ { hp: 100, name: "xxx" }, { hp: 80, name: "xxx" }, ...] const ranking = controllerNames.filter(name => { // まず鬼であるものを除外する - return controllers[name].linkedOrb !== null && !controllers[name].isOni; + return controllers[name].linkedOrb && !controllers[name].isOni; }).sort((a, b) => { // HPに基づき、降順にソートする return controllers[b].hp - controllers[a].hp; @@ -22,10 +38,11 @@ export default class RankingMaker { // { name: getStates(), ... } const onis = {}; controllerNames.filter(name => { - return controllers[name].linkedOrb !== null && controllers[name].isOni + return controllers[name].linkedOrb && controllers[name].isOni; }).forEach(name => { onis[name] = controllers[name].getStates(); }); - return { ranking, onis }; + + this.publish("ranking", { ranking, onis }); } } diff --git a/readme.md b/readme.md index 1514834..ba7be17 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,7 @@ # onigo-server +[![Build Status](https://api.travis-ci.org/shundroid/onigo-server.svg?branch=test)](https://travis-ci.org/shundroid/onigo-server) + ![](https://docs.google.com/drawings/d/11UkOxTHAYCFONLhi49WRn1hCaDZz25plo-yaG18Q2cc/pub?w=854&h=579) ## About diff --git a/scoreboard.js b/scoreboard.js index 9d690c1..be37466 100644 --- a/scoreboard.js +++ b/scoreboard.js @@ -1,56 +1,39 @@ import express from "express"; import io from "socket.io"; -import controllerModel from "./controllerModel"; -import RankingMaker from "./rankingMaker"; -import eventPublisher from "./publisher"; - -let scoreboardInstance = null; - -function Scoreboard(port) { - if (scoreboardInstance !== null) { - return scoreboardInstance; +import ComponentBase from "./componentBase"; +import { Server as createServer } from "http"; + +export default class Scoreboard extends ComponentBase { + constructor(models, port) { + super(models); + + this.app = express(); + this.server = createServer(this.app); + this.io = io(this.server); + this.io.origins(`localhost:${port}`); + + this.app.use(express.static("scoreboard")); + this.server.listen(port, () => { + console.log(`score is listening on port ${port}`); + }); + + this.currentRanking = null; + + this.sockets = []; + this.io.on("connection", socket => { + console.log("a scoreboard connected."); + this.sockets.push(socket); + if (this.appModel.ranking) { + socket.emit("data", this.appModel.ranking); + } + }); + + this.subscribe("ranking", this.updateRanking); } - scoreboardInstance = this; - - this.app = express(); - this.server = require("http").Server(this.app); - this.io = require("socket.io")(this.server); - this.io.origins(`localhost:${port}`); - - this.app.use(express.static("scoreboard")); - this.server.listen(port, () => { - console.log(`score is listening on port ${port}`); - }); - - this.currentRanking = null; - this.rankingMaker = new RankingMaker(); - - this.sockets = []; - this.io.on("connection", socket => { - console.log("a scoreboard connected."); - this.sockets.push(socket); - if (this.currentRanking !== null) { - socket.emit("data", this.currentRanking); - } - }); - - eventPublisher.on("updatedHp", () => { - this.updateRanking(); - }); - eventPublisher.on("updateLink", () => { - this.updateRanking(); - }); - eventPublisher.on("color", () => { - this.updateRanking(); - }); + updateRanking(ranking) { + this.sockets.forEach(socket => { + socket.emit("data", ranking); + }); + } } - -Scoreboard.prototype.updateRanking = function() { - this.currentRanking = this.rankingMaker.make(controllerModel.controllers); - this.sockets.forEach(socket => { - socket.emit("data", this.currentRanking); - }); -}; - -module.exports = Scoreboard; diff --git a/spheroServerManager.js b/spheroServerManager.js new file mode 100644 index 0000000..d3ba429 --- /dev/null +++ b/spheroServerManager.js @@ -0,0 +1,36 @@ +import ComponentBase from "./componentBase"; + +export default class SpheroServerManager extends ComponentBase { + constructor(models, spheroWS) { + super(models); + + this.spheroWS = spheroWS; + this.spheroServer = this.spheroWS.spheroServer; + + this.spheroServer.events.on("addClient", this.publishAddClient.bind(this)); + this.spheroServer.events.on("removeClient", this.publishRemoveClient.bind(this)); + this.spheroServer.events.on("addOrb", this.publishAddedOrb.bind(this)); + this.spheroServer.events.on("removeOrb", this.publishRemovedOrb.bind(this)); + } + + publishAddClient(key, client) { + this.publish("addClient", key, client); + } + + publishRemoveClient(key) { + this.publish("removeClient", key); + } + + publishAddedOrb(name, orb) { + this.publish("addedOrb", name, orb); + } + + publishRemovedOrb(name) { + this.publish("removedOrb", name); + } + + publishCollision(orb) { + this.publish("collision", orb); + } + +} diff --git a/test/commandRunner.test.js b/test/commandRunner.test.js new file mode 100644 index 0000000..872081c --- /dev/null +++ b/test/commandRunner.test.js @@ -0,0 +1,26 @@ +import assert from "assert"; +import CommandRunner from "../commandRunner"; + +describe("CommandRunner", () => { + const commandRunner = new CommandRunner(); + const timeoutDelay = 1000; + describe("#stopLoop()", () => { + const timeoutId = setTimeout(() => {}, timeoutDelay); + commandRunner.loopTimeoutId = timeoutId; + it("should set loopTimeoutId to null", () => { + assert.equal(commandRunner.loopTimeoutId, timeoutId); + commandRunner.stopLoop(); + assert.equal(commandRunner.loopTimeoutId, null); + }); + }); + describe("clearCustomTimeoutIds()", () => { + const timeoutId = setTimeout(() => {}, timeoutDelay); + commandRunner.customTimeoutIds = { test1: timeoutId }; + it("should delete timeoutId from customTimeoutIds", () => { + assert(commandRunner.customTimeoutIds["test1"]); + assert.equal(commandRunner.customTimeoutIds["test1"], timeoutId); + commandRunner.clearCustomTimeoutIds(); + assert(!commandRunner.customTimeoutIds["test1"]); + }); + }); +}); diff --git a/test/componentBase.test.js b/test/componentBase.test.js new file mode 100644 index 0000000..aefad75 --- /dev/null +++ b/test/componentBase.test.js @@ -0,0 +1,55 @@ +import assert from "assert"; +import publisher from "../publisher"; +import ComponentBase from "../componentBase"; + +describe("ComponentBase", () => { + publisher.clearObserveFunctions(); + describe("#constructor()", () => { + const testModel = "test-model"; + const component = new ComponentBase({ testModel }); + + it("should set models", () => { + assert(component.testModel); + assert.equal(component.testModel, testModel); + }); + }); + describe("#publish()", () => { + const component = new ComponentBase(); + publisher.subscribe("test1", (author, data) => { + it("should publish author", () => { + assert(author === component); + }); + it("should publish correct data", () => { + assert(data === "test-data"); + }); + }); + component.publish("test1", "test-data"); + }); + describe("#subscribe()", () => { + const component = new ComponentBase(); + component.subscribe("test2", data => { + it("should be called", () => { + assert(data === "test-data-2"); + }); + }); + publisher.publish(this, "test2", "test-data-2"); + + let isCalled = false; + component.subscribe("test3", data => { + isCalled = data === "test-data-3"; + }); + publisher.publish(component, "test3", "test-data-3"); + it("should not call function when author is same", () => { + assert(!isCalled); + }); + }); + describe("#subscribeModel()", () => { + const component = new ComponentBase(); + component.subscribeModel("test4", data => { + it("should be called", () => { + assert(data === "test-data-4"); + }); + }); + publisher.publish(this, "test4", "test-data-4"); + }); +}); diff --git a/test/controllerManager.test.js b/test/controllerManager.test.js new file mode 100644 index 0000000..5cdd05d --- /dev/null +++ b/test/controllerManager.test.js @@ -0,0 +1,252 @@ +import assert from "assert"; +import ControllerManager from "../controllerManager"; +import ControllerModel from "../model/controllerModel"; +import AppModel from "../model/appModel"; +import publisher from "../publisher"; +import sinon from "sinon"; +import config from "../config"; + +describe("ControllerManager", () => { + publisher.clearObserveFunctions(); + + const appModel = new AppModel(); + const controllerModel = new ControllerModel(); + + const testKey = "key-test"; + const testName = "name-test"; + + controllerModel.addUnknownClient(testKey, { + sendCustomMessage() {}, + key: testKey, + on() {} + }); + controllerModel.setName(testKey, testName); + controllerModel.get(testName).linkedOrb = { + command() {}, + hasCommand() { return true; } + }; + + const controllerManager = new ControllerManager({ appModel, controllerModel }, config.defaultHp, config.damage); + describe("#changeIsOni", () => { + const changeIsOniSpy = sinon.spy(controllerManager, "changeIsOni"); + const setIsOniSpy = sinon.spy(controllerModel.get(testName), "setIsOni"); + controllerManager.changeIsOni(testName, true); + + it("should be called", () => { + assert(changeIsOniSpy.withArgs(testName, true).called); + }); + + it("should call setIsOni of controller", () => { + assert(setIsOniSpy.withArgs(true).called); + }); + + changeIsOniSpy.restore(); + setIsOniSpy.restore(); + }); + + describe("#resetHp", () => { + const resetHpSpy = sinon.spy(controllerManager, "resetHp"); + const setHpSpy = sinon.spy(controllerModel.get(testName), "setHp"); + controllerManager.resetHp(testName); + + it("should be called", () => { + assert(resetHpSpy.withArgs(testName).called); + }); + + it("should call setHp of controller", () => { + assert(setHpSpy.withArgs(config.defaultHp).called); + }); + + resetHpSpy.restore(); + setHpSpy.restore(); + }); + + describe("#changeColor", () => { + const changeColorSpy = sinon.spy(controllerManager, "changeColor"); + const setColorSpy = sinon.spy(controllerModel.get(testName), "setColor"); + controllerManager.changeColor(testName, "red"); + + it("should be called", () => { + assert(changeColorSpy.withArgs(testName, "red").called); + }); + + it("should call setColor of controller", () => { + assert(setColorSpy.withArgs("red").called); + }); + + changeColorSpy.restore(); + setColorSpy.restore(); + }); + + describe("#updateGameState", () => { + const updateGameStateSpy = sinon.spy(controllerManager, "updateGameState"); + const sendCustomMessageSpy = sinon.spy(controllerModel.get(testName).client, "sendCustomMessage"); + controllerManager.updateGameState("active"); + + it("should be called", () => { + assert(updateGameStateSpy.withArgs("active").called); + }); + + it("should send gameState to client", () => { + assert(sendCustomMessageSpy.withArgs("gameState", "active").called); + }); + + updateGameStateSpy.restore(); + sendCustomMessageSpy.restore(); + }); + + describe("#updateRankingState", () => { + const updateGameStateSpy = sinon.spy(controllerManager, "updateRankingState"); + const sendCustomMessageSpy = sinon.spy(controllerModel.get(testName).client, "sendCustomMessage"); + controllerManager.updateRankingState("show"); + + it("should be called", () => { + assert(updateGameStateSpy.withArgs("show").called); + }); + + it("should send gameState to client", () => { + assert(sendCustomMessageSpy.withArgs("rankingState", "show").called); + }); + + updateGameStateSpy.restore(); + sendCustomMessageSpy.restore(); + }); + + describe("#updateRanking", () => { + const updateGameStateSpy = sinon.spy(controllerManager, "updateRanking"); + const sendCustomMessageSpy = sinon.spy(controllerModel.get(testName).client, "sendCustomMessage"); + controllerManager.updateRanking("test-ranking"); + + it("should be called", () => { + assert(updateGameStateSpy.withArgs("test-ranking").called); + }); + + it("should send gameState to client", () => { + assert(sendCustomMessageSpy.withArgs("ranking", "test-ranking").called); + }); + + updateGameStateSpy.restore(); + sendCustomMessageSpy.restore(); + }); + + describe("#damage", () => { + const controller = controllerModel.get(testName); + controller.hp = 100; + controller.isOni = false; + appModel.gameState = "active"; + + const damageSpy = sinon.spy(controllerManager, "damage"); + const setHpSpy = sinon.spy(controller, "setHp"); + const testOrb = { + linkedClients: [testKey] + }; + controllerManager.damage(testOrb); + + it("should be called", () => { + assert(damageSpy.withArgs(testOrb).called); + }); + + it("should call setHp", () => { + assert(setHpSpy.withArgs(config.defaultHp - config.damage).called); + }); + + damageSpy.restore(); + setHpSpy.restore(); + }); + + describe("#updateAvailableCommandsCount", () => { + const controller = controllerModel.get(testName); + const updateSpy = sinon.spy(controllerManager, "updateAvailableCommandsCount"); + const sendCustomMessageSpy = sinon.spy(controller.client, "sendCustomMessage"); + + controllerManager.updateAvailableCommandsCount(4); + + it("should be called", () => { + assert(updateSpy.withArgs(4).called); + }); + + it("should call sendCustomMessage", () => { + assert(sendCustomMessageSpy.withArgs("availableCommandsCount", 4).called); + }); + + updateSpy.restore(); + sendCustomMessageSpy.restore(); + }); + + describe("#initializeClient", () => { + const controller = controllerModel.get(testName); + const initializeClientSpy = sinon.spy(controllerManager, "initializeClient"); + const sendCustomMessageSpy = sinon.spy(controller.client, "sendCustomMessage"); + + controllerManager.initializeClient(testName); + + it("should be called", () => { + assert(initializeClientSpy.withArgs(testName)); + }); + + it("should send default datas", () => { + assert(sendCustomMessageSpy.withArgs("gameState", appModel.gameState).called); + assert(sendCustomMessageSpy.withArgs("rankingState", appModel.rankingState).called); + assert(sendCustomMessageSpy.withArgs("availableCommandsCount", appModel.availableCommandsCount).called); + assert(sendCustomMessageSpy.withArgs("clientKey", testKey).called); + }); + + initializeClientSpy.restore(); + sendCustomMessageSpy.restore(); + }); + + describe("#initializeController", () => { + it("should publish hp when controller emitted hp", () => { + const controller = controllerModel.get(testName); + controllerManager.initializeController(testName); + const testHp = 80; + + const spy = sinon.spy(); + publisher.subscribe("hp", spy); + controller.emit("hp", testHp); + assert(spy.withArgs(controllerManager, testName, testHp).calledOnce); + }); + }); + + describe("#setCommands", () => { + const controller = controllerModel.get(testName); + const setCommandsSpy = sinon.spy(controllerManager, "setCommands"); + const setCommandsInCommandRunnerSpy = sinon.spy(controller.commandRunner, "setCommands"); + + const commands = [ + { commandName: "roll", args: [80, 80] } + ]; + + controllerManager.setCommands(testName, commands); + + it("should be called", () => { + assert(setCommandsSpy.withArgs(testName).called); + }); + + it("should call setCommands in commandRunner", () => { + assert(setCommandsInCommandRunnerSpy.withArgs("commands")); + }); + + setCommandsSpy.restore(); + setCommandsInCommandRunnerSpy.restore(); + }); + + describe("#command", () => { + const controller = controllerModel.get(testName); + const commandSpy = sinon.spy(controllerManager, "command"); + const commandInOrbSpy = sinon.spy(controller.linkedOrb, "command"); + + controllerManager.command(testName, "roll", "args"); + + it("should be called", () => { + assert(commandSpy.withArgs(testName, "roll", "args").called); + }); + + it("should call setCommands in commandRunner", () => { + assert(commandInOrbSpy.withArgs("roll", "args")); + }); + + commandSpy.restore(); + commandInOrbSpy.restore(); + }); +}); diff --git a/test/dashboard.test.js b/test/dashboard.test.js new file mode 100644 index 0000000..afe8260 --- /dev/null +++ b/test/dashboard.test.js @@ -0,0 +1,96 @@ +import assert from "assert"; +import Dashboard from "../dashboard"; +import publisher from "../publisher"; +import AppModel from "../model/appModel"; +import ControllerModel from "../model/controllerModel"; +import OrbModel from "../model/orbModel"; +import sinon from "sinon"; +import { EventEmitter } from "events"; + +describe("Dashboard", function() { + let controllerModel; + let dashboard; + + before(done => { + controllerModel = new ControllerModel(); + dashboard = new Dashboard({ + appModel: new AppModel(), + controllerModel, + orbModel: new OrbModel() + }, 8082); + dashboard.initializeConnection(new EventEmitter()); + done(); + }); + + beforeEach(done => { + publisher.clearObserveFunctions(); + done(); + }); + + afterEach(done => { + dashboard.server.close(() => { + done(); + }); + }); + + describe("#initializeConnection()", function() { + it("should register listener to the socket", () => { + const spy = sinon.spy(); + const activeState = "active"; + publisher.subscribe("gameState", spy); + dashboard.socket.emit("gameState", activeState); + assert(spy.withArgs(dashboard, activeState).calledOnce); + }); + it("should register publishPingAll", () => { + const spy = sinon.spy(dashboard, "publishPingAll"); + dashboard.socket.emit("pingAll"); + assert(spy.calledOnce); + }); + it("should register publishUpdateLink", () => { + const spy = sinon.spy(dashboard, "publishUpdateLink"); + const testControllerName = "controller-name"; + const testOrbName = "orb-name"; + dashboard.socket.emit("link", testControllerName, testOrbName); + assert(spy.withArgs(testControllerName, testOrbName).calledOnce); + }); + }); + + describe("#publishPingAll()", () => { + it("should publish pingAll to eventPublisher", () => { + const spy = sinon.spy(); + publisher.subscribe("pingAll", spy); + dashboard.publishPingAll(); + assert(spy.withArgs(dashboard).calledOnce); + }); + }); + + describe("#publishUpdateLink", () => { + it("should publish updateLink to eventPublisher", () => { + const spy = sinon.spy(); + const testControllerName = "controllerName"; + const testOrbName = "orbName"; + publisher.subscribe("updateLink", spy); + dashboard.publishUpdateLink(testControllerName, testOrbName); + assert(spy.withArgs(dashboard, testControllerName, testOrbName)); + }); + }); + + describe("#formatTime()", () => { + it("should format correctly", () => { + const date = new Date(); + const formattedTime = dashboard.formatTime(date); + const nums = formattedTime.split(":"); + + assert.equal(nums.length, 3); + + assert.equal(nums[0].length, 2); + assert.equal(parseInt(nums[0]), date.getHours()); + + assert.equal(nums[1].length, 2); + assert.equal(parseInt(nums[1]), date.getMinutes()); + + assert.equal(nums[2].length, 2); + assert.equal(parseInt(nums[2]), date.getSeconds()); + }); + }); +}); diff --git a/test/model/appModel.test.js b/test/model/appModel.test.js new file mode 100644 index 0000000..b6842ed --- /dev/null +++ b/test/model/appModel.test.js @@ -0,0 +1,79 @@ +import assert from "assert"; +import AppModel from "../../model/appModel"; + +describe("AppModel", () => { + let appModel; + beforeEach(done => { + appModel = new AppModel(); + done(); + }); + describe("#constructor()", () => { + it("should initialize gameState", () => { + assert.equal(appModel.gameState, "inactive"); + }); + it("should initialize rankingState", () => { + assert.equal(appModel.rankingState, "hide"); + }); + it("should initialize availableCommandsCount", () => { + assert.equal(appModel.availableCommandsCount, 1); + }); + it("should initialize nameAndUUIDs", () => { + assert.deepEqual(appModel.nameAndUUIDs, {}); + }); + }); + describe("#updateGameState()", () => { + it("should update gameState", () => { + appModel.updateGameState("active"); + assert.equal(appModel.gameState, "active"); + }); + }); + describe("#updateRankingState()", () => { + it("should update rankingState", () => { + appModel.updateRankingState("show"); + assert.equal(appModel.rankingState, "show"); + }); + }); + describe("#updateAvailableCommandsCount", () => { + it("should update availableCommandsCount", () => { + appModel.updateAvailableCommandsCount(6); + assert.equal(appModel.availableCommandsCount, 6); + }); + }); + describe("#updateRanking", () => { + it("should update ranking", () => { + const ranking = "test-ranking"; + appModel.updateRanking(ranking); + assert.equal(appModel.ranking, ranking); + }); + }); + describe("#setNameOfUUID", () => { + it("should set the name of uuid", () => { + const testName = "test-name"; + const testUUID = "test-uuid"; + appModel.setNameOfUUID(testName, testUUID); + assert(appModel.nameAndUUIDs[testName]); + assert.equal(appModel.nameAndUUIDs[testName], testUUID); + }); + }); + describe("#containsUUID", () => { + it("should return exist of name", () => { + const testName = "test-name"; + const testUUID = "test-uuid"; + appModel.nameAndUUIDs[testName] = testUUID; + assert(appModel.containsUUID(testName)); + }); + }); + describe("#getUUID", () => { + it("should return uuid of the name", () => { + const testName = "test-name"; + const testUUID = "test-uuid"; + appModel.nameAndUUIDs[testName] = testUUID; + assert(appModel.getUUID(testName), testUUID); + }); + it("should throw an error if the name isn't found", () => { + assert.throws(() => { + appModel.getUUID(testName); + }, Error); + }); + }); +}); diff --git a/test/model/controllerModel.test.js b/test/model/controllerModel.test.js new file mode 100644 index 0000000..880878c --- /dev/null +++ b/test/model/controllerModel.test.js @@ -0,0 +1,34 @@ +import assert from "assert"; +import publisher from "../../publisher"; +import ControllerModel from "../../model/controllerModel"; + +describe("ControllerModel", () => { + let controllerModel; + beforeEach(() => { + publisher.clearObserveFunctions(); + controllerModel = new ControllerModel(); + }); + describe("#constructor", () => { + it("should initialize controllers", () => { + assert.deepEqual(controllerModel.controllers, {}); + }); + }); + describe("#toName", () => { + it("should return name of the key", () => { + const testKey = "hoge"; + const testName = "testController3"; + controllerModel.controllers = { + testController1: { + client: { key: "invalid" } + }, + testController2: { + } + }; + controllerModel.controllers[testName] = { + client: { key: testKey } + }; + const name = controllerModel.toName(testKey); + assert.equal(name, testName); + }); + }); +}); diff --git a/test/orbController.test.js b/test/orbController.test.js new file mode 100644 index 0000000..5d111e3 --- /dev/null +++ b/test/orbController.test.js @@ -0,0 +1,58 @@ +import assert from "assert"; +import AppModel from "../model/appModel"; +import OrbModel from "../model/orbModel"; +import OrbController from "../orbController"; + +describe("OrbController", () => { + const appModel = new AppModel(); + appModel.isTestMode = true; + const orbModel = new OrbModel(); + const orbController = new OrbController({ appModel, orbModel }, {}, {}, "white", null); + const testName = "name-test"; + beforeEach(done => { + orbModel.orbs = {}; + orbController.addOrbToModel(testName, { + port: "test" + }); + done(); + }); + describe("#addOrbToModel()", () => { + it("should add onto orbModel", () => { + assert(orbModel.has(testName)); + const orb = orbModel.get(testName); + assert.equal(orb.orbName, testName); + assert.equal(orb.port, "test"); + assert.equal(orb.battery, null); + assert.equal(orb.link, "unlinked"); + assert.equal(orb.pingState, "unchecked"); + }); + }); + describe("#removeOrbFromModel()", () => { + it("should remove from orbModel", () => { + assert(orbModel.has(testName)); + orbController.removeOrbFromModel(testName); + assert(!orbModel.has(testName)); + }); + }); + describe("#setPingStateAll()", () => { + it("should update pingState", () => { + assert(orbModel.has(testName)); + orbController.setPingStateAll(); + assert.equal(orbModel.get(testName).pingState, "no reply"); + }); + }); + describe("#updateBattery()", () => { + it("should update batteryState", () => { + assert(orbModel.has(testName)); + orbController.updateBattery(testName, "batteryState-test"); + assert.equal(orbModel.get(testName).battery, "batteryState-test"); + }); + }); + describe("#updatePingState()", () => { + it("should update pingState", () => { + assert(orbModel.has(testName)); + orbController.updatePingState(testName); + assert.equal(orbModel.get(testName).pingState, "reply"); + }); + }); +}); diff --git a/test/publisher.test.js b/test/publisher.test.js new file mode 100644 index 0000000..c98002e --- /dev/null +++ b/test/publisher.test.js @@ -0,0 +1,74 @@ +import assert from "assert"; +import { EventPublisher } from "../publisher"; + +describe("Publisher", function() { + describe("#constructor()", function() { + const publisher = new EventPublisher(); + it("should initialize observeFunctions to {}", function() { + assert.deepEqual({}, publisher.observeFunctions); + }); + }); + describe("#subscribe()", function() { + const publisher = new EventPublisher(); + const observeFunction = function() {}; + publisher.subscribe("a", observeFunction); + it("should make subjectName into observeFunctions", function() { + assert.deepEqual(["a"], Object.keys(publisher.observeFunctions)); + }); + it("should add observeFunction into observeFunctions", function() { + assert.deepEqual([observeFunction], publisher.observeFunctions["a"]); + }); + }); + describe("#subscribeModel()", function() { + const publisher = new EventPublisher(); + const observeFunction = function() {}; + publisher.subscribeModel("a", observeFunction); + it("should make subjectName into observeFunctionsInModel", function() { + assert.deepEqual(["a"], Object.keys(publisher.observeFunctionsInModel)); + }); + it("should add observeFunction into observeFunctionsInModel", function() { + assert.deepEqual([observeFunction], publisher.observeFunctionsInModel["a"]); + }); + }); + describe("#publish", function() { + const publisher = new EventPublisher(); + let isCalledModel = false; + const callbackInModel = function() { + isCalledModel = true; + }; + const callback = function(author, data, data2, data3) { + it("This callback function should be called", () => { + assert(true); + }); + it("This callback function should be called after callback function in model", () => { + assert(isCalledModel); + }); + it("should equal author", () => { + assert.equal(author, "hello-author"); + }); + it("should equal data", () => { + assert.equal(data, "data-test"); + assert.equal(data2, 100); + assert.equal(data3, true); + }); + }; + publisher.subscribe("a", callback); + publisher.subscribeModel("a", callbackInModel); + publisher.publish("hello-author", "a", "data-test", 100, true); + }); + describe("#clearObserveFunctions", () => { + const publisher = new EventPublisher(); + publisher.subscribe("hoge", () => {}); + publisher.subscribeModel("hoge", () => {}); + + it("should clear observeFunctions and observeFunctionsInModel", () => { + assert.notDeepEqual(publisher.observeFunctions, {}); + assert.notDeepEqual(publisher.observeFunctionsInModel, {}); + + publisher.clearObserveFunctions(); + + assert.deepEqual(publisher.observeFunctions, {}); + assert.deepEqual(publisher.observeFunctionsInModel, {}); + }); + }); +}); diff --git a/test/rankingMaker.test.js b/test/rankingMaker.test.js new file mode 100644 index 0000000..80678ce --- /dev/null +++ b/test/rankingMaker.test.js @@ -0,0 +1,70 @@ +import assert from "assert"; +import RankingMaker from "../rankingMaker"; +import ControllerModel from "../model/controllerModel"; +import sinon from "sinon"; +import publisher from "../publisher"; + +describe("RankingMaker", () => { + publisher.clearObserveFunctions(); + describe("#make", () => { + const controllerModel = new ControllerModel(); + controllerModel.controllers = { + controller1: { + linkedOrb: "orb1", + isOni: false, + hp: 80, + getStates() { return "states"; } + }, + controller2: { + linkedOrb: "orb2", + isOni: false, + hp: 100, + getStates() { return "states"; } + }, + controller3: { + linkedOrb: "orb3", + isOni: false, + hp: 80, + getStates() { return "states"; } + }, + oni1: { + linkedOrb: "orb4", + isOni: true, + hp: 100, + getStates() { return "oni-states"; } + } + }; + + const rankingMaker = new RankingMaker({ controllerModel }); + const makeSpy = sinon.spy(rankingMaker, "make"); + let rankingDetails; + publisher.subscribe("ranking", (author, ranking) => { + it("should publish ranking", () => { + assert(author === rankingMaker); + rankingDetails = ranking; + }); + }); + + rankingMaker.make("show"); + + it("should be called", () => { + assert(makeSpy.withArgs("show").called); + }); + + it("should return correct ranking", () => { + assert(typeof rankingDetails.ranking !== "undefined"); + assert.equal(rankingDetails.ranking.length, 3); + assert.deepEqual(rankingDetails.ranking[0], { name: "controller2", states: "states", isTie: false }); + assert.deepEqual(rankingDetails.ranking[1], { name: "controller1", states: "states", isTie: false }); + assert.deepEqual(rankingDetails.ranking[2], { name: "controller3", states: "states", isTie: true }); + }); + + it("should return correct onis", () => { + assert(typeof rankingDetails.onis !== "undefined"); + assert(typeof rankingDetails.onis["oni1"] !== "undefined"); + assert.equal(rankingDetails.onis["oni1"], "oni-states"); + }); + + makeSpy.restore(); + }); +}); diff --git a/test/spheroServerManager.test.js b/test/spheroServerManager.test.js new file mode 100644 index 0000000..2b81da0 --- /dev/null +++ b/test/spheroServerManager.test.js @@ -0,0 +1,32 @@ +import assert from "assert"; +import SpheroServerManager from "../spheroServerManager"; +import publisher from "../publisher"; +import sinon from "sinon"; +import { EventEmitter } from "events"; + +// test には sphero-websocket が必要で、その際ネイティブ依存のモジュールも必要になるので、 +// テストできない。テストするには、sphero-websocket の testMode 時にネイティブ依存モジュールを使わないよう +// 改善しなければならない + + +describe("SpheroServerManager", () => { + let spheroServerManager; + beforeEach(done => { + publisher.clearObserveFunctions(); + spheroServerManager = new SpheroServerManager({}, { + spheroServer: { events: new EventEmitter() } + }); + done(); + }); + describe("#publishAddedOrb", () => { + it("should publish addedOrb", () => { + const spy = sinon.spy(); + const orbName = "orb1"; + const orb = "orb"; + publisher.subscribe("addedOrb", spy); + spheroServerManager.publishAddedOrb(orbName, orb); + assert(spy.withArgs(spheroServerManager, orbName, orb)); + }); + }); +}); + diff --git a/test/uuidManager.test.js b/test/uuidManager.test.js new file mode 100644 index 0000000..d2ffced --- /dev/null +++ b/test/uuidManager.test.js @@ -0,0 +1,29 @@ +import assert from "assert"; +import UUIDManager from "../uuidManager"; +import publisher from "../publisher"; +import sinon from "sinon"; +import { EventEmitter } from "events"; + +describe("UUIDManager", () => { + let uuidManager; + const noble = new EventEmitter(); + beforeEach(done => { + noble.removeAllListeners(); + publisher.clearObserveFunctions(); + uuidManager = new UUIDManager({ + appModel: { isTestMode: false } + }, noble); + done(); + }); + + it("on#discover", () => { + const testName = "test-name"; + const testUUID = "test-uuid"; + const spy = sinon.spy(); + publisher.subscribe("setNameOfUUID", spy); + noble.emit("discover", { + advertisement: { localName: testName }, + uuid: testUUID + }); + }); +}); diff --git a/test/virtualSpheroManager.test.js b/test/virtualSpheroManager.test.js new file mode 100644 index 0000000..24be915 --- /dev/null +++ b/test/virtualSpheroManager.test.js @@ -0,0 +1,13 @@ +import assert from "assert"; +import VirtualSpheroManager from "../virtualSpheroManager"; +import VirtualSphero from "sphero-ws-virtual-plugin"; + +describe("VirtualSpheroManager", () => { + describe("#constructor()", () => { + const virtualSpheroManager = new VirtualSpheroManager({}, 8081); + it("should initialize virtualSphero", () => { + assert(typeof virtualSpheroManager === "object"); + assert(virtualSpheroManager.virtualSphero instanceof VirtualSphero); + }); + }); +}); diff --git a/uuidManager.js b/uuidManager.js new file mode 100644 index 0000000..2ca0dca --- /dev/null +++ b/uuidManager.js @@ -0,0 +1,20 @@ +import ComponentBase from "./componentBase"; + +export default class UUIDManager extends ComponentBase { + constructor(models, noble) { + super(models); + this.noble = noble; + + if (!this.appModel.isTestMode && this.noble) { + this.noble.on("stateChange", state => { + if (state === "poweredOn") { + this.noble.startScanning(); + } + }); + this.noble.on("discover", peripheral => { + this.publish("setNameOfUUID", peripheral.uuid, peripheral.advertisement.localName); + }); + } + } +} + diff --git a/virtualSpheroManager.js b/virtualSpheroManager.js new file mode 100644 index 0000000..bfd729d --- /dev/null +++ b/virtualSpheroManager.js @@ -0,0 +1,25 @@ +import ComponentBase from "./componentBase"; +import VirtualSphero from "sphero-ws-virtual-plugin"; + +export default class VirtualSpheroManager extends ComponentBase { + constructor(models, port) { + super(models); + this.virtualSphero = new VirtualSphero(port); + + this.subscribe("addedClient", this.addSphero); + this.subscribe("removedClient", this.removeSphero); + this.subscribe("command", this.command); + } + + addSphero(name) { + this.virtualSphero.addSphero(name); + } + + removeSphero(name) { + this.virtualSphero.removeSphero(name); + } + + command(controllerName, commandName, args) { + this.virtualSphero.command(controllerName, commandName, args); + } +}