Skip to content

Commit

Permalink
[Api]: Randomly choose first leader index; User join at most one grou…
Browse files Browse the repository at this point in the history
…p at a time; 'group' rename 'successVotes' to 'votes' and move from 'missions.teams' to 'missions'; Summaries add team leader. [Ui]: Approve and vote once; Remove 'btn' border.
  • Loading branch information
sontdhust committed Aug 26, 2016
1 parent 1d885a3 commit 969ccda
Show file tree
Hide file tree
Showing 12 changed files with 110 additions and 80 deletions.
5 changes: 5 additions & 0 deletions client/lib/avalon.css
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ ul.bar_tabs > li {
width: auto;
}

.btn,
.btn:hover {
border-color: transparent;
}

/*
* Modified
*/
Expand Down
117 changes: 67 additions & 50 deletions imports/api/groups/groups.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ TeamsSchema = new SimpleSchema({
// Index of leader's id which is included in `players.id`, corresponds to the index of this team in `missions.teams`
memberIndices: { type: [Number], defaultValue: [] }, // Indices of players who were selected to be sent out on the mission, correspond to `players.id`
approvals: { type: [Boolean], defaultValue: [] }, // Indicate players whether to approve the mission team make-up or not, with indices correspond to `players.id`
successVotes: { type: [Boolean], defaultValue: [] }, // Indicate members whether to vote for the mission to success or not, with indices correspond to `missions.teams.memberIndices`
});

MissionsSchema = new SimpleSchema({
teams: { type: [TeamsSchema], defaultValue: [] },
votes: { type: [Boolean], defaultValue: [] }, // Indicate members whether to vote for the mission to success or not, with indices correspond to `missions.teams.memberIndices`
});

