Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option polls #2431

Merged
merged 8 commits into from
Feb 12, 2019
17 changes: 17 additions & 0 deletions Shared/LobbyClient/Protocol/Messages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -824,4 +824,21 @@ public class UserAward
public string PwBombers { get; set;}
public string PwWarpcores { get; set; }
}


[Message(Origin.Server)]
public class BattlePoll
{
public class PollOption
{
public string Name { get; set; }
public int Id { get; set; }
public int Votes { get; set; }
}

public string Topic { get; set; } //Null if there is no poll
public List<PollOption> Options { get; set; } //Null if there is no poll
public int VotesToWin { get; set; } //If any single option receives this many votes, it will win instantly. -1 if there is no poll
public bool DefaultPoll { get; set; } //Whether this is a simple yes/no vote, with yes being the first option
}
}
96 changes: 79 additions & 17 deletions ZkLobbyServer/ServerBattle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class ServerBattle : Battle
public const int PollTimeout = 60;
public const int DiscussionTime = 35;
public const int MapVoteTime = 25;
public const int NumberOfMapChoices = 4;
public const int MinimumAutostartPlayers = 8;
public static int BattleCounter;

Expand Down Expand Up @@ -261,7 +262,8 @@ public virtual async Task ProcessPlayerJoin(ConnectedUser user, string joinPassw
Bots = Bots.Values.Select(x => x.ToUpdateBotStatus()).ToList(),
Options = ModOptions
});


if (ActivePoll != null) await user.SendCommand(ActivePoll.GetBattlePoll());

await server.Broadcast(Users.Keys.Where(x => x != user.Name), ubs.ToUpdateBattleStatus()); // send my UBS to others in battle

Expand Down Expand Up @@ -307,16 +309,13 @@ public async Task RecalcSpectators()
}


public async Task RegisterVote(Say e, bool vote)
public async Task RegisterVote(Say e, int vote)
{
if (ActivePoll != null)
{
if (await ActivePoll.Vote(e, vote))
{
var oldPoll = ActivePoll;
pollTimer.Enabled = false;
ActivePoll = null;
oldPoll.PublishResult();
StopVote();
}
}
else await Respond(e, "There is no poll going on, start some first");
Expand Down Expand Up @@ -475,30 +474,68 @@ public async Task<bool> StartGame()
return true;
}

public async Task StartVote(BattleCommand command, Say e, string args, int timeout = PollTimeout, CommandPoll poll = null)
public async Task<bool> StartVote(BattleCommand cmd, Say e, string args, int timeout = PollTimeout, CommandPoll poll = null)
{
if (ActivePoll != null)
cmd = cmd.Create();

string topic = cmd.Arm(this, e, args);
if (topic == null) return false;
Func<string, string> selector = cmd.GetIneligibilityReasonFunc(this);
if (e != null && selector(e.User) != null) return false;
var options = new List<PollOption>();
options.Add(new PollOption()
{
await Respond(e, $"Please wait, another poll already in progress: {ActivePoll.question}");
return;
Name = "Yes",
Action = async () =>
{
if (cmd.Access == BattleCommand.AccessType.NotIngame && spring.IsRunning) return;
if (cmd.Access == BattleCommand.AccessType.Ingame && !spring.IsRunning) return;
await cmd.ExecuteArmed(this, e);
}
});
options.Add(new PollOption()
{
Name = "No",
Action = async () => { }
});

if (await StartVote(selector, options, e, topic, poll: new CommandPoll(this, true, true)))
{
await RegisterVote(e, 1);
return true;
}
if (poll == null) poll = new CommandPoll(this);
if (await poll.Setup(command, e, args))
return false;
}

public async Task<bool> StartVote(Func<string, string> eligibilitySelector, List<PollOption> options, Say creator, string topic, int timeout = PollTimeout, CommandPoll poll = null)
{
if (ActivePoll != null)
{
ActivePoll = poll;
pollTimer.Interval = timeout * 1000;
pollTimer.Enabled = true;
await Respond(creator, $"Please wait, another poll already in progress: {ActivePoll.Topic}");
return false;
}
if (poll == null) poll = new CommandPoll(this);
await poll.Setup(eligibilitySelector, options, creator, topic);
ActivePoll = poll;
pollTimer.Interval = timeout * 1000;
pollTimer.Enabled = true;
return true;
}


public async void StopVote(Say e = null)
public async void StopVote()
{
var oldPoll = ActivePoll;
if (ActivePoll != null) await ActivePoll.End();
if (pollTimer != null) pollTimer.Enabled = false;
ActivePoll = null;
oldPoll.PublishResult();
await server.Broadcast(Users.Keys, new BattlePoll()
{
Options = null,
Topic = null,
VotesToWin = -1
});
}

