const Player = require('./Player.js');
const Timer = require('./Timer.js');
const Utils = require('./Utils.js');
const Table = require('./Table.js');
const fs = require('fs-extra');
const path = require('path');
const CircularJSON = require('./circularjson.js');
/** A group of players playing in a {@link Period}. */
class Group {
/**
* Create a new Group.
*
* @param {String} id The id of this group.
* @param {Period} period The period this group belongs to.
*/
constructor(id, period) {
/**
* this group's id
* @type {String}
*/
this.id = id;
/**
* Each Group belongs to a single Period.
* @type {Period}
*/
this.period = period;
/**
* a list of the players in this group.
* @type array
* @default []
*/
this.players = [];
/**
* whether or not all players in this group have been created yet.
* @type boolean
* @default false
*/
this.allPlayersCreated = false;
/**
* 'outputHide' fields are not included in output.
* @type array
* @default []
*/
this.outputHide = [];
/**
* 'outputHideAuto' fields are not included in output.
* @type {String[]}
*/
this.outputHideAuto = ['stage', 'status', 'outputHide', 'outputHideAuto', 'players', 'stageTimer', 'period', 'tables', 'type', 'stageIndex', 'stageEndedIndex'];
/**
* @type array
* @default []
*/
this.tables = [];
/**
* @type number
* @default 0
*/
this.stageIndex = 0;
this.stageStartedIndex = -1;
this.stageEndedIndex = -1;
}
/**
* Returns the stage that this group is currently in.
*/
stage() {
return this.app().stages[this.stageIndex];
}
/**
* Loads the group from a given set of data.
*
* CALLED FROM:
* - {@link Session#load}
*
* @param {type} json The JSON data describing the group.
* @param {type} session The session to which the group belongs.
* @return {Group} The group.
*/
static load(json, session, data) {
var app = session.apps[json.appIndex-1];
var period = app.periods[json.periodId-1];
var id = json.id;
var newGroup = new Group(id, period);
if (period.groups.length > id-1) {
var curGroup = period.groups[id-1];
newGroup.players = curGroup.players;
}
for (var j in json) {
newGroup[j] = json[j];
}
if (json !== null && json.stageTimerStart !== undefined) {
var lastTimeOn = data.lastTimeOn;
var timeLeft = json.stageTimerTimeLeft;
if (session.isRunning) {
timeLeft = timeLeft - (lastTimeOn - new Date(json.stageTimerStart).getTime());
if (timeLeft >= 0) {
var stage = app.stages[json.stageTimerStageIndex];
var group = newGroup;
var callback = eval('(' + json.stageTimerCallback + ')');
newGroup.stageTimer = Timer.load(json.stageTimerDuration, timeLeft, json.stageTimerStageIndex, callback);
newGroup.stageTimer.resume();
newGroup.save();
}
}
}
period.groups[id-1] = newGroup;
}
/**
* Find the player with the given participant ID.
* @param {String} id the given id.
* @return {type} the player where player.participant.id == id.
*/
playerByParticipantId(id) {
for (var i=0; i<this.players.length; i++) {
if (this.players[i].participant.id === id) {
return this.players[i];
}
}
return null;
}
/**
* playerWithParticipant - description
*
* @param {type} participant description
* @return {type} description
*/
playerWithParticipant(participant) {
return this.playerByParticipantId(participant.id);
}
slowestPlayers() {
var out = [];
var minStageIndex = null;
for (var i in this.players) {
var part = this.players[i];
if (minStageIndex === null || part.stageIndex <= minStageIndex) {
if (minStageIndex === null || part.stageIndex < minStageIndex) {
minStageIndex = part.stageIndex;
out = [];
}
out.push(part);
}
}
return out;
}
addTable(name) {
this[name] = new Table.new(name, 'this.context.emit', this, this.roomId(), this.session());
this.tables.push(name);
}
playerWithId(id) {
for (var i=0; i<this.players.length; i++) {
if (this.players[i].idInGroup === id) {
return this.players[i];
}
}
return null;
}
jt() {
return this.session().jt;
}
canProcessMessage() {
return true;
}
/**
* old - description
*
* @return {type} description
*/
old() {
return this.app().previousGroup(this);
}
/**
* playersExcept - description
*
* @param {type} player description
* @return {type} description
*/
playersExcept(player) {
return this.playersExceptIds([player.id]);
}
playersExceptIds(ids) {
if (!Array.isArray(ids)) {
ids = [ids];
}
var out = [];
for (var i=0; i<this.players.length; i++) {
if (!ids.includes(this.players[i].participant.id)) {
out.push(this.players[i]);
}
}
return out;
}
clearStageTimer() {
//console.log('clearing stage timer');
if (this.stageTimer !== undefined) {
this.stageTimer.clear();
this.stageTimer = undefined;
}
}
/**
* isFinished - description
*
* @return {type} description
*/
isFinished() {
for (var i=0; i<this.players.length; i++) {
if (!this.players[i].isFinished()) {
return false;
}
}
return true;
}
/*
* outputFields - description
*
* @return {type} description
*/
outputFields() {
var fields = [];
for (var prop in this) {
if (
!Utils.isFunction(this[prop]) &&
!this.outputHide.includes(prop) &&
!this.outputHideAuto.includes(prop) &&
!this.tables.includes(prop)
)
fields.push(prop);
}
return fields;
}
player(id) {
return Utils.findByIdWOJQ(this.players, id);
}
/**
* shell - description
*
* @return {type} description
*/
shellWithParent() {
var out = {};
var fields = this.outputFields();
for (var f in fields) {
var field = fields[f];
out[field] = this[field];
}
out.period = this.period.shellWithParent();
out.numPlayers = this.players.length;
if (this.stageTimer !== undefined) {
out.stageTimerStart = this.stageTimer.timeStarted;
out.stageTimerDuration = this.stageTimer.duration;
} else {
out.timer = 'none';
}
out.tables = this.tables;
for (var i in this.tables) {
var name = this.tables[i];
if (this[name] != null) {
out[name] = this[name].shell();
}
}
return out;
}
shellForPlayerUpdate() {
var out = this.shellWithChildren();
out.period = this.period.shellWithParent();
return out;
}
timeInStage() {
if (this.stageTimer == null) {
return 0;
}
return this.stageTimer.state().timeElapsed;
}
/**
* emitUpdate - description
*
* @return {type} description
*/
emitUpdate() {
this.emit('groupUpdate', this.shellWithChildren());
}
/**
* Returns the sum of "field" over all players in the group.
*
* @param {String} field
*/
sum(field) {
return Utils.sum(this.players, field);
}
/**
* Emit the given message to subscriber's of this group.
*
* @param {type} msgTitle The title of the message.
* @param {type} msgData The data of the message.
*/
emit(msgTitle, msgData) {
this.session().io().to(this.roomId()).emit(msgTitle, CircularJSON.stringify(msgData));
}
/**
* session - description
*
* @return {@link Session} The session that this group belongs to.
*/
session() {
return this.period.session();
}
/**
* roomId - description
*
* @return {type} description
*/
roomId() {
return this.period.roomId() + '_group_' + this.id;
}
/*
* moveToNextStage - description
*
* @return {type} description
*/
moveToNextStage() {
for (var i=0; i<this.players.length; i++) {
this.app().playerMoveToNextStage(this.players[i]);
}
}
showStatus() {
console.log('Group ' + this.id + ': stageIndex=' + this.stageIndex + ', stageEndedIndex=' + this.stageEndedIndex);
this.session().printStatuses();
}
/**
* Returns the app this group is in.
*
* @return {App} The app.
*/
app() {
return this.period.app;
}
/*
* Add client to this group.<br>
* 1. The client joins this group's channel.
*
* @param {type} client description
*/
addClient(client) {
client.socket.join(this.roomId());
}
/**
* shellAll - description
*
* @return {type} description
*/
shellWithChildren() {
var out = {};
var fields = this.outputFields();
for (var f in fields) {
var field = fields[f];
out[field] = this[field];
}
out.period = this.period.id;
out.players = [];
for (var i in this.players) {
out.players[i] = this.players[i].shellWithChildren();
}
if (this.stageTimer !== undefined) {
out.stageTimerStart = this.stageTimer.timeStarted;
out.stageTimerDuration = this.stageTimer.duration;
out.stageTimerTimeLeft = this.stageTimer.timeLeft;
}
out.tables = this.tables;
for (var i in this.tables) {
var name = this.tables[i];
if (this[name] !== undefined) {
out[name] = this[name].shell();
}
}
return out;
}
/*
* getOutputDir - description
*
* @return {type} description
*/
getOutputDir() {
return this.period.getOutputDir() + '/groups/' + this.id;
}
/**
* shellLocal - description
*
* @return {type} description
*/
shell() {
var out = {};
var fields = this.outputFields();
for (var f in fields) {
var field = fields[f];
out[field] = this[field];
}
out.tables = this.tables;
if (this.stageTimer !== undefined) {
out.stageTimerStart = this.stageTimer.timeStarted;
out.stageTimerDuration = this.stageTimer.duration;
out.stageTimerTimeLeft = this.stageTimer.timeLeft;
out.stageTimerStageIndex = this.stageTimer.stageIndex;
out.stageTimerCallback = this.stageTimer.callback.toString();
}
out.periodId = this.period.id;
out.appIndex = this.app().indexInSession();
return out;
}
/**
* save - description
*/
save() {
try {
this.session().jt.log('Group.save: ' + this.roomId());
var toSave = this.shell();
this.session().saveDataFS(toSave, 'GROUP');
for (var i=0; i<this.tables.length; i++) {
var table = this[this.tables[i]];
if (table !== undefined) {
this[this.tables[i]].save();
}
}
} catch (err) {
console.log('Error saving group ' + this.id + ': ' + err);
console.log(err.stack);
}
}
canPlayersStart(stage) {
if (this.stageStartedIndex >= stage.indexInApp()) {
return true;
}
// If do not need to wait for all players, return true.
if (!stage.waitToStart) {
return true;
}
// If any player is not ready, return false.
for (let p in this.players) {
let player = this.players[p];
if (!player.isReady(stage.indexInApp())) {
return false;
}
}
return true;
}
canPlayersEnd(stage) {
// If Group has already finished, do not allow players to finish.
if (this.stageEndedIndex >= stage.indexInApp()) {
return false;
}
// If do not need to wait for all players, return true.
if (!stage.waitToEnd) {
return true;
}
// If any player is not finished playing, return false.
for (let p in this.players) {
let player = this.players[p];
if (!player.isFinished()) {
return false;
}
}
// Otherwise, return true.
return true;
}
// checkIfWaitingToEnd(stage, endPlayers, canParticipate) {
// console.log('Group.checkIfWaitingToEnd: ' + this.roomId());
// this.showStatus();
// if (canParticipate == null) {
// canParticipate = true;
// }
// let group = this;
// // Wait for players to submit their forms.
// var waitingForPlayers = false;
// if (!canParticipate) {
// this.attemptToStartNextStage();
// return;
// }
// if (stage.waitOnTimerEnd) {
// for (var p in group.players) {
// var player = group.players[p];
// // If player is in an earlier stage, wait.
// if (player.stage.indexInApp() < stage.indexInApp()) {
// waitingForPlayers = true;
// } else if (player.stage.indexInApp() > stage.indexInApp()) {
// // If player is past this stage, proceed.
// } else {
// // If player is in this stage and not finished...
// if (!player.isFinished()) {
// // If any clients are connected, let player finish via call to "endStage".
// if (player.participant.clients.length > 0) {
// waitingForPlayers = true;
// if (endPlayers) {
// player.emit('endStage', player.shellWithParent());
// }
// }
// // If not, end player immediately.
// else {
// if (endPlayers) {
// console.log('No connected clients for ' + player.id + ', ending immediately.');
// player.attemptToEndStage(false);
// } else {
// waitingForPlayers = true;
// }
// }
// }
// }
// }
// }
// // If not waiting for any players, proceed without waiting for players to submit their forms.
// if (!waitingForPlayers) {
// for (var p in group.players) {
// var player = group.players[p];
// if (player.stage.id === stage.id && player.status !== 'done') {
// player.justEndStage();
// }
// }
// console.log(this.jt().settings.getConsoleTimeStamp() + ' END - GROUP : ' + stage.id + ', ' + group.roomId());
// stage.groupEnd(group);
// this.attemptToStartNextStage();
// } else {
// // debugger;
// }
// }
startStage(stage) {
if (!stage.canGroupParticipate(this)) {
this.endStage();
}
if (!stage.canGroupStart(this)) {
return;
}
this.stageStartedIndex++;
this.stageIndex = stage.indexInApp();
let groupDuration = stage.getGroupDuration(this);
if (groupDuration > 0) {
let timeOutCB = function(stage) {
this.session().addMessageToStartOfQueue(this, stage, 'forceEndStage');
}.bind(this, stage);
this.stageTimer = new Timer.new(
timeOutCB,
groupDuration*1000,
stage.indexInApp()
);
}
try {
console.log(this.jt().settings.getConsoleTimeStamp() + ' START - GROUP : ' + stage.id + ', ' + this.roomId());
stage.groupStart(this);
} catch (err) {
console.log(err.stack);
}
try {
this.save();
} catch (err) {}
for (var p in this.players) {
try {
this.players[p].startStage(stage);
} catch (err) {}
}
}
forceEndStage(stage) {
console.log('Group.forceEndStage: ' + stage.id);
this.clearStageTimer();
this.endStage(stage, true);
}
endStage(stage, forcePlayersToEnd) {
if (forcePlayersToEnd == null) {
forcePlayersToEnd = false;
}
if (!stage.canGroupEnd(this, forcePlayersToEnd)) {
return;
}
// If waiting for any players, stop.
if (this.waitingForPlayersInStage(stage, forcePlayersToEnd)) {
return;
}
this.clearStageTimer();
for (var p in this.players) {
var player = this.players[p];
if (player.stage.id === stage.id && player.status !== 'finished') {
player.endStage(false);
}
}
console.log(this.jt().settings.getConsoleTimeStamp() + ' END - GROUP : ' + stage.id + ', ' + this.roomId());
this.stageEndedIndex = stage.indexInApp();
stage.groupEnd(this);
if (stage.waitToEnd) {
for (var p in this.players) {
let player = this.players[p];
if (player.stageIndex === stage.indexInApp()) {
player.moveToNextStage();
}
}
}
this.stageIndex = stage.indexInApp() + 1;
if (this.stageIndex < this.app().stages.length) {
// move group (and all its players) to next stage.
this.startStage(this.stage());
} else {
// move all players to next period.
for (var p in this.players) {
this.players[p].moveToNextStage();
}
}
}
// Check if this group is waiting for players to finish playing the given stage.
// If it is, tell those players to end the stage.
// Return whether or not any player
waitingForPlayersInStage(stage, forcePlayersToEnd) {
let waitingForPlayers = false;
if (stage.waitOnTimerEnd) {
for (var p in this.players) {
var player = this.players[p];
// If player is in an earlier stage, wait.
if (player.stage.indexInApp() < stage.indexInApp()) {
waitingForPlayers = true;
} else if (player.stage.indexInApp() > stage.indexInApp()) {
// If player is past this stage, proceed.
} else {
// If player is in this stage and not finished...
if (!player.isFinished()) {
waitingForPlayers = true;
if (forcePlayersToEnd) {
// If any clients are connected, let player finish via call to "endStage".
if (player.participant.clients.length > 0) {
player.emit('endStage', player.shellWithParent());
}
// If not, end player immediately.
else {
console.log('No connected clients for ' + player.id + ', ending immediately.');
player.endStage(false);
}
}
}
}
}
}
return waitingForPlayers;
}
}
var exports = module.exports = {};
exports.new = Group;
exports.load = Group.load;