The Problem with Votekick

The original, free Ace of Spades game powered by the Voxlap engine. Known as “Classic,” 0.75, 0.76, and all 0.x versions. Created by Ben Aksoy.
68 posts Page 4 of 5 First unread post

Should server owners reduce the amount of votes required to successfully pass a votekick?

38%
Yes
18
15%
No, it is fine as is
7
48%
No, it is a problem with the community, not the script
23

Total votes: 48

rakiru
Coder
Coder
Posts: 1349
Joined: Sun Nov 11, 2012 12:26 pm


Reki wrote:
That's true, we can fix it so that when it gets to all-but-1, any less than that stays as such.

We're interested in finding when x - (1/4x + 3) =< 1. This is easily seen as x being less than 6 players.
(While we're on this discussion, let's deal with fractions in our 1/4 formula by rounding them.)
A 6-player server will require 4 other people to vote yes.
A 5-or-fewer server will require everyone but the person being voted to vote yes.

Actually this is all a bit too complicated, maybe we should stick with raw percentages.
I could knock this up in a few minutes really, and I think it'd be far better than the current method.
rakiru
Coder
Coder
Posts: 1349
Joined: Sun Nov 11, 2012 12:26 pm


Ok, I haven't tested this as I currently lack the ability to, but this should work, and if it doesn't, then hopefully only a couple of small bugs need fixed:
Code: Select all
# This is based on the original votekick.py script as I don't have the
# luxury of a large server to test on, so I'll keep the same license.
# Oh, and since pyspades/pysnip is under the GPL, this is required to be
# under the same license, because it's cancerous like that. Anyway,
# original license below:

    # Copyright (c) James Hofmann 2012.

    # pyspades is free software: you can redistribute it and/or modify
    # it under the terms of the GNU General Public License as published by
    # the Free Software Foundation, either version 3 of the License, or
    # (at your option) any later version.

    # pyspades is distributed in the hope that it will be useful,
    # but WITHOUT ANY WARRANTY; without even the implied warranty of
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    # GNU General Public License for more details.

    # You should have received a copy of the GNU General Public License
    # along with pyspades.  If not, see <http://www.gnu.org/licenses/>.

# Modifications copyright (c) Sean Gordon 2013.
# Also, 80-char line length limits are stupid - are we living in the 80s?

from twisted.internet.reactor import seconds
from scheduler import Scheduler
from commands import name, add, get_player, InvalidPlayer

######################
# OPTIONS & MESSAGES #
######################
# Note: You should change these options in the config if possible. These are defaults!
O_REQUIRE_REASON = True
# If a player starts a bad votekick and the threshold (as defined below) of negative votes is met, kick that player
O_KICK_INSTIGATOR = True
# Broadcast votes in chat?
O_SHOW_VOTES = True
O_VOTE_DURATION = 2 * 60 # In seconds
O_VOTE_COOLDOWN = 3 * 60 # In seconds
O_BAN_TIME = 15 # In minutes, set to 0 to kick without banning
O_CHAT_UPDATE_TIME = 30 # In seconds
# Formula for calculating required votes: ROUND(MULTIPLIER * TOTAL_PLAYERS + CONSTANT) where ROUND() rounds to nearest whole number
O_FORMULA_MULTIPLIER = 1/4
O_FORMULA_CONSTANT = 3
# The count can be limited at TOTAL_PLAYERS - 1 to prevent impossible votekick scenarios (e.g. 3 players online, 4 votes required)
O_CAP_REQUIRED_VOTES = True
# Disallow voting if there are less than a certain number of players
O_MINIMUM_PLAYERS = 3

