Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions src/model/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from random import shuffle
from threading import RLock
from time import time
from nussschale.nussschale import nconfig

from model.multideck import MultiDeck
from nussschale.util.locks import mutex, named_mutex
Expand All @@ -51,6 +52,8 @@ class Match:
Class Attributes:
frozen (bool): Whether matches are currently frozen, i.e. whether their
state transitions are disabled.
afk_limit (int): Maximum number of rounds a participant can spend AFK
before they are kicked from the game.
"""

# The minimum amount of players for a match
Expand Down Expand Up @@ -94,6 +97,9 @@ class Match:
# Whether matches are currently frozen
frozen = False

# Limit on number of AFK rounds before kicking players
afk_limit = 2

@classmethod
@named_mutex("_pool_lock")
def get_by_id(cls, id):
Expand Down Expand Up @@ -228,6 +234,9 @@ def __init__(self):
# The chat of this match, tuples with type/message
self._chat = [("SYSTEM", "<b>Match was created.</b>")]

# The limit on the number of AFK rounds before kicking players
self.afk_limit = nconfig().get("afk-limit", 2)

def put_in_pool(self):
"""Puts this match into the match pool."""
Match.add_match(self.id, self)
Expand Down Expand Up @@ -412,6 +421,12 @@ def _enter_state(self):
self._timer = time() + pick_time
elif self._state == "COOLDOWN":
self._timer = time() + Match._TIMER_COOLDOWN
# Kick AFK players for doing nothing for two rounds
participants = list(self.get_participants(False))[:]
for part in participants:
if part.afkCount >= self.afk_limit:
self.abandon_participant(part.id,
"was kicked for being AFK for two rounds.")
elif self._state == "ENDING":
self._timer = time() + Match._TIMER_ENDING

Expand Down Expand Up @@ -457,6 +472,14 @@ def check_timer(self):
elif self._state == "CHOOSING":
self._set_state("PICKING")
elif self._state == "PICKING":
# If no winner was picked, mark picker as AFK
picker = None
for part in self.get_participants(False):
if part.picking:
picker = part
break
assert picker is not None
picker.increase_AFK()
self._chat.append(("SYSTEM",
"<b>No winner was picked!</b>"))
self._set_state("COOLDOWN")
Expand Down Expand Up @@ -489,11 +512,13 @@ def check_participants(self):
del self._participants[pid]

@mutex
def abandon_participant(self, pid):
def abandon_participant(self, pid, message="left."):
"""Removes the given participant from the match.

Args:
pid (str): The ID of the participant.
message (str): The message to send when the user leaves without
the nickname. Defaults to "left."

Contract:
This method locks the match's instance lock and the participant's
Expand All @@ -503,7 +528,7 @@ def abandon_participant(self, pid):
return
nick = self._participants[pid].nickname
self._chat.append(("SYSTEM",
"<b>%s left.</b>" % nick))
"<b>%s %s</b>" % (nick, message)))
if self._participants[pid].picking:
self.notify_picker_leave(pid)
del self._participants[pid]
Expand Down Expand Up @@ -805,11 +830,11 @@ def retrieve_chat(self, offset=0):
return res

@mutex
def send_message(self, nick, msg):
def send_message(self, part, msg):
"""Sends a user message to the chat of this match.

Args:
nick (str): The nickname of the user.
part (Participant): The user who sent the message.
msg (str): The message that is sent to the chat.

Contract:
Expand All @@ -818,7 +843,8 @@ def send_message(self, nick, msg):
msg = re.sub("(https?://\\S+)",
"<a href=\"\\1\" target=\"_blank\">\\1</a>",
msg)
self._chat.append(("USER", "<b>%s</b>: %s" % (nick, msg)))
self._chat.append(("USER", "<b>%s</b>: %s" % (part.nickname, msg)))
part.reset_AFK()

@mutex
def declare_round_winner(self, order):
Expand All @@ -836,6 +862,8 @@ def declare_round_winner(self, order):
winner = None
for part in self.get_participants(False):
if part.picking or part.choose_count() < gc:
if part.picking:
part.reset_AFK()
continue
if part.order == order:
winner = part
Expand Down Expand Up @@ -891,11 +919,13 @@ def _pick_possible(self):
"""
n = 0
for part in self.get_participants(False):
# Find players who haven't played
if part.choose_count() > 0:
n += 1
if n == 2:
return True
return False
part.reset_AFK()
elif not part.picking:
part.increase_AFK()
return n >= 2

def _unchoose_incomplete(self):
"""Unchooses incomplete hands.
Expand Down
22 changes: 22 additions & 0 deletions src/model/participant.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class Participant:
occurring.
order: The order key of the particpant, used for shuffling.
spectator: Whether the participant is a spectator.
afkCount: The number of rounds the participant has spent AFK.
"""

# The number of hand cards per type
Expand All @@ -60,6 +61,9 @@ class Participant:
# The timeout timer after refreshing a participant, in seconds
_PARTICIPANT_REFRESH_TIMER = 15

# Rounds spent AFK
afkCount = 0

def __init__(self, id: str, nickname: str) -> None:
"""Constructor.

Expand Down Expand Up @@ -117,6 +121,24 @@ def increase_score(self) -> None:
assert not self.spectator, "Trying to increase score for spectator"
self.score += 1

@mutex
def increase_AFK(self) -> None:
"""Increases AFK count by one.

Contract:
This method locks the particpant's lock.
"""
self.afkCount += 1

@mutex
def reset_AFK(self) -> None:
"""Reset the particpant's AFK count to zero.

Contract:
This method locks the particpant's lock.
"""
self.afkCount = 0

def refresh(self) -> None:
"""Refreshes the timeout timer of this participant."""
# Locking is not needed here as access is atomic.
Expand Down
2 changes: 1 addition & 1 deletion src/pages/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ def api_chat_send(ctx: EndpointContext) -> None:
# Check the chat message for sanity
if 0 < len(msg) < 200:
# Send the message
match.send_message(part.nickname, msg)
match.send_message(part, msg)
ctx.json_ok()
else:
raise HTTPException.forbidden(True, "invalid size")
Expand Down