diff --git a/src/settings.py b/src/settings.py index 27a8d3f..bfe782b 100644 --- a/src/settings.py +++ b/src/settings.py @@ -80,6 +80,9 @@ STATS_TYPE = "default" # default/accurate/team/disabled - what role information LOVER_WINS_WITH_FOOL = False # if fool is lynched, does their lover win with them? DEFAULT_SEEN_AS_VILL = True # non-wolves are seen as villager regardless of the default role +START_VOTES_SCALE = 0.3 +START_VOTES_MAX = 4 + # Debug mode settings, whether or not timers and stasis should apply during debug mode DISABLE_DEBUG_MODE_TIMERS = True DISABLE_DEBUG_MODE_TIME_LORD = False diff --git a/src/wolfgame.py b/src/wolfgame.py index 79182e2..64cf074 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -439,6 +439,7 @@ def reset(): var.NO_LYNCH = set() var.FGAMED = False var.GAMEMODE_VOTES = {} #list of players who have used !game + var.START_VOTES = set() # list of players who have voted to !start var.LOVERS = {} # need to be here for purposes of random reset_settings() @@ -1164,7 +1165,7 @@ def join_player(cli, player, chan, who = None, forced = False): var.JOINED_THIS_GAME_ACCS.add(acc) var.CAN_START_TIME = datetime.now() + timedelta(seconds=var.MINIMUM_WAIT) cli.msg(chan, ('\u0002{0}\u0002 has started a game of Werewolf. '+ - 'Type "{1}join" to join. Type "{1}start" to start the game. '+ + 'Type "{1}join" to join. Type "{1}start" to vote to start the game. '+ 'Type "{1}wait" to increase the start wait time.').format(player, botconfig.CMD_CHAR)) # Set join timer @@ -2185,6 +2186,11 @@ def show_votes(cli, nick, chan, rest): the_message = ", ".join(votelist) if len(pl) >= var.MIN_PLAYERS: the_message += "{0}Votes needed for a majority: {1}".format("; " if votelist else "", int(math.ceil(len(pl)/2))) + + with var.WARNING_LOCK: + if var.START_VOTES: + the_message += "; Votes to start the game: {} ({})".format(len(var.START_VOTES), ', '.join(var.START_VOTES)) + elif var.PHASE == "night": cli.notice(nick, "Voting is only during the day.") return @@ -2976,6 +2982,10 @@ def del_player(cli, nick, forced_death = False, devoice = True, end_game = True, if var.PHASE == "join": if nick in var.GAMEMODE_VOTES: del var.GAMEMODE_VOTES[nick] + + with var.WARNING_LOCK: + var.START_VOTES.discard(nick) + # Died during the joining process as a person if var.AUTO_TOGGLE_MODES and nick in var.USERS and var.USERS[nick]["moded"]: for newmode in var.USERS[nick]["moded"]: @@ -3497,6 +3507,10 @@ def rename_player(cli, prefix, nick): if var.PHASE == "join": if prefix in var.GAMEMODE_VOTES: var.GAMEMODE_VOTES[nick] = var.GAMEMODE_VOTES.pop(prefix) + with var.WARNING_LOCK: + if prefix in var.START_VOTES: + var.START_VOTES.discard(prefix) + var.START_VOTES.add(nick) # Check if player was disconnected if var.PHASE in ("night", "day"): @@ -3570,6 +3584,11 @@ def leave(cli, what, nick, why=""): if var.PHASE == "join": lpl = len(var.list_players()) - 1 + + if lpl < var.MIN_PLAYERS: + with var.WARNING_LOCK: + var.START_VOTES = set() + if lpl == 0: population = (" No more players remaining.") else: @@ -4962,7 +4981,7 @@ def check_exchange(cli, actor, nick): return True return False -@cmd("retract", "r", pm=True, phases=("day", "night")) +@cmd("retract", "r", pm=True, phases=("day", "night", "join")) def retract(cli, nick, chan, rest): """Takes back your vote during the day (for whom to lynch).""" @@ -4972,6 +4991,19 @@ def retract(cli, nick, chan, rest): cli.notice(nick, "You're not currently playing.") return + with var.GRAVEYARD_LOCK, var.WARNING_LOCK: + if var.PHASE == "join": + if not nick in var.START_VOTES: + cli.notice(nick, "You haven't voted to start.") + else: + var.START_VOTES.discard(nick) + cli.msg(chan, "\u0002{0}\u0002's vote to start was retracted.".format(nick)) + + if len(var.START_VOTES) < 1: + var.TIMERS['start_votes'][0].cancel() + del var.TIMERS['start_votes'] + return + if chan == nick: # PM, use different code role = var.get_role(nick) if role not in var.WOLF_ROLES - {"wolf cub"} and role != "hunter" and nick not in var.VENGEFUL_GHOSTS.keys(): @@ -6837,6 +6869,15 @@ def cgamemode(cli, arg): cli.msg(chan, "Mode \u0002{0}\u0002 not found.".format(modeargs[0])) +def expire_start_votes(cli, chan): + # Should never happen as the timer is removed on game start, but just to be safe + if var.PHASE != 'join': + return + + with var.WARNING_LOCK: + var.START_VOTES = set() + cli.msg(chan, "Not enough votes to start were accumulated in 1 minute, removing start votes.") + @cmd("start", phases=("join",)) def start_cmd(cli, nick, chan, rest): """Starts a game of Werewolf.""" @@ -6892,6 +6933,34 @@ def start(cli, nick, chan, forced = False, restart = ""): cli.msg(chan, "{0}: At most \u0002{1}\u0002 players may play.".format(nick, var.MAX_PLAYERS)) return + with var.WARNING_LOCK: + if not forced and nick in var.START_VOTES: + cli.notice(nick, "You have already voted to start the game.") + return + + start_votes_required = min(math.ceil(len(villagers) * var.START_VOTES_SCALE), var.START_VOTES_MAX) + if not forced and len(var.START_VOTES) < start_votes_required: + # If there's only one more vote required, start the game immediately. + # Checked here to make sure that a player that has already voted can't + # vote again for the final start. + if len(var.START_VOTES) < start_votes_required - 1: + var.START_VOTES.add(nick) + msg = "{0} has voted to \u0002start\u0002 the game. \u0002{1}\u0002 more {2} required." + remaining_votes = start_votes_required - len(var.START_VOTES) + + if remaining_votes == 1: + cli.msg(chan, msg.format(nick, remaining_votes, 'vote')) + else: + cli.msg(chan, msg.format(nick, remaining_votes, 'votes')) + + # If this was the first vote + if len(var.START_VOTES) == 1: + t = threading.Timer(60, expire_start_votes, (cli, chan)) + var.TIMERS["start_votes"] = (t, time.time(), 60) + t.daemon = True + t.start() + return + if not var.FGAMED: votes = {} #key = gamemode, not hostmask for gamemode in var.GAMEMODE_VOTES.values(): @@ -7077,7 +7146,7 @@ def start(cli, nick, chan, forced = False, restart = ""): var.SPECIAL_ROLES["goat herder"] = [ nick ] with var.WARNING_LOCK: # cancel timers - for name in ("join", "join_pinger"): + for name in ("join", "join_pinger", "start_votes"): if name in var.TIMERS: var.TIMERS[name][0].cancel() del var.TIMERS[name]