# The only messages you're likely to want to change, if any, are the first two
S_DEFAULT_REASON = "NO REASON GIVEN, BECAUSE I'M LAZY"
S_BAD_VOTEKICK_REASON = "INSTIGATED A BAD VOTEKICK"
S_NO_VOTEKICK = "No votekick in progress"
S_IN_PROGRESS = "Votekick already in progress"
S_SELF_VOTEKICK = "You can't votekick yourself"
S_IMMUNE = "This player can't be votekicked"
S_COOLING_DOWN = "You can't start another votekick yet"
S_REASON_REQUIRED = "You must provide a reason for the votekick"
S_NOT_ENOUGH_PLAYERS = "There aren't enough players to vote"
S_DID_NOT_START = "You didn't start this votekick"
S_VOTE_YES = "{player} voted YES"
S_VOTE_NO = "{player} voted NO"
S_ENDED = "Votekick for {target} has ended. {result}"
S_RESULT_TIMED_OUT = 'Votekick timed out'
S_RESULT_CANCELLED = 'Votekick cancelled'
S_RESULT_BANNED = "Banned by admin"
S_RESULT_KICKED = "Kicked by admin"
S_RESULT_INSTIGATOR_KICKED = "Instigator kicked by admin"
S_RESULT_TARGET_LEFT = "Target {target} left during votekick"
S_RESULT_INSTIGATOR_LEFT = "Instigator {instigator} left during votekick"
S_RESULT_INSTIGATOR_BANNED = "Instigator {instigator} kicked"
S_RESULT_TARGET_BANNED = 'Player {target} kicked'
S_ANNOUNCE = '{instigator} started a VOTEKICK against {target} - Vote by using /Y or /N'
S_ANNOUNCE_SELF = "You started a votekick against {target} - Say /CANCEL to stop it"
S_ANNOUNCE_IRC = "* {instigator} started a votekick against player {target} - Reason: {reason}"
S_UPDATE = '{instigator} is votekicking {target} - Vote by using /Y or /N..'
S_REASON = 'Reason: {reason}'


class VotekickException(Exception):
    pass

############
# COMMANDS #
############

@name('votekick')
def start_votekick(connection, *args):
    protocol = connection.protocol
    if connection not in protocol.players:
        raise KeyError()
    player = connection
    
    if not args:
        if protocol.votekick:
            # player requested votekick info
            protocol.votekick.send_chat_update(player)
            return
        else:
			raise ValueError()
    
    value = args[0]
    try:
        target = get_player(protocol, '#' + value)
    except InvalidPlayer:
        target = get_player(protocol, value)
    reason = " ".join(args[1:])
    
    try:
        votekick = Votekick.start(player, target, reason)
        protocol.votekick = votekick
    except VotekickException as err:
        return str(err)


@name('cancel')
def cancel_votekick(connection):
    protocol = connection.protocol
    votekick = protocol.votekick
    if not votekick:
        return S_DID_NOT_START
    if connection in protocol.players:
        player = connection
        if player is votekick.instigator or player.admin or player.rights.cancel:
			#TODO: Check if instigator is losing vote, so they don't cancel it just to stop themselves being kicked
			votekick.end(S_RESULT_CANCELLED)
        else:
            return S_CANT_CANCEL


@name('y')
def vote_yes(connection):
    protocol = connection.protocol
    if connection not in protocol.players:
        raise KeyError()
    player = connection
    
    votekick = protocol.votekick
    if not votekick:
        return S_NO_VOTEKICK
    else:
		votekick.vote(player, True)


@name('n')
def vote_no(connection):
    protocol = connection.protocol
    if connection not in protocol.players:
        raise KeyError()
    player = connection
    
    votekick = protocol.votekick
    if not votekick:
        return S_NO_VOTEKICK
    else:
		votekick.vote(player, False)


add(start_votekick)
add(cancel_votekick)
add(vote_yes)
add(vote_no)