MessagesSchema = new SimpleSchema({
Expand All @@ -67,6 +67,7 @@ Groups.schema = new SimpleSchema({
ownerId: { type: String, regEx: SimpleSchema.RegEx.Id }, // Owner's id
name: { type: String },
players: { type: [PlayersSchema], defaultValue: [] }, // Group players, include the owner
firstLeaderIndex: { type: Number, defaultValue: 0 },
missions: { type: [MissionsSchema], defaultValue: [] }, // Mission proposals
guessMerlin: { type: Boolean, optional: true }, // Indicate whether Assassin correctly guesses Merlin's identity or not
messages: { type: [MessagesSchema], defaultValue: [] },
Expand All @@ -75,24 +76,22 @@ Groups.schema = new SimpleSchema({

Groups.attachSchema(Groups.schema);

// This represents the keys from Groups objects that should be published
// to the client. If we add secret properties to Group objects, don't list
// them here to keep them private to the server.
Groups.publicFieldsWhenFindAll = {
ownerId: 1,
name: 1,
'players.id': 1,
'missions.length': 1,
};

// TODO: Remove secret properties to keep them private
Groups.publicFieldsWhenFindOne = {
ownerId: 1,
name: 1,
players: 1,
missions: 1,
guessMerlin: 1,
messages: 1,
Groups.publicFields = {
findAll: {
ownerId: 1,
name: 1,
'players.id': 1,
'missions.length': 1,
},
findOne: { // TODO: Remove secret properties to keep them private
ownerId: 1,
name: 1,
players: 1,
firstLeaderIndex: 1,
missions: 1,
guessMerlin: 1,
messages: 1,
}
};

Groups.helpers({
Expand All @@ -118,35 +117,41 @@ Groups.helpers({
isPlaying() {
return this.missions.length != 0;
},
getLastMission() { // Private use only
return this.missions[this.missions.length - 1];
},
getLastTeam() { // Force return `null` if missions list or teams list is empty (instead of `undefined`)
const lastMission = this.missions[this.missions.length - 1];
const lastMission = this.getLastMission();
return (lastMission || null) && lastMission.teams && lastMission.teams[lastMission.teams.length - 1] || null;
},
getTeamsCount() {
return this.missions.reduce((c, m) => c + m.teams.length, 0);
},
getLastTeamsCount() {
return this.missions[this.missions.length - 1].teams.length;
return this.getLastMission().teams.length;
},
findRequiredFailVotesCount(missionIndex) {
return missionIndex == 3 && this.players.length >= 7 ? 2 : 1;
},
getSummaries() {
return this.missions.map((m, i) =>
m.teams.map((t, j) => {
const summary = { memberIndices: t.memberIndices, denierIndices: [], failVotesCount: null, result: undefined };
let lastMissionsTeamsCount = 0;
return this.missions.map((m, i) => {
const mission = m.teams.map((t, j) => {
const team = { leaderIndex: (this.firstLeaderIndex + lastMissionsTeamsCount + j) % this.players.length, memberIndices: t.memberIndices, denierIndices: [], failVotesCount: null, result: undefined };
if (t.memberIndices.length != 0 && t.approvals.indexOf(null) == -1) {
summary.denierIndices = t.approvals.map((a, i) => a ? -1 : i).filter(i => i >= 0);
team.denierIndices = t.approvals.map((a, i) => a ? -1 : i).filter(i => i >= 0);
if (j < Groups.MISSION_TEAMS_COUNT - 1 && t.approvals.filter(a => !a).length >= t.approvals.filter(a => a).length) { // Denied, Hammer
summary.result = null;
} else if (t.successVotes.indexOf(null) == -1) {
summary.failVotesCount = t.successVotes.filter(a => !a).length;
summary.result = summary.failVotesCount < this.findRequiredFailVotesCount(i) ? true : false;
team.result = null;
} else if (m.votes.indexOf(null) == -1) {
team.failVotesCount = m.votes.filter(a => !a).length;
team.result = team.failVotesCount < this.findRequiredFailVotesCount(i) ? true : false;
}
}
return summary;
})
);
return team;
});
lastMissionsTeamsCount += m.teams.length;
return mission;
});
},
startNewMission() {
const summaryResults = this.getSummaries().map(m => m[m.length - 1].result);
Expand All @@ -157,7 +162,7 @@ Groups.helpers({
this.startGuessingMerlin();
return;
}
const newMission = { teams: [] };
const newMission = { teams: [], votes: [] };
Groups.update(this._id, {
$push: { missions: newMission }
});
Expand All @@ -172,10 +177,10 @@ Groups.helpers({
// const memberIndices = _.shuffle(Array.from(new Array(this.players.length), (_, i) => i)).slice(0, Groups.MISSIONS_MEMBERS_COUNT[this.players.length][this.missions.length - 1]); // TEST
const memberIndices = [];
Groups.update(this._id, {
$push: { [`missions.${this.missions.length - 1}.teams`]: { memberIndices: memberIndices, approvals: [], successVotes: [] } }
$push: { [`missions.${this.missions.length - 1}.teams`]: { memberIndices: memberIndices, approvals: [] } }
});
// FIXME: Remove redundancy (@ref 'methods' `selectedMembers`)
this.missions[this.missions.length - 1].teams.push({ memberIndices: memberIndices, approvals: [], successVotes: [] }); // Update local variable
this.getLastMission().teams.push({ memberIndices: memberIndices, approvals: [] }); // Update local variable
if (!this.isSelectingMembers()) {
this.startWaitingForApproval();
}
Expand All @@ -195,23 +200,29 @@ Groups.helpers({
}
},
startWaitingForVote() {
const lastMission = this.missions[this.missions.length - 1];
// const successVotes = Array.from(new Array(Groups.MISSIONS_MEMBERS_COUNT[this.players.length][this.missions.length - 1]), (_, i) => Groups.ROLES[this.players[lastMission.teams[lastMission.teams.length - 1].memberIndices[i]].role].side ? true : Math.random() > 0.3 ? true : false); // TEST
const successVotes = Array.from(new Array(Groups.MISSIONS_MEMBERS_COUNT[this.players.length][this.missions.length - 1]), (_, i) => Groups.ROLES[this.players[lastMission.teams[lastMission.teams.length - 1].memberIndices[i]].role].side ? null : null);
const lastMission = this.getLastMission();
// const votes = Array.from(new Array(Groups.MISSIONS_MEMBERS_COUNT[this.players.length][this.missions.length - 1]), (_, i) => Groups.ROLES[this.players[lastMission.teams[lastMission.teams.length - 1].memberIndices[i]].role].side ? true : Math.random() > 0.3 ? true : false); // TEST
const votes = Array.from(new Array(Groups.MISSIONS_MEMBERS_COUNT[this.players.length][this.missions.length - 1]), (_, i) => Groups.ROLES[this.players[lastMission.teams[lastMission.teams.length - 1].memberIndices[i]].role].side ? null : null);
Groups.update(this._id, {
$set: { [`missions.${this.missions.length - 1}.teams.${lastMission.teams.length - 1}.successVotes`]: successVotes }
$set: { [`missions.${this.missions.length - 1}.votes`]: votes }
});
// FIXME: Remove redundancy (@ref 'methods' `vote`)
this.getLastTeam().successVotes = successVotes; // Update local variable
this.getLastMission().votes = votes; // Update local variable
if (!this.isWaitingForVote()) {
this.startNewMission();
}
},
startGuessingMerlin() {
// const guessMerlin = Math.random() > 0.5 ? true : false; // TEST
const guessMerlin = null;
Groups.update(this._id, {
$set: { guessMerlin: null }
// $set: { guessMerlin: Math.random() > 0.5 ? true : false } // TEST
$set: { guessMerlin: guessMerlin }
});
// FIXME: Remove redundancy (@ref 'methods' `guess`)
this.guessMerlin = guessMerlin; //Update local variable
if (this.guessMerlin != null) {
this.finish();
}
},
finish() {
const playerActivities = [];
Expand Down Expand Up @@ -239,13 +250,13 @@ Groups.helpers({
}
},
getLeader() {
return this.missions.length > 0 ? Meteor.users.findOne(this.players[(this.getTeamsCount() - 1) % this.players.length].id) : null;
return this.missions.length > 0 ? Meteor.users.findOne(this.players[(this.firstLeaderIndex + this.getTeamsCount() - 1) % this.players.length].id) : null;
},
hasLeader(userId) {
return this.missions.length > 0 ? this.players[(this.getTeamsCount() - 1) % this.players.length].id == userId : false;
return this.missions.length > 0 ? this.players[(this.firstLeaderIndex + this.getTeamsCount() - 1) % this.players.length].id == userId : false;
},
hasHammer(userId) {
return this.missions.length > 0 ? this.players[(this.getTeamsCount() - this.getLastTeamsCount() + Groups.MISSION_TEAMS_COUNT - 1) % this.players.length].id == userId : false;
return this.missions.length > 0 ? this.players[(this.firstLeaderIndex + this.getTeamsCount() - this.getLastTeamsCount() + Groups.MISSION_TEAMS_COUNT - 1) % this.players.length].id == userId : false;
},
hasMember(userId) {
return this.getLastTeam() != null && this.getLastTeam().memberIndices.find(i => this.players[i].id == userId) != undefined;
Expand All @@ -266,11 +277,17 @@ Groups.helpers({
return this.getLastTeam() != null && this.getLastTeam().approvals.indexOf(null) == -1 && this.getLastTeam().approvals.filter(a => !a).length >= this.getLastTeam().approvals.filter(a => a).length;
},
isWaitingForVote() {
return this.getLastTeam() != null && this.getLastTeam().successVotes.indexOf(null) != -1;
return this.getLastMission() != null && this.getLastMission().votes.indexOf(null) != -1;
},
isGuessingMerlin() {
return this.guessMerlin === null;
},
checkPlayerHasApproved(userId) {
return this.getLastTeam() != null && this.getLastTeam().approvals[this.players.map(p => p.id).indexOf(userId)] != null;
},
checkMemberHasVoted(userId) {
return this.getLastMission() != null && this.getLastMission().votes[this.getLastTeam().memberIndices.indexOf(this.players.map(p => p.id).indexOf(userId))] != null;
},
getSituation() {
const situation = { status: '', slot: undefined, result: undefined };
if (!this.isPlaying()) {
Expand Down Expand Up @@ -312,7 +329,7 @@ Groups.helpers({
},
findInformation(userId, playerId) {
const indices = this.players.map(p => p.id);
const userRole = this.players[indices.indexOf(userId)].role;
const userRole = this.players[indices.indexOf(userId)] && this.players[indices.indexOf(userId)].role || 'Undecided';
const playerIndex = indices.indexOf(playerId);
const playerRole = this.players[playerIndex].role;
const otherPlayer = userId != playerId;
Expand All @@ -321,10 +338,10 @@ Groups.helpers({
let status = '';
if (this.isWaitingForApproval()) {
const approval = this.getLastTeam().approvals[playerIndex];
status = approval == null ? 'Undecided' : approval ? 'Approved' : 'Denied';
status = approval === undefined ? '' : otherPlayer || approval == null ? 'Waiting' : approval ? 'Approved' : 'Denied';
}
if (this.isWaitingForVote()) {
const vote = this.getLastTeam().successVotes[this.getLastTeam().memberIndices.indexOf(playerIndex)];
const vote = this.getLastMission().votes[this.getLastTeam().memberIndices.indexOf(playerIndex)];
status = vote === undefined ? '' : otherPlayer || vote == null ? 'Waiting' : vote ? 'Voted Success' : 'Voted Fail';
}
return { role: role, side: side, status: status };
Expand Down Expand Up @@ -354,7 +371,7 @@ Groups.helpers({
<b>{approval == null ? '' : `(You ${approval ? 'approved' : 'denied'})`}</b>
</p>;
} else if (this.isWaitingForVote() && this.hasMember(userId)) {
const vote = this.getLastTeam().successVotes[this.getLastTeam().memberIndices.indexOf(this.players.map(p => p.id).indexOf(userId))];
const vote = this.getLastMission().votes[this.getLastTeam().memberIndices.indexOf(this.players.map(p => p.id).indexOf(userId))];
suggestion =
<p>
Press buttons to vote for the mission success or fail
Expand Down
6 changes: 3 additions & 3 deletions imports/api/groups/methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,13 @@ export const start = new ValidatedMethod({
const minions = Array.from(new Array(evilPlayersCount - roles.filter(r => !Groups.ROLES[r].side).length)).map(() => 'Minion');
_.shuffle(roles.concat(servants, minions)).forEach((r, i) => players[i].role = r);
Groups.update(groupId, {
$set: { players: players }
$set: { players: players, firstLeaderIndex: Math.floor(Math.random() * playersCount) }
});
group = Groups.findOne(groupId); // Update local variable
group.startNewMission();
} else {
Groups.update(groupId, {
$set: { players: group.players.map(player => ({ id: player.id, role: 'Undecided' })), messages: [] },
$set: { players: group.players.map(player => ({ id: player.id, role: 'Undecided' })), firstLeaderIndex: 0, messages: [] },
$unset: { guessMerlin: 1 }
});
}
Expand Down Expand Up @@ -149,7 +149,7 @@ export const vote = new ValidatedMethod({
run({ groupId, success }) {
let group = Groups.findOne(groupId);
Groups.update(groupId, {
$set: { [`missions.${group.missions.length - 1}.teams.${group.missions[group.missions.length - 1].teams.length - 1}.successVotes.${group.getLastTeam().memberIndices.indexOf(group.players.map(p => p.id).indexOf(this.userId))}`]: success }
$set: { [`missions.${group.missions.length - 1}.votes.${group.getLastTeam().memberIndices.indexOf(group.players.map(p => p.id).indexOf(this.userId))}`]: success }
});
group = Groups.findOne(groupId); // Update local variable
if (!group.isWaitingForVote()) {
Expand Down
6 changes: 3 additions & 3 deletions imports/api/groups/server/publications.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ Meteor.publish('groups.findAll', (name, page) => {
const lastGroup = Groups.find(selector, { sort: { createdAt: -1 }, limit: skippedGroupsCount }).fetch().pop();
selector.createdAt = { $lt: lastGroup.createdAt };
}
return Groups.find(selector, { fields: Groups.publicFieldsWhenFindAll, sort: { createdAt: -1 }, limit: GROUPS_PER_PAGE });
return Groups.find(selector, { fields: Groups.publicFields.findAll, sort: { createdAt: -1 }, limit: GROUPS_PER_PAGE });
});

Meteor.publish('groups.findOne', function (id) { // Do not use arrow function here
Meteor.publish('groups.findOne', id => {
check(id, String);
return Groups.find({ _id: id }, { fields: Groups.publicFieldsWhenFindOne });
return Groups.find({ _id: id }, { fields: Groups.publicFields.findOne });
});
17 changes: 11 additions & 6 deletions imports/startup/server/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@ import { Groups } from '../../api/groups/groups.jsx';
Meteor.startup(() => {
// Code to run on server at startup
if (Meteor.users.find().count() == 0) {
for (const i of Array(15).keys()) {
for (const i of Array(30).keys()) {
Accounts.createUser({ username: `player${i + 1}`, password: 'password' });
}
}
if (Groups.find().count() == 0) {
let j = 1;
const players = [];
for (const i of Array(10).keys()) {
let c = 1;
let players = [];
for (const i of Array(30).keys()) {
const user = Meteor.users.findOne({ username: `player${i + 1}` });
Groups.update(Groups.insert({ ownerId: user._id, name: `group${j++}` }), { $push: { players: { $each: players } } });
players.push({ id: user._id, role: 'Undecided' });
const groupsCount = Groups.find().count();
if (i + 1 < (groupsCount + 1) * (groupsCount + 2)) {
players.push({ id: user._id, role: 'Undecided' });
} else {
Groups.update(Groups.insert({ ownerId: user._id, name: `group${c++}` }), { $push: { players: { $each: players.reverse() } } });
players = [];
}
}
}
});
12 changes: 6 additions & 6 deletions imports/ui/components/group_row.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import { Groups } from '../../api/groups/groups.jsx'; // Constants only
import { join, leave } from '../../api/groups/methods.js';
import { MediumAndSmallDevice, TinyDevice } from '../layouts/devices.jsx';

Expand All @@ -13,9 +12,9 @@ export default class GroupRow extends React.Component {
// REGION: Component Specifications

render() {
const { group } = this.props;
const { group, joinedOtherGroup } = this.props;
const situation = group.getSituation();
const joined = group.hasPlayer(Meteor.userId());
const joinedThisGroup = group.hasPlayer(Meteor.userId());
const isPlaying = group.isPlaying();
return (
<tr>
Expand Down Expand Up @@ -61,9 +60,9 @@ export default class GroupRow extends React.Component {
<td>
<a href={`/groups/${group._id}`} className="btn btn-sm btn-dark"><i className="fa fa-group"></i> Go to</a>
{
!!Meteor.userId() && (joined || situation.slot != false) && !isPlaying ?
!joined && !isPlaying ?
<a className="btn btn-sm btn-success" onClick={this.joinGroup}><i className="fa fa-sign-in"></i> Join</a> :
!!Meteor.userId() && (joinedThisGroup || situation.slot != false) && !isPlaying ?
!joinedThisGroup && !isPlaying ?
!joinedOtherGroup ? <a className="btn btn-sm btn-success" onClick={this.joinGroup}><i className="fa fa-sign-in"></i> Join</a> : null :
<a className="btn btn-sm btn-danger" onClick={this.leaveGroup}><i className="fa fa-sign-out"></i> Leave</a> : null
}
</td>
Expand Down Expand Up @@ -106,6 +105,7 @@ export default class GroupRow extends React.Component {

GroupRow.propTypes = {
group: React.PropTypes.object,
joinedOtherGroup: React.PropTypes.bool,
};

GroupRow.contextTypes = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import { Groups } from '../../api/groups/groups.jsx';
import GroupRow from './group_row.jsx';
import { MediumAndSmallDevice } from '../layouts/devices.jsx';

export default class GroupTable extends React.Component {
export default class GroupsTable extends React.Component {

// REGION: Component Specifications

Expand All @@ -20,14 +21,14 @@ export default class GroupTable extends React.Component {
</tr>
</thead>
<tbody>
{groups.map(g => <GroupRow group={g} key={g._id}/>)}
{groups.map(g => <GroupRow key={g._id} group={g} joinedOtherGroup={Groups.find({ 'players.id': Meteor.userId() }).count() > 0}/>)}
</tbody>
</table>
</div>
);
}
}

GroupTable.propTypes = {
GroupsTable.propTypes = {
groups: React.PropTypes.array,
};
Loading

0 comments on commit 969ccda

Please sign in to comment.