public async Task SwitchEngine(string engine)
Expand Down Expand Up @@ -762,7 +799,32 @@ private void discussionTimer_Elapsed(object sender, ElapsedEventArgs e)
discussionTimer.Stop();
var poll = new CommandPoll(this, false);
poll.PollEnded += MapVoteEnded;
StartVote(new CmdMap(), null, "", MapVoteTime, poll);
var options = new List<PollOption>();
for (int i = 0; i < NumberOfMapChoices; i++)
{
Resource map;
if (i < NumberOfMapChoices / 2 && MinimalMapSupportLevel < MapSupportLevel.Featured)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be simpler

Suggested change
if (i < NumberOfMapChoices / 2 && MinimalMapSupportLevel < MapSupportLevel.Featured)
if (i < NumberOfMapChoices / 2)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A server could be set to only allow Matchmaker maps. Imagine a 1v1 training server for example. In this case he should not show any featured maps.

{
map = MapPicker.GetRecommendedMap(GetContext(), MapSupportLevel.Featured); //choose at least 50% featured maps
}
else
{
map = MapPicker.GetRecommendedMap(GetContext(), (MinimalMapSupportLevel < MapSupportLevel.Featured) ? MapSupportLevel.Supported : MinimalMapSupportLevel);
}
options.Add(new PollOption()
{
Name = map.InternalName,
Action = async () =>
{
var cmd = new CmdMap().Create();
cmd.Arm(this, null, map.ResourceID.ToString());
if (cmd.Access == BattleCommand.AccessType.NotIngame && spring.IsRunning) return;
if (cmd.Access == BattleCommand.AccessType.Ingame && !spring.IsRunning) return;
await cmd.ExecuteArmed(this, null);
}
});
}
StartVote(new CmdMap().GetIneligibilityReasonFunc(this), options, null, "Choose the next map", MapVoteTime, poll);
}

private void MapVoteEnded(object sender, PollOutcome e)
Expand Down
113 changes: 70 additions & 43 deletions ZkLobbyServer/autohost/CommandPoll.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,65 +10,88 @@ public class CommandPoll
{
private int winCount;
private ServerBattle battle;
private Dictionary<string, bool> userVotes = new Dictionary<string, bool>();
private Dictionary<string, int> userVotes = new Dictionary<string, int>(); //stores votes, zero indexed
private Func<string, string> EligiblitySelector; //return null if player is allowed to vote, otherwise reason
private bool AbsoluteMajorityVote; //if set to yes, at least N/2 players need to vote for an option to be selected. Otherwise the option with the majority of votes wins
private bool DefaultPoll; //if set to yes, there must be only two options being yes and no.

public bool Ended { get; private set; } = false;
public string question { get; private set; }
public string Topic { get; private set; }
public List<PollOption> Options;
public Say Creator { get; private set; }
private BattleCommand command;
private bool AbsoluteMajorityVote;
public PollOutcome Outcome { get; private set; }

public event EventHandler<PollOutcome> PollEnded;

public CommandPoll(ServerBattle battle, bool absoluteMajorityVote = true)
public CommandPoll(ServerBattle battle, bool absoluteMajorityVote = true, bool defaultPoll = false)
{
this.battle = battle;
this.AbsoluteMajorityVote = absoluteMajorityVote;
DefaultPoll = defaultPoll;
}

public async Task<bool> Setup(BattleCommand cmd, Say e, string args)
public async Task Setup(Func<string, string> eligibilitySelector, List<PollOption> options, Say creator, string Topic)
{
command = cmd.Create();
EligiblitySelector = eligibilitySelector;
Options = options;
Creator = creator;


Creator = e;
question = command.Arm(battle, e, args);
if (question == null) return false;
string ignored;
winCount = battle.Users.Values.Count(x => command.GetRunPermissions(battle, x.Name, out ignored) >= BattleCommand.RunPermission.Vote && !cmd.IsSpectator(battle, x.Name, x)) / 2 + 1;
winCount = battle.Users.Values.Count(x => EligiblitySelector(x.Name) == null) / 2 + 1;
if (winCount <= 0) winCount = 1;

if (!await Vote(e, true))
{
if (e == null) await battle.SayBattle($"Poll: {question} [!y=0/{winCount}, !n=0/{winCount}]");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, this can't be removed because in-game needs it.

}
else
{
return false;
}
await battle.server.Broadcast(battle.Users.Keys, GetBattlePoll());
}

return true;
public BattlePoll GetBattlePoll()
{
return new BattlePoll()
{
Options = Options.Select((o, i) => new BattlePoll.PollOption()
{
Id = i + 1,
Name = o.Name,
Votes = userVotes.Count(x => x.Value == i)
}).ToList(),
Topic = Topic,
VotesToWin = winCount,
DefaultPoll = DefaultPoll
};
}

private async Task<bool> CheckEnd(bool timeout)
{
var yes = userVotes.Count(x => x.Value == true);
var no = userVotes.Count(x => x.Value == false);

if (yes >= winCount || timeout && !AbsoluteMajorityVote && yes > no)
List<int> votes = Options.Select((o, i) => userVotes.Count(x => x.Value == i)).ToList();
var winnerId = votes.IndexOf(votes.Max());

if (votes[winnerId] >= winCount || timeout && !AbsoluteMajorityVote)
{
Ended = true;
await battle.SayBattle($"Poll: {question} [END:SUCCESS]");
if (command.Access == BattleCommand.AccessType.NotIngame && battle.spring.IsRunning) return true;
if (command.Access == BattleCommand.AccessType.Ingame && !battle.spring.IsRunning) return true;
await command.ExecuteArmed(battle, Creator);
Outcome = new PollOutcome() { Success = true };
if (DefaultPoll)
{
await battle.SayBattle($"Poll: {Topic} [END:SUCCESS]");
}
else
{
await battle.SayBattle($"Option Poll: {Topic} [END: Selected {Options[winnerId].Name}]");
}
await Options[winnerId].Action();
Outcome = new PollOutcome() { ChosenOption = Options[winnerId] };
return true;
}
else if (no >= winCount || timeout)
else if (timeout)
{
Ended = true;
await battle.SayBattle($"Poll: {question} [END:FAILED]");
Outcome = new PollOutcome() { Success = false };
if (DefaultPoll)
{
await battle.SayBattle($"Poll: {Topic} [END:FAILED]");
}
else
{
await battle.SayBattle($"Option Poll: {Topic} [END: No option achieved absolute majority]");
}
Outcome = new PollOutcome() { ChosenOption = null };
return true;
}
return false;
Expand All @@ -85,34 +108,38 @@ public async Task End()
}


public async Task<bool> Vote(Say e, bool vote)
//Vote for an option, one-indexed
public async Task<bool> Vote(Say e, int vote)
{
if (e == null) return false;
string reason;
if (command.GetRunPermissions(battle, e.User, out reason) >= BattleCommand.RunPermission.Vote && !Ended)
string ineligibilityReason = EligiblitySelector(e.User);
if (ineligibilityReason == null && !Ended)
{
if (command.IsSpectator(battle, e.User, null)) return false;

userVotes[e.User] = vote;
userVotes[e.User] = vote - 1;

var yes = userVotes.Count(x => x.Value == true);
var no = userVotes.Count(x => x.Value == false);
await battle.SayBattle(string.Format("Poll: {0} [!y={1}/{3}, !n={2}/{3}]", question, yes, no, winCount));
if (DefaultPoll) await battle.SayBattle(string.Format("Poll: {0} [!y={1}/{3}, !n={2}/{3}]", Topic, userVotes.Count(x => x.Value == 0), userVotes.Count(x => x.Value == 1), winCount));
await battle.server.Broadcast(battle.Users.Keys, GetBattlePoll());

if (await CheckEnd(false)) return true;
}
else
{
await battle.Respond(e, reason);
await battle.Respond(e, ineligibilityReason);
return false;
}
return false;
}
}

}
public class PollOption
{
public string Name;
public Func<Task> Action;
}