class Votekick(object):
    schedule = None
    
    def _get_votes_remaining(self):
        return self.protocol.get_required_votes() - len([a for a,b in d.iteritems() if b]) + 1
    def _get_votes_remaining_no(self):
        return self.protocol.get_required_votes() - len([a for a,b in d.iteritems() if not b) + 1
    votes_remaining = property(_get_votes_remaining)
    votes_remaining_no = property(_get_votes_remaining_no)
    
    @classmethod
    def start(cls, instigator, target, reason = None):
        protocol = instigator.protocol
        last_votekick = instigator.last_votekick
        if reason then:
			reason = reason.strip()
			
        if protocol.votekick:
            raise VotekickException(S_IN_PROGRESS)
        elif instigator is target:
            raise VotekickException(S_SELF_VOTEKICK)
        elif not protocol.is_enough_players():
            raise VotekickException(S_NOT_ENOUGH_PLAYERS)
        elif target.admin or target.rights.cancel:
            raise VotekickException(S_IMMUNE)
        elif not instigator.admin and (last_votekick is not None and seconds() - last_votekick < cls.interval):
            raise VotekickException(S_COOLING_DOWN)
        elif self.require_reason and not reason:
            raise VotekickException(S_REASON_REQUIRED)
        
        result = protocol.on_votekick_start(instigator, target, reason)
        if result is not None:
            raise VotekickException(result)
        
        reason = reason or S_DEFAULT_REASON
        return cls(instigator, target, reason)
    
    def __init__(self, instigator, target, reason):
        self.protocol = protocol = instigator.protocol
        self.instigator = instigator
        self.target = target
        self.reason = reason
        self.votes = {instigator : True}
        self.ended = False
        
        protocol.irc_say(S_ANNOUNCE_IRC.format(instigator = instigator.name,
            target = target.name, reason = self.reason))
        protocol.send_chat(S_ANNOUNCE.format(instigator = instigator.name,
            target = target.name), sender = instigator)
        protocol.send_chat(S_REASON.format(reason = self.reason),
            sender = instigator)
        instigator.send_chat(S_ANNOUNCE_SELF.format(target = target.name))
        
        schedule = Scheduler(protocol)
        schedule.call_later(self.duration, self.end, S_RESULT_TIMED_OUT)
        schedule.loop_call(self.chat_update_time, self.send_chat_update)
        self.schedule = schedule
    
    def vote(self, player, vote):
        if self.target is player:
            return
        if self.public_votes:
            self.protocol.send_chat((S_VOTE_YES if vote else S_VOTE_NO).format(player = player.name))
        self.votes[player] = vote
        if self.votes_remaining <= 0:
            # vote passed, ban or kick accordingly
            target = self.target
            self.end(S_RESULT_TARGET_BANNED)
            print '%s votekicked' % target.name
            if self.ban_duration > 0.0:
                target.ban(self.reason, self.ban_duration)
            else:
                target.kick(silent = True)
        elif self.votes_remaining_no <= 0:
            # vote failed badly, ban or kick accordingly
            target = self.instigator
            self.end(S_RESULT_INSTIGATOR_BANNED
            print '%s votekicked' % target.name
            if self.ban_duration > 0.0:
                target.ban(S_BAD_VOTEKICK_REASON, self.ban_duration)
            else:
                target.kick(silent = True)
    
    def release(self):
        self.instigator = None
        self.target = None
        self.votes = None
        if self.schedule:
            self.schedule.reset()
        self.schedule = None
        self.protocol.votekick = None
    
    def end(self, result):
        self.ended = True
        message = S_ENDED.format(target = self.target.name, result = result)
        self.protocol.send_chat(message, irc = True)
        if not self.instigator.admin:
            self.instigator.last_votekick = seconds()
        self.protocol.on_votekick_end()
        self.release()
    
    def send_chat_update(self, target = None):
        # send only to target player if provided, otherwise broadcast to server
        target = target or self.protocol
        target.send_chat(S_UPDATE.format(instigator = self.instigator.name, target = self.target.name, needed = self.votes_remaining))
        target.send_chat(S_REASON.format(reason = self.reason))


def apply_script(protocol, connection, config):
	# Using the same config keys as the other version where possible
    Votekick.require_reason   = config.get('votekick_require_reason',     O_REQUIRE_REASON)
    Votekick.kick_instigator  = config.get('votekick_kick_instigator',    O_KICK_INSTIGATOR)
    Votekick.show_votes       = config.get('votekick_public_votes',       O_SHOW_VOTES)
    Votekick.vote_duration    = config.get('votekick_vote_duration',      O_VOTE_DURATION)
    Votekick.cooldown         = config.get('votekick_cooldown',           O_VOTE_COOLDOWN)
    Votekick.ban_duration     = config.get('votekick_ban_duration',       O_BAN_TIME)
    Votekick.chat_update_time = config.get('votekick_chat_update_time',   O_CHAT_UPDATE_TIME)
    formula_multiplier        = config.get('votekick_formula_multiplier', O_FORMULA_MULTIPLIER)
    formula_constant          = config.get('votekick_formula_constant',   O_FORMULA_CONSTANT)
    cap_required_votes        = config.get('votekick_cap_required_votes', O_CAP_REQUIRED_VOTES)
    minimum_players           = config.get('votekick_minimum_players',    O_MINIMUM_PLAYERS)
    
    class VotekickProtocol(protocol):
        votekick = None
        
        def get_player_count(self):
            return sum(not player.disconnected for player in self.players.itervalues())
        
        def is_enough_players(self):
            return self.get_player_count() >= self.minimum_players
        
        def get_required_votes(self):
			player_count = self.get_player_count
            threshold = self.formula_multiplier * player_count + self.formula_constant
            if self.cap_required_votes and threshold > player_count - 1:
				threshold = player_count - 1
        
        def on_map_leave(self):
            if self.votekick:
                self.votekick.release()
            protocol.on_map_leave(self)
        
        def on_ban(self, banee, reason, duration):
            votekick = self.votekick
            if votekick and votekick.target is self:
                votekick.end(S_RESULT_BANNED)
            protocol.on_ban(self, connection, reason, duration)
        
        def on_votekick_start(self, instigator, target, reason):
            pass
        
        def on_votekick_end(self):
            pass
    
    class VotekickConnection(connection):
        last_votekick = None
        
        def on_disconnect(self):
            votekick = self.protocol.votekick
            if votekick:
                if votekick.target is self:
                    # target leaves, gets votekick ban
                    reason = votekick.reason
                    votekick.end(S_RESULT_TARGET_LEFT.format(target = self.name))
                    self.ban(reason, Votekick.ban_duration)
                elif votekick.instigator is self:
                    # instigator leaves, votekick is called off
                    s = S_RESULT_INSTIGATOR_LEFT.format(instigator = self.name)
                    votekick.end(s)
                else:
                    # make sure we still have enough players
                    votekick.votes.pop(self, None)
                    if votekick.votes_remaining <= 0:
                        votekick.end(S_NOT_ENOUGH_PLAYERS)
            connection.on_disconnect(self)
        
        def kick(self, reason = None, silent = False):
            votekick = self.protocol.votekick
            if votekick:
                if votekick.target is self:
                    votekick.end(S_RESULT_KICKED)
                elif votekick.instigator is self:
                    votekick.end(S_RESULT_INSTIGATOR_KICKED)
            connection.kick(self, reason, silent)
    
    return VotekickProtocol, VotekickConnection
Edit: It's also possible that I've missed something crucial and not realised.
Triplefox
Scripter
Scripter
Posts: 32
Joined: Thu Nov 22, 2012 5:28 am


Personally, I've concluded that votekick is a very complicated placebo. Even in games with dedicated UI for it, people are too distracted to make a good decision, and as it's made more effective it becomes a larger target for abuse.

What can be done instead? Have the game instantly elect temporary moderators - as many as we think is safe at any given time, each granted limited powers to do basic moderation tasks. The computer can create a heuristic for who is most likely to do a good job(if I were to implement it, I'd use time played with "real" admins/trusted), and a hierarchy based on that heuristic can be imposed to mitigate the possibility of takeovers. We can further supplement the system with a report/commend command that would give the admins some idea of temp-mod performance, and an "untrusted" blacklist if necessary.

I know this can work, because I've seen the model of "grant lots of powers" used in IRC very successfully. In that system a bot monitors everything and decides at its discretion to give ops, usually after someone has been visiting for many weeks. In general the channel is 70-80% ops at any one time. The last takeover attempt I can remember was about seven years ago, and drama is limited to personal issues.

This could also be further customized for each server, for example a bot quizzing the mod-elect on the server's rules before granting any powers.
jordan
Deuced Up
Posts: 97
Joined: Thu Jan 03, 2013 12:45 pm


I thought about it more, maybe to make it more effective a small pop-up should come up in like the top left corner of the screen and you could just press yes or no.
Sasquatch
Mapper
Mapper
Posts: 141
Joined: Wed Nov 14, 2012 5:18 pm


rakiru wrote:
jdrew wrote:
If you want get a better script then make one
That doesn't really work if you don't run a server and the server owners don't know about it or want/use it.
I just kinda made a thread about this. This is why coming together to make a universal decision is so critical. Sure, people get kicked for improper reasons sometimes, but that doesn't happen nearly as often as people hacking and griefing get away with what they do. And yes, the community is in part to blame for this, but note that a lot of players are kids, or foreigners who don't speak english, and tend to be oblivious to votekicking, or the chat entirely. And at any rate, if someone gets kicked for 24 hours (the way it should be), then there are still lots of other servers to play on, and the odds of being kicked twice on accident is pretty low. Now if you're a hacker, pretty much you're objective is going to be to ruin as many servers as possible, but once you've exhausted all of them, now you're left without places to play after that. I will never understand why people are conservative with kicking. It should not be discreet and should be for a long time. Period.
rakiru
Coder
Coder
Posts: 1349
Joined: Sun Nov 11, 2012 12:26 pm


Triplefox wrote:
Personally, I've concluded that votekick is a very complicated placebo. Even in games with dedicated UI for it, people are too distracted to make a good decision, and as it's made more effective it becomes a larger target for abuse.

What can be done instead? Have the game instantly elect temporary moderators - as many as we think is safe at any given time, each granted limited powers to do basic moderation tasks. The computer can create a heuristic for who is most likely to do a good job(if I were to implement it, I'd use time played with "real" admins/trusted), and a hierarchy based on that heuristic can be imposed to mitigate the possibility of takeovers. We can further supplement the system with a report/commend command that would give the admins some idea of temp-mod performance, and an "untrusted" blacklist if necessary.

I know this can work, because I've seen the model of "grant lots of powers" used in IRC very successfully. In that system a bot monitors everything and decides at its discretion to give ops, usually after someone has been visiting for many weeks. In general the channel is 70-80% ops at any one time. The last takeover attempt I can remember was about seven years ago, and drama is limited to personal issues.

This could also be further customized for each server, for example a bot quizzing the mod-elect on the server's rules before granting any powers.
That sounds rather dangerous really, and there's no way to ensure the people selected would even user it (they could even miss the fact they have the power all together, since the only way to alert them is via a small chat box).

If you've seen it done before, I'll trust you that it can work, but I'm curious about the theme of the IRC channel you mentioned - how do the general users of it compare to the young, foreign, hacker-filled AoS players?
GreaseMonkey
Coder
Coder
Posts: 733
Joined: Tue Oct 30, 2012 11:07 pm


The "op party" trick works really well on IRC. However, this is because these channels are run by the sorts of people who go on IRC. I'm not sure how well it'll work with the sorts of people who play Ace of Spades.

Imagine someone in {EPIC} having tempmod powers. *shudder*
Bigcheecho
Build and Shoot's 1st Birthday
Build and Shoot's 1st Birthday
Posts: 582
Joined: Sat Dec 22, 2012 3:43 pm


Reki wrote:
Here are some ideas I proposed in a post ages ago (though revamped with examples this time!)

1) The threshold for successfully passing a kick shouldn't just be a fixed percentage of the total server population. Instead, add a constant term to create a curve.

Example: Someone (not me) proposed 1/4[Servertotal]+3.
4 people on: 1+3 = 4 votes needed (for such small populations, having everyone vote isn't hard)
8 people on: 2+3 = 5 votes needed (still small, quite easy to achieve majority)
20 people on: 5+3 = 8 votes needed (server's moderately filled, a bit too hard to go for 1/2)
32 people on: 8+3 = 11 votes needed (about 1/3 of the server)
Reki wrote:
That's true, we can fix it so that when it gets to all-but-1, any less than that stays as such.

We're interested in finding when x - (1/4x + 3) =< 1. This is easily seen as x being less than 6 players.
(While we're on this discussion, let's deal with fractions in our 1/4 formula by rounding them.)
A 6-player server will require 4 other people to vote yes.
A 5-or-fewer server will require everyone but the person being voted to vote yes.
I made a graph for your equation, designed for x=2-32. Click it to use zoom and all that fun stuff.
  • x=players online
    y=votes required

    red line=actual value with decimal
    black line=rounded value with Reki's rules of 5 or less
Image
Last edited by Bigcheecho on Mon Jan 07, 2013 3:05 pm, edited 8 times in total.
xytor
Deuce
Posts: 5
Joined: Sun Jan 06, 2013 11:58 pm


Votekick is a double-edged sword. I've never actually been kicked, but occasionally people accused me of hacking if I infiltrated an enemy base and headshot everybody who spawned. It's extremely easy to get headshots in this game... The only thing that saved me from being kicked is the high vote number.
GreaseMonkey
Coder
Coder
Posts: 733
Joined: Tue Oct 30, 2012 11:07 pm


xytor wrote:
Votekick is a double-edged sword. I've never actually been kicked, but occasionally people accused me of hacking if I infiltrated an enemy base and headshot everybody who spawned. It's extremely easy to get headshots in this game... The only thing that saved me from being kicked is the high vote number.
Is it safe to say we need to bring back /n?
Sasquatch
Mapper
Mapper
Posts: 141
Joined: Wed Nov 14, 2012 5:18 pm


GreaseMonkey wrote:
Is it safe to say we need to bring back /n?
Sasquatch wrote:
...Sure, people get kicked for improper reasons sometimes, but that doesn't happen nearly as often as people hacking and griefing get away with what they do. And yes, the community is in part to blame for this, but note that a lot of players are kids, or foreigners who don't speak english, and tend to be oblivious to votekicking, or the chat entirely. And at any rate, if someone gets kicked for 24 hours (the way it should be), then there are still lots of other servers to play on, and the odds of being kicked twice on accident is pretty low. Now if you're a hacker, pretty much you're objective is going to be to ruin as many servers as possible, but once you've exhausted all of them, now you're left without places to play after that. I will never understand why people are conservative with kicking. It should not be discreet and should be for a long time. Period.
Honestly you cannot appeal to every case you hear where someone is kicked on accident. I've almost never experienced accusations of hacking and when I am accused, if you literally just say nothing instead of arguing, you're chances or being kicked become much lower. At that point most people will just check the death logs to see if your name has popped up four times in the last second with headshots, and if it hasn't, you're usually safe. Than and if you're not hacking the lack of getting snipped from outside of the fog range makes people tend to vote less.
rakiru
Coder
Coder
Posts: 1349
Joined: Sun Nov 11, 2012 12:26 pm


Bigcheecho wrote:
Oh, and Reki, YOU ROUNDED 4.5 WRONG. Just saying.
That depends on how you're rounding.
Bigcheecho
Build and Shoot's 1st Birthday
Build and Shoot's 1st Birthday
Posts: 582
Joined: Sat Dec 22, 2012 3:43 pm


rakiru wrote:
Bigcheecho wrote:
Oh, and Reki, YOU ROUNDED 4.5 WRONG. Just saying.
That depends on how you're rounding.
Oh, OK. I was taught that >4 meant to round up. Sorry then, Reki.
rakiru
Coder
Coder
Posts: 1349
Joined: Sun Nov 11, 2012 12:26 pm


Bigcheecho wrote:
rakiru wrote:
Bigcheecho wrote:
Oh, and Reki, YOU ROUNDED 4.5 WRONG. Just saying.
That depends on how you're rounding.
Oh, OK. I was taught that >4 meant to round up. Sorry then, Reki.
The usual way is >= .5 is up, although you can also take the ceiling (> 0) or floor (< 1) of the number too.
Bigcheecho
Build and Shoot's 1st Birthday
Build and Shoot's 1st Birthday
Posts: 582
Joined: Sat Dec 22, 2012 3:43 pm


rakiru wrote:
Bigcheecho wrote:
Oh, OK. I was taught that >4 meant to round up. Sorry then, Reki.
The usual way is >= .5 is up, although you can also take the ceiling (> 0) or floor (< 1) of the number too.
And I assume that's how it's programmed on most scripting languages, which is fine with me.
Thank god this didn't turn into a flame war about the rules of rounding, or then Dan would have to lock this thread, and I wouldn't be able to make more mathematical graphs for other suggested mathematical solutions for votekicking. Blue_Happy1
68 posts Page 4 of 5 First unread post
Return to “Ace of Spades 0.x Discussion”

Who is online

Users browsing this forum: No registered users and 32 guests