public class PollOutcome : EventArgs
{
public bool Success { get; set; }
public PollOption ChosenOption { get; set; }
}
}
15 changes: 13 additions & 2 deletions ZkLobbyServer/autohost/Commands/BattleCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
Expand Down Expand Up @@ -57,6 +58,16 @@ public async Task Run(ServerBattle battle, Say e, string arguments = null)
if (Arm(battle, e, arguments) != null) await ExecuteArmed(battle, e);
}

public Func<string, string> GetIneligibilityReasonFunc(ServerBattle battle)
{
return x =>
{
string reason;
if (GetRunPermissions(battle, x, out reason) >= BattleCommand.RunPermission.Vote && !IsSpectator(battle, x, battle.Users[x])) return null;
return reason;
};
}

public bool IsSpectator(ServerBattle battle, string userName, UserBattleStatus user)
{
if (user == null)
Expand Down Expand Up @@ -84,7 +95,7 @@ public bool IsSpectator(ServerBattle battle, string userName, UserBattleStatus u
/// <returns></returns>
public virtual RunPermission GetRunPermissions(ServerBattle battle, string userName, out string reason)
{
reason = "";
reason = "You can't use this command";
if (Access == AccessType.NoCheck) return RunPermission.Run;

User user = null;
Expand Down
4 changes: 2 additions & 2 deletions ZkLobbyServer/autohost/Commands/CmdEndvote.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ public override string Arm(ServerBattle battle, Say e, string arguments = null)

public override async Task ExecuteArmed(ServerBattle battle, Say e)
{
battle.StopVote(e);
battle.StopVote();
await battle.SayBattle("poll cancelled");
}

public override RunPermission GetRunPermissions(ServerBattle battle, string userName, out string reason)
{
reason = "";
reason = "You can't use this command";
if (battle.ActivePoll?.Creator?.User == userName) return RunPermission.Run; // can end own poll
var ret = base.GetRunPermissions(battle, userName, out reason);
if (ret == RunPermission.Vote) return RunPermission.None; // do not allow vote (ever)
Expand Down
Loading