From 05114896505cdad254e1616f4fdff8fdd1639cc2 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Sat, 3 Feb 2018 20:08:46 +0800 Subject: [PATCH 01/54] Can customize phase skip permission Defaults to server owner Match phase skip method now takes nickname argument Follow 80 char line convention Fixed syntax error in match.js --- src/model/match.py | 29 ++++++++++++++++++++++++----- src/pages/api.py | 2 +- src/res/js/match/match.js | 1 + 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index ebf238f..a7fca67 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -37,6 +37,7 @@ 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 @@ -51,6 +52,7 @@ class Match: Class Attributes: frozen (bool): Whether matches are currently frozen, i.e. whether their state transitions are disabled. + skip_role (str): Which users are allowed to skip the current phase. """ # The minimum amount of players for a match @@ -94,6 +96,9 @@ class Match: # Whether matches are currently frozen frozen = False + # Which users are allowed to skip the phase + skip_role = "owner" + @classmethod @named_mutex("_pool_lock") def get_by_id(cls, id): @@ -228,6 +233,9 @@ def __init__(self): # The chat of this match, tuples with type/message self._chat = [("SYSTEM", "Match was created.")] + # Which users are allowed to skip the phase + self.skip_role = nconfig().get("skip-role", "owner") + def put_in_pool(self): """Puts this match into the match pool.""" Match.add_match(self.id, self) @@ -311,18 +319,29 @@ def user_can_skip_phase(self, nickname): bool: Whether the given nickname belongs to a user that can skip to the next phase """ - # Currently, only the owner can skip to the next phase - return self.get_owner_nick() == nickname + if self.skip_role == "picker": + for part in self.get_participants(False): + if part.picking and part.nickname == nickname: + return True + return False + elif self.skip_role == "anyone": + return True + else: + return self.get_owner_nick() == nickname - def skip_to_next_phase(self): + def skip_to_next_phase(self, nick): """Skips directly to the next phase + + Args: + nick (str): The nickname of the user who is skipping the phase. """ if int(self._timer - time()) > 1: self._timer = time() self._chat.append(("SYSTEM", - "" + self.get_owner_nick() + " skipped to next phase")) + "" + nick + " skipped to next phase")) else: - self._chat.append(("SYSTEM", "Can't skip phase with less than 1 second remaining")) + self._chat.append(("SYSTEM", + "Can't skip phase with less than 1 second remaining")) def _set_state(self, state): """Updates the state for this match. diff --git a/src/pages/api.py b/src/pages/api.py index 710fd92..3aa7fc1 100644 --- a/src/pages/api.py +++ b/src/pages/api.py @@ -316,7 +316,7 @@ def api_skip(ctx: EndpointContext) -> None: raise HTTPException.forbidden(True, "not authorized to skip phase") # Skip remaining time - match.skip_to_next_phase() + match.skip_to_next_phase(part.nickname) ctx.json_ok() diff --git a/src/res/js/match/match.js b/src/res/js/match/match.js index 10f1555..873636a 100644 --- a/src/res/js/match/match.js +++ b/src/res/js/match/match.js @@ -424,6 +424,7 @@ method: "POST", url: "/api/skip" }) + } /* * Toggle hand visibility. From 9a979eb50276ab09652981dadc9490c1f1ad5fbe Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Sat, 3 Feb 2018 20:30:41 +0800 Subject: [PATCH 02/54] Implemented majority vote skipping --- src/model/match.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/model/match.py b/src/model/match.py index a7fca67..1919e1b 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -53,6 +53,7 @@ class Match: frozen (bool): Whether matches are currently frozen, i.e. whether their state transitions are disabled. skip_role (str): Which users are allowed to skip the current phase. + skip_count (int): Number of people who want to skip the phase. """ # The minimum amount of players for a match @@ -98,6 +99,7 @@ class Match: # Which users are allowed to skip the phase skip_role = "owner" + skip_count = 0 @classmethod @named_mutex("_pool_lock") @@ -326,6 +328,14 @@ def user_can_skip_phase(self, nickname): return False elif self.skip_role == "anyone": return True + elif self.skip_role == "majority": + self.skip_count += 1 + majority = int(len(list(self.get_participants(False))) / 2) + if self.skip_count > majority: + self.skip_count = 0 + return True + else: + return False else: return self.get_owner_nick() == nickname From 59eb1250feb07393bd91c2faa81a9c7f438bda3b Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Sat, 3 Feb 2018 20:33:16 +0800 Subject: [PATCH 03/54] Anyone can skip if picker is chosen and phase is not "choosing" --- src/model/match.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index 1919e1b..e54f172 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -322,10 +322,13 @@ def user_can_skip_phase(self, nickname): can skip to the next phase """ if self.skip_role == "picker": - for part in self.get_participants(False): - if part.picking and part.nickname == nickname: - return True - return False + if self._state == "CHOOSING": + for part in self.get_participants(False): + if part.picking and part.nickname == nickname: + return True + return False + else: + return True elif self.skip_role == "anyone": return True elif self.skip_role == "majority": From 996150ab19995ca9dc226f3f129955dccb1cc92c Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 7 Feb 2018 09:14:14 +0800 Subject: [PATCH 04/54] Added chat message for majority skipping Indicates who wants to skip Indicates how many skips required to reach majority --- src/model/match.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/model/match.py b/src/model/match.py index e54f172..44c2e49 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -338,6 +338,11 @@ def user_can_skip_phase(self, nickname): self.skip_count = 0 return True else: + self._chat.append(("SYSTEM", + "" + nickname + + " wants to skip the phase. " + + str(majority - self.skip_count + 1) + + " request(s) left to reach majority.")) return False else: return self.get_owner_nick() == nickname From 87b6b5b9e107e3ea081ce526938cff4679348aa3 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 7 Feb 2018 09:27:57 +0800 Subject: [PATCH 05/54] Moved skip property to participant class Checking for majority now O(n) time Prevent same user from making multiple skip requests user_can_skip_phase now takes participant argument --- src/model/match.py | 21 ++++++++++++--------- src/model/participant.py | 4 ++++ src/pages/api.py | 2 +- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index 44c2e49..2101262 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -53,7 +53,6 @@ class Match: frozen (bool): Whether matches are currently frozen, i.e. whether their state transitions are disabled. skip_role (str): Which users are allowed to skip the current phase. - skip_count (int): Number of people who want to skip the phase. """ # The minimum amount of players for a match @@ -99,7 +98,6 @@ class Match: # Which users are allowed to skip the phase skip_role = "owner" - skip_count = 0 @classmethod @named_mutex("_pool_lock") @@ -314,7 +312,7 @@ def get_seconds_to_next_phase(self): # Locking is not needed here as access is atomic. return int(self._timer - time()) - def user_can_skip_phase(self, nickname): + def user_can_skip_phase(self, participant): """Determine whether a user can skip to the next phase Returns: @@ -332,20 +330,25 @@ def user_can_skip_phase(self, nickname): elif self.skip_role == "anyone": return True elif self.skip_role == "majority": - self.skip_count += 1 + particpant.wants_skip = True + skip_count = 0 + for part in self.get_participants(False): + if part.wants_skip: + skip_count += 1 majority = int(len(list(self.get_participants(False))) / 2) - if self.skip_count > majority: - self.skip_count = 0 + if skip_count > majority: + for part in self.get_participants(False): + part.wants_skip = False return True else: self._chat.append(("SYSTEM", - "" + nickname + + "" + participant.nickname + " wants to skip the phase. " + - str(majority - self.skip_count + 1) + + str(majority - skip_count + 1) + " request(s) left to reach majority.")) return False else: - return self.get_owner_nick() == nickname + return self.get_owner_nick() == participant.nickname def skip_to_next_phase(self, nick): """Skips directly to the next phase diff --git a/src/model/participant.py b/src/model/participant.py index 0346a34..e29b3d2 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -52,6 +52,7 @@ class Participant: occurring. order: The order key of the particpant, used for shuffling. spectator: Whether the participant is a spectator. + wants_skip: Whether the participant wants to skip the phase. """ # The number of hand cards per type @@ -94,6 +95,9 @@ def __init__(self, id: str, nickname: str) -> None: # participant is part of a match. self.spectator = False + # Whether the particpant wants to skip the phase. + self.wants_skip = False + # The hand of this participant self._hand = OrderedDict() # type: Dict[int, HandCard] self._hand_counter = 1 diff --git a/src/pages/api.py b/src/pages/api.py index 3aa7fc1..44be9ef 100644 --- a/src/pages/api.py +++ b/src/pages/api.py @@ -312,7 +312,7 @@ def api_skip(ctx: EndpointContext) -> None: part = match.get_participant(ctx.session["id"]) # Check that the POST request was made by a user that can skip phases - if not match.user_can_skip_phase(part.nickname): + if not match.user_can_skip_phase(part): raise HTTPException.forbidden(True, "not authorized to skip phase") # Skip remaining time From 94afb18805e166abd4d7a4f5a2177194edfb1a62 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 7 Feb 2018 09:32:00 +0800 Subject: [PATCH 06/54] Fixed typo --- src/model/match.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/match.py b/src/model/match.py index 2101262..f49b595 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -330,7 +330,7 @@ def user_can_skip_phase(self, participant): elif self.skip_role == "anyone": return True elif self.skip_role == "majority": - particpant.wants_skip = True + participant.wants_skip = True skip_count = 0 for part in self.get_participants(False): if part.wants_skip: From d35d1c7f27865171a8a38984afebf8afa2862f0c Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Thu, 8 Feb 2018 08:24:41 +0800 Subject: [PATCH 07/54] Fixed user_can_skip_phase for 'picker' Fixed to use new function call Added argument to comment for user_can_skip_phase --- src/model/match.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index f49b595..c0b3c1b 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -315,16 +315,17 @@ def get_seconds_to_next_phase(self): def user_can_skip_phase(self, participant): """Determine whether a user can skip to the next phase + Args: + participant: The participant that made the request + to skip the phase + Returns: bool: Whether the given nickname belongs to a user that can skip to the next phase """ if self.skip_role == "picker": if self._state == "CHOOSING": - for part in self.get_participants(False): - if part.picking and part.nickname == nickname: - return True - return False + return participant.picking else: return True elif self.skip_role == "anyone": From 4fb70fd19d96b7012c91a121e06fc00393cfecf7 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 08:58:22 +0800 Subject: [PATCH 08/54] Added wild card count selector POST request handler can also return numbers If parameter doesn't have a filename, assume it's the wild card counter instead --- src/nussschale/handler.py | 17 ++++++++++------- src/res/tpl/dashboard.html | 2 ++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/nussschale/handler.py b/src/nussschale/handler.py index aafbb74..401e507 100644 --- a/src/nussschale/handler.py +++ b/src/nussschale/handler.py @@ -325,13 +325,16 @@ def _unwrap_param(self, allow_list: bool, param: _RawPOSTParam assert isinstance(param.value, str) return param.value else: - fp = param.file - fp.seek(0, 2) - size = fp.tell() - fp.seek(0) - nlog().log("File upload: '%s', %i bytes" % (param.filename, - size)) - return IOWrapper(param.file, param) + if param.filename == None: + return param.value + else : + fp = param.file + fp.seek(0, 2) + size = fp.tell() + fp.seek(0) + nlog().log("File upload: '%s', %i bytes" % (param.filename, + size)) + return IOWrapper(param.file, param) else: raise ValueError("Unsupported parameter type") diff --git a/src/res/tpl/dashboard.html b/src/res/tpl/dashboard.html index d242cb7..720c5e5 100644 --- a/src/res/tpl/dashboard.html +++ b/src/res/tpl/dashboard.html @@ -66,6 +66,8 @@
+ Number of blank cards: + Choose a deck... (Maximum file size for decks is 800kB) From 65813a4e0924da3b532c63ada769ca667a0ac956 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 09:01:13 +0800 Subject: [PATCH 09/54] Added wild card count properties to Match Get wild card count from POST request --- src/model/match.py | 4 ++++ src/pages/match.py | 1 + 2 files changed, 5 insertions(+) diff --git a/src/model/match.py b/src/model/match.py index c0b3c1b..51b3e0d 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -99,6 +99,10 @@ class Match: # Which users are allowed to skip the phase skip_role = "owner" + # Number of wild cards in the match + wild_card_count = 0 + wild_cards_played = 0 + @classmethod @named_mutex("_pool_lock") def get_by_id(cls, id): diff --git a/src/pages/match.py b/src/pages/match.py index c9aab54..02f4b9c 100644 --- a/src/pages/match.py +++ b/src/pages/match.py @@ -124,6 +124,7 @@ def create_match(ctx: EndpointContext) -> None: # Create a new match match = Match() + match.wild_card_count = ctx.get_param_as("wildcards", int) # Create the deck from the upload try: From c3d5351cdec3d36ff9ee0cfa3ee255bf674853c3 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 11:51:20 +0800 Subject: [PATCH 10/54] Implemented prototype wild card drawing algorithm --- src/model/match.py | 14 +++++++++++--- src/model/multideck.py | 10 ++++++++-- src/model/participant.py | 14 +++++++++++--- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index 51b3e0d..37f8c60 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -99,9 +99,10 @@ class Match: # Which users are allowed to skip the phase skip_role = "owner" - # Number of wild cards in the match + # Wild card data wild_card_count = 0 - wild_cards_played = 0 + wilds_in_play = 0 + total_cards = 0 @classmethod @named_mutex("_pool_lock") @@ -714,6 +715,12 @@ def create_deck(self, data): # Create multidecks for type in self._deck: self._multidecks[type] = MultiDeck[Card, int](self._deck[type]) + self.total_cards += len(self._deck[type]) + + wilds = [Card(card_id_counter + 1 + i, "WILD", "") + for i in range(self.wild_card_count)] + self._multidecks["WILD"] = MultiDeck[Card, int](wilds) + self.total_cards += self.wild_card_count return True, "OK" @@ -954,7 +961,8 @@ def _replenish_hands(self): method. """ for part in self.get_participants(False): - part.replenish_hand(self._multidecks) + part.replenish_hand(self._multidecks, self.wilds_in_play, + self.total_cards) def _select_match_card(self): """Selects a random statement card for this match. diff --git a/src/model/multideck.py b/src/model/multideck.py index c3ee747..1c0fb82 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -25,7 +25,7 @@ requested. Thus the multideck lock can not be part of any deadlock. """ -from random import shuffle +from random import shuffle, randint from threading import RLock from typing import Generic, List, Optional, Set, TypeVar, cast @@ -68,11 +68,14 @@ def _id_of(o: T) -> U: return cast(U, id) @mutex - def request(self, banned_ids: Set[U]) -> Optional[T]: + def request(self, banned_ids: Set[U], wilds = None, wilds_in_play = 0, + cards_left = 0, banned_wilds: Set[U] = None) -> Optional[T]: """Requests a card from the multideck. Args: banned_ids: A set of IDs that may not be chosen. + wilds: The multideck with the wild cards. Defaults to None + wilds_in_play: The number of wild cards currently in players' hands Returns: The object that was selected. This might be None if the @@ -81,6 +84,9 @@ def request(self, banned_ids: Set[U]) -> Optional[T]: Contract: This method locks the deck's lock. """ + if wilds != None: + if randint(1, cards_left) <= len(wilds._backing) - wilds_in_play: + return wilds.request(banned_wilds) ptr = 0 # Try to find a viable object diff --git a/src/model/participant.py b/src/model/participant.py index e29b3d2..fccdf37 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -183,18 +183,25 @@ def choose_count(self) -> int: return n @mutex - def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"] - ) -> None: + def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], + wilds_in_play, cards_left) -> None: """Replenishes the hand of this participant from the given decks. Args: mdecks: Maps card type to a multideck of the card type. + wilds_in_play: Number of wild cards currently in players' hands Contract: This method locks the participant's lock. """ assert not self.spectator, "Trying to replenish spectator" + # Find any wild cards already held + banned_wilds = set() + for hcard in self._hand.values(): + if hcard.card.type == "WILD": + banned_wilds.add(hcard.card.id) + # Replenish for every type for type in filter(lambda x: x != "STATEMENT", mdecks): # Count cards of that type and fetch IDs @@ -207,7 +214,8 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"] # Fill hand to limit for i in range(Participant._HAND_CARDS_PER_TYPE - k_in_hand): - pick = mdecks[type].request(ids_banned) + pick = mdecks[type].request(ids_banned, mdecks["WILD"], + wilds_in_play, cards_left) if pick is None: break # Can't fulfill the requirement... ids_banned.add(pick.id) From 997a334a19aa654cdf95ce6448ebba0a35cfced5 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 12:06:33 +0800 Subject: [PATCH 11/54] Fixed wild card drawing Added WILD type to API --- src/model/participant.py | 3 ++- src/pages/api.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/model/participant.py b/src/model/participant.py index fccdf37..9a397ea 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -215,7 +215,8 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], # Fill hand to limit for i in range(Participant._HAND_CARDS_PER_TYPE - k_in_hand): pick = mdecks[type].request(ids_banned, mdecks["WILD"], - wilds_in_play, cards_left) + wilds_in_play, cards_left, + banned_wilds) if pick is None: break # Can't fulfill the requirement... ids_banned.add(pick.id) diff --git a/src/pages/api.py b/src/pages/api.py index 44be9ef..ad633ff 100644 --- a/src/pages/api.py +++ b/src/pages/api.py @@ -200,7 +200,8 @@ def api_cards(ctx: EndpointContext) -> None: if not part.spectator: hand_cards = { "OBJECT": {}, - "VERB": {} + "VERB": {}, + "WILD": {} } # type: Dict[str, Dict] hand = part.get_hand() for id, hcard in hand.items(): From 77141b775191eec9c4069d4f8d99c1d01cd905fc Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 14:54:43 +0800 Subject: [PATCH 12/54] Added wild cards to UI (Card set needs resizing) No longer try to replenish wild cards --- src/model/participant.py | 2 +- src/res/js/match/match.js | 14 ++++++++++++-- src/res/tpl/match.html | 6 ++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/model/participant.py b/src/model/participant.py index 9a397ea..7a8c3e5 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -203,7 +203,7 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], banned_wilds.add(hcard.card.id) # Replenish for every type - for type in filter(lambda x: x != "STATEMENT", mdecks): + for type in filter(lambda x: x != "STATEMENT" and x != "WILD", mdecks): # Count cards of that type and fetch IDs k_in_hand = 0 ids_banned = set() # type: Set[int] diff --git a/src/res/js/match/match.js b/src/res/js/match/match.js index 873636a..c72ac97 100644 --- a/src/res/js/match/match.js +++ b/src/res/js/match/match.js @@ -35,7 +35,8 @@ let externalUpdateAllowed = true let handResolver = new Map([ ["OBJECT", new Map()], - ["VERB", new Map()] + ["VERB", new Map()], + ["WILD", new Map()] ]) let numSelected = 0 let selectedCards = new Map() @@ -130,7 +131,8 @@ function updateCards(data) { let sentHand = new Map([ ["OBJECT", new Map()], - ["VERB", new Map()] + ["VERB", new Map()], + ["WILD", new Map()] ]) if (data.hasOwnProperty("hand")) { for (let type of sentHand.keys()) { @@ -416,6 +418,13 @@ pickTab("tab-objects") } + /** + * Chooses the wild cards tab. + */ + function chooseWildsTab() { + pickTab("tab-wilds") + } + /** * Sends POST request to skip the remaining time */ @@ -455,6 +464,7 @@ pickTab("tab-actions") $("#tab-actions").click(chooseActionsTab) $("#tab-objects").click(chooseObjectsTab) + $("#tab-wilds").click(chooseWildsTab) $("#skip-button").click(skipTime) $("#toggle-hand-button").click(toggleHand) $("#toggle-chat-button").click(toggleChat) diff --git a/src/res/tpl/match.html b/src/res/tpl/match.html index 7b11e2d..ffd5c85 100644 --- a/src/res/tpl/match.html +++ b/src/res/tpl/match.html @@ -69,6 +69,9 @@
Objects
+
+ Wild cards +
@@ -77,6 +80,9 @@
No cards on your hand.
+
+
No cards on your hand.
+
From 57f45701885e36273733c1441b6f5cdff9fae721 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 17:19:20 +0800 Subject: [PATCH 13/54] Can now send wild cards Server side API can change card text Changed to `is not None` for more Python-y-ness and consistency --- src/model/multideck.py | 2 +- src/model/participant.py | 13 +++++++++++++ src/pages/api.py | 4 ++++ src/res/js/match/match.js | 9 ++++++++- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/model/multideck.py b/src/model/multideck.py index 1c0fb82..0782d84 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -84,7 +84,7 @@ def request(self, banned_ids: Set[U], wilds = None, wilds_in_play = 0, Contract: This method locks the deck's lock. """ - if wilds != None: + if wilds is not None: if randint(1, cards_left) <= len(wilds._backing) - wilds_in_play: return wilds.request(banned_wilds) ptr = 0 diff --git a/src/model/participant.py b/src/model/participant.py index 7a8c3e5..d3387f1 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -260,6 +260,19 @@ def toggle_chosen(self, handid: int, allowance: int) -> None: and other_hcard.chosen >= k): other_hcard.chosen = None + @mutex + def set_card_text(self, handid, text): + """Changes the text on a given card. To be used for wild cards. + + Args: + handid: The ID of the hand card. + text: The new text for the card. + + Contract: + This method locks the participant's lock. + """ + self._hand[handid].card.text = text + @mutex def get_choose_data(self, redacted: bool ) -> List[Optional[Dict[str, Union[bool, str]]]]: diff --git a/src/pages/api.py b/src/pages/api.py index ad633ff..fed4395 100644 --- a/src/pages/api.py +++ b/src/pages/api.py @@ -170,6 +170,10 @@ def api_choose(ctx: EndpointContext) -> None: except ValueError: raise HTTPException.forbidden(True, "invalid id") + text = ctx.get_param_as("text", str) + if text != "": + part.set_card_text(handid, text) + part.toggle_chosen(handid, match.count_gaps()) # todo: check for wrong id match.check_choosing_done() ctx.json_ok() diff --git a/src/res/js/match/match.js b/src/res/js/match/match.js index c72ac97..b612ee9 100644 --- a/src/res/js/match/match.js +++ b/src/res/js/match/match.js @@ -351,10 +351,17 @@ if (!allowChoose) { return } + var data = { + "handId": id, + "text": "" + } + if (id in handResolver.get("WILD")) { + data["text"] = prompt("Enter card text:") + } $.ajax({ method: "POST", url: "/api/choose", - data: {"handId": id}, + data: data, success: () => updateSelection(id), error: (x, e, f) => console.log(`/api/choose error: ${e} ${f}`) }) From 541bd097bed17e11bd4ef8965a33d2908db1a9e3 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 17:31:41 +0800 Subject: [PATCH 14/54] No longer ask for custom text when deselecting Increased wild card limit to 100 --- src/res/js/match/match.js | 5 ++++- src/res/tpl/dashboard.html | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/res/js/match/match.js b/src/res/js/match/match.js index b612ee9..0fa507e 100644 --- a/src/res/js/match/match.js +++ b/src/res/js/match/match.js @@ -355,7 +355,10 @@ "handId": id, "text": "" } - if (id in handResolver.get("WILD")) { + // Ask user for custom card text if the card isn't already selected and + // it's a wild card. + if (id in handResolver.get("WILD") && + !handResolver.get("WILD").get(id).hasClass("card-selected")) { data["text"] = prompt("Enter card text:") } $.ajax({ diff --git a/src/res/tpl/dashboard.html b/src/res/tpl/dashboard.html index 720c5e5..8695bb2 100644 --- a/src/res/tpl/dashboard.html +++ b/src/res/tpl/dashboard.html @@ -67,7 +67,7 @@
Number of blank cards: - + Choose a deck... (Maximum file size for decks is 800kB) From 6cbebcfc7f2a7e2ecdefb3187cf341c8dd5a6f5d Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Sat, 31 Mar 2018 13:52:59 +0800 Subject: [PATCH 15/54] Added copyright --- src/nussschale/handler.py | 1 + src/pages/match.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/nussschale/handler.py b/src/nussschale/handler.py index 401e507..f8661a3 100644 --- a/src/nussschale/handler.py +++ b/src/nussschale/handler.py @@ -2,6 +2,7 @@ MIT License Copyright (c) 2017-2018 LordKorea +Copyright (c) 2018 Arc676/Alessandro Vinciguerra Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/src/pages/match.py b/src/pages/match.py index 02f4b9c..f0b7831 100644 --- a/src/pages/match.py +++ b/src/pages/match.py @@ -2,6 +2,7 @@ MIT License Copyright (c) 2017-2018 LordKorea +Copyright (c) 2018 Arc676/Alessandro Vinciguerra Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From 75471033532f5ebc3f98708aee8f5397edb3b858 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Mon, 2 Apr 2018 09:25:56 +0800 Subject: [PATCH 16/54] Better wild card tracking --- src/model/match.py | 3 +-- src/model/multideck.py | 10 ++++++---- src/model/participant.py | 8 ++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index 37f8c60..9dd7f97 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -101,7 +101,6 @@ class Match: # Wild card data wild_card_count = 0 - wilds_in_play = 0 total_cards = 0 @classmethod @@ -961,7 +960,7 @@ def _replenish_hands(self): method. """ for part in self.get_participants(False): - part.replenish_hand(self._multidecks, self.wilds_in_play, + part.replenish_hand(self._multidecks, self.total_cards) def _select_match_card(self): diff --git a/src/model/multideck.py b/src/model/multideck.py index 0782d84..58c6df4 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -2,6 +2,7 @@ MIT License Copyright (c) 2017-2018 LordKorea +Copyright (c) 2018 Arc676/Alessandro Vinciguerra Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -68,14 +69,15 @@ def _id_of(o: T) -> U: return cast(U, id) @mutex - def request(self, banned_ids: Set[U], wilds = None, wilds_in_play = 0, - cards_left = 0, banned_wilds: Set[U] = None) -> Optional[T]: + def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, + banned_wilds: Set[U] = None) -> Optional[T]: """Requests a card from the multideck. Args: banned_ids: A set of IDs that may not be chosen. wilds: The multideck with the wild cards. Defaults to None - wilds_in_play: The number of wild cards currently in players' hands + cards_left: The number of cards remaining to be drawn + banned_wilds: A set of IDs of wild cards that are aleady drawn Returns: The object that was selected. This might be None if the @@ -85,7 +87,7 @@ def request(self, banned_ids: Set[U], wilds = None, wilds_in_play = 0, This method locks the deck's lock. """ if wilds is not None: - if randint(1, cards_left) <= len(wilds._backing) - wilds_in_play: + if randint(1, cards_left) <= len(wilds._queue): return wilds.request(banned_wilds) ptr = 0 diff --git a/src/model/participant.py b/src/model/participant.py index d3387f1..fa760a3 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -2,6 +2,7 @@ MIT License Copyright (c) 2017-2018 LordKorea +Copyright (c) 2018 Arc676/Alessandro Vinciguerra Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -184,12 +185,12 @@ def choose_count(self) -> int: @mutex def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], - wilds_in_play, cards_left) -> None: + cards_left) -> None: """Replenishes the hand of this participant from the given decks. Args: mdecks: Maps card type to a multideck of the card type. - wilds_in_play: Number of wild cards currently in players' hands + cards_left: Number of cards remaining to be drawn Contract: This method locks the participant's lock. @@ -215,8 +216,7 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], # Fill hand to limit for i in range(Participant._HAND_CARDS_PER_TYPE - k_in_hand): pick = mdecks[type].request(ids_banned, mdecks["WILD"], - wilds_in_play, cards_left, - banned_wilds) + cards_left, banned_wilds) if pick is None: break # Can't fulfill the requirement... ids_banned.add(pick.id) From 072d1e2a99ddb7aef9c6a5351f374d5f5a5de4dd Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Mon, 2 Apr 2018 09:45:32 +0800 Subject: [PATCH 17/54] Refactored MultiDeck.request Moved queue building and picking to separate methods Build queue when creating MultiDeck --- src/model/multideck.py | 61 +++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/src/model/multideck.py b/src/model/multideck.py index 58c6df4..37c482a 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -51,6 +51,7 @@ def __init__(self, deck: List[T]) -> None: self._backing = deck # type: List[T] self._queue = [] # type: List[T] self._contained = set() # type: Set[U] + refillQueue() @staticmethod def _id_of(o: T) -> U: @@ -71,13 +72,15 @@ def _id_of(o: T) -> U: @mutex def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, banned_wilds: Set[U] = None) -> Optional[T]: - """Requests a card from the multideck. + """Requests a card from the multideck. The last three arguments are + optional and should be omitted when requesting a card from the wild + deck. Args: banned_ids: A set of IDs that may not be chosen. - wilds: The multideck with the wild cards. Defaults to None - cards_left: The number of cards remaining to be drawn - banned_wilds: A set of IDs of wild cards that are aleady drawn + wilds: The multideck with the wild cards. Defaults to None. + cards_left: The number of cards remaining to be drawn. + banned_wilds: A set of IDs of wild cards that are aleady drawn. Returns: The object that was selected. This might be None if the @@ -92,36 +95,50 @@ def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, ptr = 0 # Try to find a viable object - while ptr < len(self._queue): - obj = self._queue[ptr] - if MultiDeck._id_of(obj) not in banned_ids: - self._contained.remove(MultiDeck._id_of(obj)) - del self._queue[ptr] - return obj - ptr += 1 + card, ptr = pickFromQueue(ptr) + if card is not None: + return card # No object in the queue works... Need to refill queue! # Note: For backing decks that are only slightly bigger than the # set of banned ids this might result in less than optimal randomness. # However in practice, deck size does exceed the number of banned ids # by at least factor 2. - pool = [] - for obj in self._backing: - if MultiDeck._id_of(obj) not in self._contained: - pool.append(obj) - shuffle(pool) - for obj in pool: - self._contained.add(MultiDeck._id_of(obj)) - self._queue.append(obj) + refillQueue() # Try to find a viable object again + card, ptr = pickFromQueue(ptr) + + # Still no object found: Failure, as the queue is already maximal. + return card + + def pickFromQueue(ptr): + """Pick a card from the queue + + Args: + ptr: The index from which to start looking for viable cards + + Returns: + A tuple containing the selected card and the new value of ptr. The + card will be None if the request can't be fulfilled. + """ while ptr < len(self._queue): obj = self._queue[ptr] if MultiDeck._id_of(obj) not in banned_ids: self._contained.remove(MultiDeck._id_of(obj)) del self._queue[ptr] - return obj + return obj, ptr ptr += 1 + return None, ptr - # Still no object found: Failure, as the queue is already maximal. - return None + def refillQueue(): + """Refill the card queue + """ + pool = [] + for obj in self._backing: + if MultiDeck._id_of(obj) not in self._contained: + pool.append(obj) + shuffle(pool) + for obj in pool: + self._contained.add(MultiDeck._id_of(obj)) + self._queue.append(obj) From 81b52e7729427f424461e56d7852d15ad29495a2 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Mon, 2 Apr 2018 09:58:21 +0800 Subject: [PATCH 18/54] Fixed syntax and arguments --- src/model/multideck.py | 72 ++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/model/multideck.py b/src/model/multideck.py index 37c482a..6315d61 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -40,6 +40,39 @@ class MultiDeck(Generic[T, U]): """A (refilling) deck used to make selection seem more 'random'.""" + def pickFromQueue(self, ptr, banned_ids: Set[U]): + """Pick a card from the queue + + Args: + ptr: The index from which to start looking for viable cards + banned_ids: A set of IDs that may not be chosen. + + Returns: + A tuple containing the selected card and the new value of ptr. The + card will be None if the request can't be fulfilled. + """ + while ptr < len(self._queue): + obj = self._queue[ptr] + if MultiDeck._id_of(obj) not in banned_ids: + self._contained.remove(MultiDeck._id_of(obj)) + del self._queue[ptr] + return obj, ptr + ptr += 1 + return None, ptr + + def refillQueue(self): + """Refill the card queue + """ + pool = [] + for obj in self._backing: + if MultiDeck._id_of(obj) not in self._contained: + pool.append(obj) + shuffle(pool) + for obj in pool: + self._contained.add(MultiDeck._id_of(obj)) + self._queue.append(obj) + + def __init__(self, deck: List[T]) -> None: """Constructor. @@ -51,7 +84,7 @@ def __init__(self, deck: List[T]) -> None: self._backing = deck # type: List[T] self._queue = [] # type: List[T] self._contained = set() # type: Set[U] - refillQueue() + self.refillQueue() @staticmethod def _id_of(o: T) -> U: @@ -95,7 +128,7 @@ def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, ptr = 0 # Try to find a viable object - card, ptr = pickFromQueue(ptr) + card, ptr = self.pickFromQueue(ptr, banned_ids) if card is not None: return card @@ -104,41 +137,10 @@ def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, # set of banned ids this might result in less than optimal randomness. # However in practice, deck size does exceed the number of banned ids # by at least factor 2. - refillQueue() + self.refillQueue() # Try to find a viable object again - card, ptr = pickFromQueue(ptr) + card, ptr = self.pickFromQueue(ptr, banned_ids) # Still no object found: Failure, as the queue is already maximal. return card - - def pickFromQueue(ptr): - """Pick a card from the queue - - Args: - ptr: The index from which to start looking for viable cards - - Returns: - A tuple containing the selected card and the new value of ptr. The - card will be None if the request can't be fulfilled. - """ - while ptr < len(self._queue): - obj = self._queue[ptr] - if MultiDeck._id_of(obj) not in banned_ids: - self._contained.remove(MultiDeck._id_of(obj)) - del self._queue[ptr] - return obj, ptr - ptr += 1 - return None, ptr - - def refillQueue(): - """Refill the card queue - """ - pool = [] - for obj in self._backing: - if MultiDeck._id_of(obj) not in self._contained: - pool.append(obj) - shuffle(pool) - for obj in pool: - self._contained.add(MultiDeck._id_of(obj)) - self._queue.append(obj) From f2c685744cdbc8bb7c9f6785e30a9842692eb1c2 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Mon, 2 Apr 2018 10:23:38 +0800 Subject: [PATCH 19/54] Added wild cards to CSS Added default text to wild cards Now says "Wild card" instead of being empty --- src/model/match.py | 2 +- src/res/css/dark/deck.css | 12 +++++++++++- src/res/css/light/deck.css | 5 +++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index 9dd7f97..acfef47 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -716,7 +716,7 @@ def create_deck(self, data): self._multidecks[type] = MultiDeck[Card, int](self._deck[type]) self.total_cards += len(self._deck[type]) - wilds = [Card(card_id_counter + 1 + i, "WILD", "") + wilds = [Card(card_id_counter + 1 + i, "WILD", "Wild card") for i in range(self.wild_card_count)] self._multidecks["WILD"] = MultiDeck[Card, int](wilds) self.total_cards += self.wild_card_count diff --git a/src/res/css/dark/deck.css b/src/res/css/dark/deck.css index 71d77a4..63338f8 100644 --- a/src/res/css/dark/deck.css +++ b/src/res/css/dark/deck.css @@ -36,6 +36,16 @@ color: #441; } +.wild-card { + color: #001; + background-color: #BBB; +} + +.wild-card a, .wild-card a:link, .wild-card a:visited, .wild-card a:hover, + .wild-card a:active { + color: #441; +} + .knob-statement { background-color: #444; } @@ -51,4 +61,4 @@ .grey-card { filter: grayscale(50%); opacity: 0.5; -} \ No newline at end of file +} diff --git a/src/res/css/light/deck.css b/src/res/css/light/deck.css index 4df3bef..012ccd0 100644 --- a/src/res/css/light/deck.css +++ b/src/res/css/light/deck.css @@ -31,6 +31,11 @@ background-color: #DD9; } +.wild-card { + color: #111; + background-color: #DDD; +} + .knob-statement { background-color: #444; } From 790b5d9bc440ab12ffa2747cab286392e42a7340 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Mon, 2 Apr 2018 10:33:54 +0800 Subject: [PATCH 20/54] Fixed wild card choosing --- src/res/js/match/match.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/res/js/match/match.js b/src/res/js/match/match.js index 0fa507e..f0d7fff 100644 --- a/src/res/js/match/match.js +++ b/src/res/js/match/match.js @@ -357,7 +357,7 @@ } // Ask user for custom card text if the card isn't already selected and // it's a wild card. - if (id in handResolver.get("WILD") && + if (handResolver.get("WILD").has(id) && !handResolver.get("WILD").get(id).hasClass("card-selected")) { data["text"] = prompt("Enter card text:") } @@ -382,6 +382,7 @@ let remove = false let card = handResolver.get("OBJECT").get(id) card = card || handResolver.get("VERB").get(id) + card = card || handResolver.get("WILD").get(id) // Remove card choices for (let i = 0; i < 4; i++) { From 32d3ed50c46a6e4634ab24058e8bf9d4cdf165e4 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Thu, 5 Apr 2018 11:47:19 +0800 Subject: [PATCH 21/54] Track participant hand size for card count Added method for determining participant hand size Use participant hand size to determine size of available card pool Probability of drawing wild card depends on size of card pool Particpant.replenish_hand now returns number of cards drawn --- src/model/match.py | 7 +++++-- src/model/multideck.py | 3 ++- src/model/participant.py | 22 +++++++++++++++++++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index acfef47..52fdc20 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -959,9 +959,12 @@ def _replenish_hands(self): The caller ensures that the match's lock is held when calling this method. """ + card_count = self.total_cards for part in self.get_participants(False): - part.replenish_hand(self._multidecks, - self.total_cards) + card_count -= part.get_card_count() + for part in self.get_participants(False): + drawn = part.replenish_hand(self._multidecks, card_count) + card_count -= drawn def _select_match_card(self): """Selects a random statement card for this match. diff --git a/src/model/multideck.py b/src/model/multideck.py index 6315d61..2c18c0a 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -142,5 +142,6 @@ def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, # Try to find a viable object again card, ptr = self.pickFromQueue(ptr, banned_ids) - # Still no object found: Failure, as the queue is already maximal. + # If no object found: Failure, as the queue is already maximal. + # Otherwise, return the given card return card diff --git a/src/model/participant.py b/src/model/participant.py index fa760a3..f6d7340 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -166,6 +166,19 @@ def get_hand(self) -> Dict[int, "HandCard"]: assert not self.spectator, "Trying to get hand for spectator" return deepcopy(self._hand) + @mutex + def get_card_count(self) -> int: + """Determines the number of cards in the participant's hand. + + Returns: + The number of cards in the participant's hand. + + Contract: + This method locks the participant's lock. + """ + assert not self.spectator, "Trying to get hand for spectator" + return len(self._hand.items()) + @mutex def choose_count(self) -> int: """Retrieves the number of chosen cards in the hand of this player. @@ -185,17 +198,21 @@ def choose_count(self) -> int: @mutex def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], - cards_left) -> None: + cards_left) -> int: """Replenishes the hand of this participant from the given decks. Args: mdecks: Maps card type to a multideck of the card type. cards_left: Number of cards remaining to be drawn + Returns: + The number of cards drawn by the participant. + Contract: This method locks the participant's lock. """ assert not self.spectator, "Trying to replenish spectator" + cards_drawn = 0 # Find any wild cards already held banned_wilds = set() @@ -221,8 +238,11 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], break # Can't fulfill the requirement... ids_banned.add(pick.id) self._hand[self._hand_counter] = HandCard(pick) + cards_drawn += 1 self._hand_counter += 1 + return cards_drawn + @mutex def toggle_chosen(self, handid: int, allowance: int) -> None: """Toggles whether the hand card with the given ID is chosen. From 6b5d5b6b8d85d0f7fd394dc125b18a4819315773 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Thu, 5 Apr 2018 12:15:56 +0800 Subject: [PATCH 22/54] Fixed card drawing Draw fewer cards if wild cards in hand --- src/model/participant.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/model/participant.py b/src/model/participant.py index f6d7340..5cbddc4 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -31,6 +31,7 @@ from threading import RLock from time import time from typing import Dict, List, Mapping, Optional, Set, TYPE_CHECKING, Union +from math import floor, ceil from nussschale.util.locks import mutex @@ -216,9 +217,11 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], # Find any wild cards already held banned_wilds = set() + wilds_in_hand = 0 for hcard in self._hand.values(): if hcard.card.type == "WILD": banned_wilds.add(hcard.card.id) + wilds_in_hand += 1 # Replenish for every type for type in filter(lambda x: x != "STATEMENT" and x != "WILD", mdecks): @@ -230,6 +233,12 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], ids_banned.add(hcard.card.id) k_in_hand += 1 + # Reduce number of cards to draw if wild cards are in hand + if type == "OBJECT": + k_in_hand += floor(wilds_in_hand / 2) + else: + k_in_hand += ceil(wilds_in_hand / 2) + # Fill hand to limit for i in range(Participant._HAND_CARDS_PER_TYPE - k_in_hand): pick = mdecks[type].request(ids_banned, mdecks["WILD"], From 5188216ae8895f899996f18f5d3f9085e5ee01ee Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 11 Apr 2018 08:41:37 +0800 Subject: [PATCH 23/54] Defined return types for added methods --- src/model/multideck.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/model/multideck.py b/src/model/multideck.py index 2c18c0a..6d3b7c1 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -40,11 +40,11 @@ class MultiDeck(Generic[T, U]): """A (refilling) deck used to make selection seem more 'random'.""" - def pickFromQueue(self, ptr, banned_ids: Set[U]): + def pickFromQueue(self, ptr, banned_ids: Set[U]) -> Optional[T]: """Pick a card from the queue Args: - ptr: The index from which to start looking for viable cards + ptr: The index from which to start looking for viable cards. banned_ids: A set of IDs that may not be chosen. Returns: @@ -60,7 +60,7 @@ def pickFromQueue(self, ptr, banned_ids: Set[U]): ptr += 1 return None, ptr - def refillQueue(self): + def refillQueue(self) -> None: """Refill the card queue """ pool = [] From 0e5db051c5ee3ad2318669ba3def2cb8739fcab3 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 11 Apr 2018 08:55:58 +0800 Subject: [PATCH 24/54] Converted to underscore style method names --- src/model/multideck.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/model/multideck.py b/src/model/multideck.py index 6d3b7c1..b1ae432 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -40,7 +40,7 @@ class MultiDeck(Generic[T, U]): """A (refilling) deck used to make selection seem more 'random'.""" - def pickFromQueue(self, ptr, banned_ids: Set[U]) -> Optional[T]: + def pick_from_queue(self, ptr, banned_ids: Set[U]) -> Optional[T]: """Pick a card from the queue Args: @@ -60,7 +60,7 @@ def pickFromQueue(self, ptr, banned_ids: Set[U]) -> Optional[T]: ptr += 1 return None, ptr - def refillQueue(self) -> None: + def refill_queue(self) -> None: """Refill the card queue """ pool = [] @@ -84,7 +84,7 @@ def __init__(self, deck: List[T]) -> None: self._backing = deck # type: List[T] self._queue = [] # type: List[T] self._contained = set() # type: Set[U] - self.refillQueue() + self.refill_queue() @staticmethod def _id_of(o: T) -> U: @@ -128,7 +128,7 @@ def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, ptr = 0 # Try to find a viable object - card, ptr = self.pickFromQueue(ptr, banned_ids) + card, ptr = self.pick_from_queue(ptr, banned_ids) if card is not None: return card @@ -137,10 +137,10 @@ def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, # set of banned ids this might result in less than optimal randomness. # However in practice, deck size does exceed the number of banned ids # by at least factor 2. - self.refillQueue() + self.refill_queue() # Try to find a viable object again - card, ptr = self.pickFromQueue(ptr, banned_ids) + card, ptr = self.pick_from_queue(ptr, banned_ids) # If no object found: Failure, as the queue is already maximal. # Otherwise, return the given card From c8db551c92f5cefc3714cb030aaeb405937ce8ef Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 11 Apr 2018 08:46:34 +0800 Subject: [PATCH 25/54] Added method for adding a card to queue Can be used to put wild cards back --- src/model/multideck.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/model/multideck.py b/src/model/multideck.py index b1ae432..6a29014 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -40,7 +40,21 @@ class MultiDeck(Generic[T, U]): """A (refilling) deck used to make selection seem more 'random'.""" - def pick_from_queue(self, ptr, banned_ids: Set[U]) -> Optional[T]: + @mutex + def putInQueue(self, obj): + """Put a card in the queue (used to allow wild cards to be redrawn + after being played for the first time, but also to build the queue) + + Args: + obj: The card to put in the queue. + + Contract: + This method locks the deck's lock. + """ + self._contained.add(MultiDeck._id_of(obj)) + self._queue.append(obj) + + def pickFromQueue(self, ptr, banned_ids: Set[U]) -> Optional[T]: """Pick a card from the queue Args: @@ -69,9 +83,7 @@ def refill_queue(self) -> None: pool.append(obj) shuffle(pool) for obj in pool: - self._contained.add(MultiDeck._id_of(obj)) - self._queue.append(obj) - + self.putInQueue(obj) def __init__(self, deck: List[T]) -> None: """Constructor. From 7bb1e44ad1a31441ae5df1f4d4fbfdc85cb005c7 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 11 Apr 2018 09:09:12 +0800 Subject: [PATCH 26/54] Return card list in delete_chosen Fixed underscore style method names in MultiDeck Added wild card replacement mode property to Math Wild card replacement determined by config Try to replace wild cards when deleting chosen cards --- src/model/match.py | 31 +++++++++++++++++++++++++++++-- src/model/multideck.py | 6 +++--- src/model/participant.py | 8 +++++++- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index 52fdc20..c309313 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -102,6 +102,7 @@ class Match: # Wild card data wild_card_count = 0 total_cards = 0 + wild_replace_mode = "no" @classmethod @named_mutex("_pool_lock") @@ -240,6 +241,9 @@ def __init__(self): # Which users are allowed to skip the phase self.skip_role = nconfig().get("skip-role", "owner") + # Whether wild cards should be replaced in the queue after being played + self.wild_replace_mode = nconfig().get("wild-replace-mode", "no") + def put_in_pool(self): """Puts this match into the match pool.""" Match.add_match(self.id, self) @@ -395,7 +399,8 @@ def _leave_state(self): if self._state == "COOLDOWN": # Delete all chosen cards from the hands for part in self.get_participants(False): - part.delete_chosen() + cards = part.delete_chosen() + self._replace_wild_cards(cards) def _enter_state(self): """Handles a transition into the current state. @@ -892,7 +897,8 @@ def declare_round_winner(self, order): else: self._set_state("COOLDOWN") else: - part.delete_chosen() + cards = part.delete_chosen() + self._replace_wild_cards(cards) @mutex def check_choosing_done(self): @@ -966,6 +972,27 @@ def _replenish_hands(self): drawn = part.replenish_hand(self._multidecks, card_count) card_count -= drawn + def _replace_wild_cards(self, cards): + """If the match is configured to do so, put any wild cards played during + the last round back in the MultiDeck's queue so they can be redrawn by + other players in future rounds. + + Args: + cards: A list of card objects played during the last round (the ones + deleted at the end of the last round). + + Contract: + The caller ensures that the match's lock is held when calling this + method. + """ + # If the match isn't configured to replace wild cards or there are no + # cards to put back, do nothing + if self.wild_replace_mode == "no" or len(cards) == 0: + return + for card in cards: + if card.type == "WILD": + self.mdecks["WILD"].putInQueue(card) + def _select_match_card(self): """Selects a random statement card for this match. diff --git a/src/model/multideck.py b/src/model/multideck.py index 6a29014..b338084 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -41,7 +41,7 @@ class MultiDeck(Generic[T, U]): """A (refilling) deck used to make selection seem more 'random'.""" @mutex - def putInQueue(self, obj): + def put_in_queue(self, obj): """Put a card in the queue (used to allow wild cards to be redrawn after being played for the first time, but also to build the queue) @@ -54,7 +54,7 @@ def putInQueue(self, obj): self._contained.add(MultiDeck._id_of(obj)) self._queue.append(obj) - def pickFromQueue(self, ptr, banned_ids: Set[U]) -> Optional[T]: + def pick_from_queue(self, ptr, banned_ids: Set[U]) -> Optional[T]: """Pick a card from the queue Args: @@ -83,7 +83,7 @@ def refill_queue(self) -> None: pool.append(obj) shuffle(pool) for obj in pool: - self.putInQueue(obj) + self.put_in_queue(obj) def __init__(self, deck: List[T]) -> None: """Constructor. diff --git a/src/model/participant.py b/src/model/participant.py index 5cbddc4..d5d3ce5 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -140,9 +140,13 @@ def unchoose_all(self) -> None: hcard.chosen = None @mutex - def delete_chosen(self) -> None: + def delete_chosen(self) -> List["Card"]: """Deletes all chosen hand cards from this participant. + Returns: + A list of deep-copied Card objects corresponding to the cards + removed from the participant's hand. + Contract: This method locks the participant's lock. """ @@ -151,7 +155,9 @@ def delete_chosen(self) -> None: for hid, hcard in self._hand.items(): if hcard.chosen is not None: del_list.append(hid) + cards = [] for hid in del_list: + cards.append(deepcopy(self._hand[hid].card)) del self._hand[hid] @mutex From 90c98442a45bd8a8439265f840f99eec7aac912e Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 11 Apr 2018 09:21:42 +0800 Subject: [PATCH 27/54] Fixed typos --- src/model/match.py | 2 +- src/model/participant.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/model/match.py b/src/model/match.py index c309313..7388db9 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -991,7 +991,7 @@ def _replace_wild_cards(self, cards): return for card in cards: if card.type == "WILD": - self.mdecks["WILD"].putInQueue(card) + self._multidecks["WILD"].put_in_queue(card) def _select_match_card(self): """Selects a random statement card for this match. diff --git a/src/model/participant.py b/src/model/participant.py index d5d3ce5..182dc63 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -159,6 +159,7 @@ def delete_chosen(self) -> List["Card"]: for hid in del_list: cards.append(deepcopy(self._hand[hid].card)) del self._hand[hid] + return cards @mutex def get_hand(self) -> Dict[int, "HandCard"]: From 8923b9c26c258b382f8d6523b19d7e78faf4a0a8 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Sat, 3 Feb 2018 20:08:46 +0800 Subject: [PATCH 28/54] Can customize phase skip permission Defaults to server owner Match phase skip method now takes nickname argument Follow 80 char line convention Fixed syntax error in match.js --- src/model/match.py | 31 +++++++++++++++++++++++-------- src/pages/api.py | 2 +- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index e884725..8f3f039 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -37,6 +37,7 @@ 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 @@ -51,6 +52,7 @@ class Match: Class Attributes: frozen (bool): Whether matches are currently frozen, i.e. whether their state transitions are disabled. + skip_role (str): Which users are allowed to skip the current phase. """ # The minimum amount of players for a match @@ -94,6 +96,9 @@ class Match: # Whether matches are currently frozen frozen = False + # Which users are allowed to skip the phase + skip_role = "owner" + @classmethod @named_mutex("_pool_lock") def get_by_id(cls, id): @@ -228,6 +233,9 @@ def __init__(self): # The chat of this match, tuples with type/message self._chat = [("SYSTEM", "Match was created.")] + # Which users are allowed to skip the phase + self.skip_role = nconfig().get("skip-role", "owner") + def put_in_pool(self): """Puts this match into the match pool.""" Match.add_match(self.id, self) @@ -314,6 +322,7 @@ def user_can_skip_phase(self, part): Returns: bool: Whether the given participant can skip to the next phase """ + # The match must not be ending if self._state == "ENDING": return False @@ -322,23 +331,29 @@ def user_can_skip_phase(self, part): if len(self._participants) < Match._MINIMUM_PLAYERS: return False - # Currently, only the owner can skip to the next phase - return self.get_owner_nick() == part.nickname + if self.skip_role == "picker": + for part in self.get_participants(False): + if part.picking and part.nickname == nickname: + return True + return False + elif self.skip_role == "anyone": + return True + else: + return self.get_owner_nick() == nickname @mutex - def skip_to_next_phase(self): - """Skips directly to the next phase. + def skip_to_next_phase(self, nick): + """Skips directly to the next phase - Contract: - This method locks the match's instance lock. + Args: + nick (str): The nickname of the user who is skipping the phase. """ # One second difference to prevent edge cases of timer change close to # game state transitions. if self._timer - time() > 1: self._timer = time() self._chat.append(("SYSTEM", - "" + self.get_owner_nick() - + " skipped to the next phase.")) + "" + nick + " skipped to next phase")) def _set_state(self, state): """Updates the state for this match. diff --git a/src/pages/api.py b/src/pages/api.py index c4678a4..c6f3178 100644 --- a/src/pages/api.py +++ b/src/pages/api.py @@ -316,7 +316,7 @@ def api_skip(ctx: EndpointContext) -> None: raise HTTPException.forbidden(True, "not authorized to skip phase") # Skip remaining time - match.skip_to_next_phase() + match.skip_to_next_phase(part.nickname) ctx.json_ok() From c5dbacfbc3422d52599d64e862cc7ed51d40e96f Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Sat, 3 Feb 2018 20:30:41 +0800 Subject: [PATCH 29/54] Implemented majority vote skipping --- src/model/match.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/model/match.py b/src/model/match.py index 8f3f039..dce1b22 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -53,6 +53,7 @@ class Match: frozen (bool): Whether matches are currently frozen, i.e. whether their state transitions are disabled. skip_role (str): Which users are allowed to skip the current phase. + skip_count (int): Number of people who want to skip the phase. """ # The minimum amount of players for a match @@ -98,6 +99,7 @@ class Match: # Which users are allowed to skip the phase skip_role = "owner" + skip_count = 0 @classmethod @named_mutex("_pool_lock") @@ -338,6 +340,14 @@ def user_can_skip_phase(self, part): return False elif self.skip_role == "anyone": return True + elif self.skip_role == "majority": + self.skip_count += 1 + majority = int(len(list(self.get_participants(False))) / 2) + if self.skip_count > majority: + self.skip_count = 0 + return True + else: + return False else: return self.get_owner_nick() == nickname From e2d516fda6294d442cba254efa773fdea98af9b9 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Sat, 3 Feb 2018 20:33:16 +0800 Subject: [PATCH 30/54] Anyone can skip if picker is chosen and phase is not "choosing" --- src/model/match.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index dce1b22..1d39ce4 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -334,10 +334,13 @@ def user_can_skip_phase(self, part): return False if self.skip_role == "picker": - for part in self.get_participants(False): - if part.picking and part.nickname == nickname: - return True - return False + if self._state == "CHOOSING": + for part in self.get_participants(False): + if part.picking and part.nickname == nickname: + return True + return False + else: + return True elif self.skip_role == "anyone": return True elif self.skip_role == "majority": From f447025a6b5bc7547e3f4fa61a5bdef6519ea8e8 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 7 Feb 2018 09:14:14 +0800 Subject: [PATCH 31/54] Added chat message for majority skipping Indicates who wants to skip Indicates how many skips required to reach majority --- src/model/match.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/model/match.py b/src/model/match.py index 1d39ce4..3858db3 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -350,6 +350,11 @@ def user_can_skip_phase(self, part): self.skip_count = 0 return True else: + self._chat.append(("SYSTEM", + "" + nickname + + " wants to skip the phase. " + + str(majority - self.skip_count + 1) + + " request(s) left to reach majority.")) return False else: return self.get_owner_nick() == nickname From ea8baebf772f4d6ab40351c24e0be2620d2f696e Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 7 Feb 2018 09:27:57 +0800 Subject: [PATCH 32/54] Moved skip property to participant class Checking for majority now O(n) time Prevent same user from making multiple skip requests user_can_skip_phase now takes participant argument --- src/model/match.py | 26 +++++++++++++------------- src/model/participant.py | 4 ++++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index 3858db3..850fef9 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -53,7 +53,6 @@ class Match: frozen (bool): Whether matches are currently frozen, i.e. whether their state transitions are disabled. skip_role (str): Which users are allowed to skip the current phase. - skip_count (int): Number of people who want to skip the phase. """ # The minimum amount of players for a match @@ -99,7 +98,6 @@ class Match: # Which users are allowed to skip the phase skip_role = "owner" - skip_count = 0 @classmethod @named_mutex("_pool_lock") @@ -315,11 +313,8 @@ def get_seconds_to_next_phase(self): return int(self._timer - time()) @mutex - def user_can_skip_phase(self, part): - """Determine whether a user can skip to the next phase. - - Args: - obj: The participant in question. + def user_can_skip_phase(self, participant): + """Determine whether a user can skip to the next phase Returns: bool: Whether the given participant can skip to the next phase @@ -344,20 +339,25 @@ def user_can_skip_phase(self, part): elif self.skip_role == "anyone": return True elif self.skip_role == "majority": - self.skip_count += 1 + particpant.wants_skip = True + skip_count = 0 + for part in self.get_participants(False): + if part.wants_skip: + skip_count += 1 majority = int(len(list(self.get_participants(False))) / 2) - if self.skip_count > majority: - self.skip_count = 0 + if skip_count > majority: + for part in self.get_participants(False): + part.wants_skip = False return True else: self._chat.append(("SYSTEM", - "" + nickname + + "" + participant.nickname + " wants to skip the phase. " + - str(majority - self.skip_count + 1) + + str(majority - skip_count + 1) + " request(s) left to reach majority.")) return False else: - return self.get_owner_nick() == nickname + return self.get_owner_nick() == participant.nickname @mutex def skip_to_next_phase(self, nick): diff --git a/src/model/participant.py b/src/model/participant.py index 0346a34..e29b3d2 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -52,6 +52,7 @@ class Participant: occurring. order: The order key of the particpant, used for shuffling. spectator: Whether the participant is a spectator. + wants_skip: Whether the participant wants to skip the phase. """ # The number of hand cards per type @@ -94,6 +95,9 @@ def __init__(self, id: str, nickname: str) -> None: # participant is part of a match. self.spectator = False + # Whether the particpant wants to skip the phase. + self.wants_skip = False + # The hand of this participant self._hand = OrderedDict() # type: Dict[int, HandCard] self._hand_counter = 1 From 0a9eddd97401a65eef0bbc5c3c4bef8b506f9f25 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 7 Feb 2018 09:32:00 +0800 Subject: [PATCH 33/54] Fixed typo --- src/model/match.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/match.py b/src/model/match.py index 850fef9..fd5ef02 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -339,7 +339,7 @@ def user_can_skip_phase(self, participant): elif self.skip_role == "anyone": return True elif self.skip_role == "majority": - particpant.wants_skip = True + participant.wants_skip = True skip_count = 0 for part in self.get_participants(False): if part.wants_skip: From 0ec373de3b41cbf5492bdc1ad2873a4ee2945142 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Thu, 8 Feb 2018 08:24:41 +0800 Subject: [PATCH 34/54] Fixed user_can_skip_phase for 'picker' Fixed to use new function call Added argument to comment for user_can_skip_phase --- src/model/match.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index fd5ef02..0a63745 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -316,6 +316,10 @@ def get_seconds_to_next_phase(self): def user_can_skip_phase(self, participant): """Determine whether a user can skip to the next phase + Args: + participant: The participant that made the request + to skip the phase + Returns: bool: Whether the given participant can skip to the next phase """ @@ -330,10 +334,7 @@ def user_can_skip_phase(self, participant): if self.skip_role == "picker": if self._state == "CHOOSING": - for part in self.get_participants(False): - if part.picking and part.nickname == nickname: - return True - return False + return participant.picking else: return True elif self.skip_role == "anyone": From fb49032aadbb4cb80a46dab6a74db162edf0cc99 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 08:58:22 +0800 Subject: [PATCH 35/54] Added wild card count selector POST request handler can also return numbers If parameter doesn't have a filename, assume it's the wild card counter instead --- src/nussschale/handler.py | 17 ++++++++++------- src/res/tpl/dashboard.html | 2 ++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/nussschale/handler.py b/src/nussschale/handler.py index aafbb74..401e507 100644 --- a/src/nussschale/handler.py +++ b/src/nussschale/handler.py @@ -325,13 +325,16 @@ def _unwrap_param(self, allow_list: bool, param: _RawPOSTParam assert isinstance(param.value, str) return param.value else: - fp = param.file - fp.seek(0, 2) - size = fp.tell() - fp.seek(0) - nlog().log("File upload: '%s', %i bytes" % (param.filename, - size)) - return IOWrapper(param.file, param) + if param.filename == None: + return param.value + else : + fp = param.file + fp.seek(0, 2) + size = fp.tell() + fp.seek(0) + nlog().log("File upload: '%s', %i bytes" % (param.filename, + size)) + return IOWrapper(param.file, param) else: raise ValueError("Unsupported parameter type") diff --git a/src/res/tpl/dashboard.html b/src/res/tpl/dashboard.html index d242cb7..720c5e5 100644 --- a/src/res/tpl/dashboard.html +++ b/src/res/tpl/dashboard.html @@ -66,6 +66,8 @@
+ Number of blank cards: + Choose a deck... (Maximum file size for decks is 800kB) From 7858ebe3a508213c7066ce7b27c9a255d2d19500 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 09:01:13 +0800 Subject: [PATCH 36/54] Added wild card count properties to Match Get wild card count from POST request --- src/model/match.py | 4 ++++ src/pages/match.py | 1 + 2 files changed, 5 insertions(+) diff --git a/src/model/match.py b/src/model/match.py index 0a63745..2913026 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -99,6 +99,10 @@ class Match: # Which users are allowed to skip the phase skip_role = "owner" + # Number of wild cards in the match + wild_card_count = 0 + wild_cards_played = 0 + @classmethod @named_mutex("_pool_lock") def get_by_id(cls, id): diff --git a/src/pages/match.py b/src/pages/match.py index c9aab54..02f4b9c 100644 --- a/src/pages/match.py +++ b/src/pages/match.py @@ -124,6 +124,7 @@ def create_match(ctx: EndpointContext) -> None: # Create a new match match = Match() + match.wild_card_count = ctx.get_param_as("wildcards", int) # Create the deck from the upload try: From 1518fd836019387726fe0bdaaf0ba508a5ee8dfd Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 11:51:20 +0800 Subject: [PATCH 37/54] Implemented prototype wild card drawing algorithm --- src/model/match.py | 14 +++++++++++--- src/model/multideck.py | 10 ++++++++-- src/model/participant.py | 14 +++++++++++--- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index 2913026..afbac5b 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -99,9 +99,10 @@ class Match: # Which users are allowed to skip the phase skip_role = "owner" - # Number of wild cards in the match + # Wild card data wild_card_count = 0 - wild_cards_played = 0 + wilds_in_play = 0 + total_cards = 0 @classmethod @named_mutex("_pool_lock") @@ -723,6 +724,12 @@ def create_deck(self, data): # Create multidecks for type in self._deck: self._multidecks[type] = MultiDeck[Card, int](self._deck[type]) + self.total_cards += len(self._deck[type]) + + wilds = [Card(card_id_counter + 1 + i, "WILD", "") + for i in range(self.wild_card_count)] + self._multidecks["WILD"] = MultiDeck[Card, int](wilds) + self.total_cards += self.wild_card_count return True, "OK" @@ -963,7 +970,8 @@ def _replenish_hands(self): method. """ for part in self.get_participants(False): - part.replenish_hand(self._multidecks) + part.replenish_hand(self._multidecks, self.wilds_in_play, + self.total_cards) def _select_match_card(self): """Selects a random statement card for this match. diff --git a/src/model/multideck.py b/src/model/multideck.py index c3ee747..1c0fb82 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -25,7 +25,7 @@ requested. Thus the multideck lock can not be part of any deadlock. """ -from random import shuffle +from random import shuffle, randint from threading import RLock from typing import Generic, List, Optional, Set, TypeVar, cast @@ -68,11 +68,14 @@ def _id_of(o: T) -> U: return cast(U, id) @mutex - def request(self, banned_ids: Set[U]) -> Optional[T]: + def request(self, banned_ids: Set[U], wilds = None, wilds_in_play = 0, + cards_left = 0, banned_wilds: Set[U] = None) -> Optional[T]: """Requests a card from the multideck. Args: banned_ids: A set of IDs that may not be chosen. + wilds: The multideck with the wild cards. Defaults to None + wilds_in_play: The number of wild cards currently in players' hands Returns: The object that was selected. This might be None if the @@ -81,6 +84,9 @@ def request(self, banned_ids: Set[U]) -> Optional[T]: Contract: This method locks the deck's lock. """ + if wilds != None: + if randint(1, cards_left) <= len(wilds._backing) - wilds_in_play: + return wilds.request(banned_wilds) ptr = 0 # Try to find a viable object diff --git a/src/model/participant.py b/src/model/participant.py index e29b3d2..fccdf37 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -183,18 +183,25 @@ def choose_count(self) -> int: return n @mutex - def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"] - ) -> None: + def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], + wilds_in_play, cards_left) -> None: """Replenishes the hand of this participant from the given decks. Args: mdecks: Maps card type to a multideck of the card type. + wilds_in_play: Number of wild cards currently in players' hands Contract: This method locks the participant's lock. """ assert not self.spectator, "Trying to replenish spectator" + # Find any wild cards already held + banned_wilds = set() + for hcard in self._hand.values(): + if hcard.card.type == "WILD": + banned_wilds.add(hcard.card.id) + # Replenish for every type for type in filter(lambda x: x != "STATEMENT", mdecks): # Count cards of that type and fetch IDs @@ -207,7 +214,8 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"] # Fill hand to limit for i in range(Participant._HAND_CARDS_PER_TYPE - k_in_hand): - pick = mdecks[type].request(ids_banned) + pick = mdecks[type].request(ids_banned, mdecks["WILD"], + wilds_in_play, cards_left) if pick is None: break # Can't fulfill the requirement... ids_banned.add(pick.id) From 0d07b6511f2c9e631ff43c9f9f6516347149bdcc Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 12:06:33 +0800 Subject: [PATCH 38/54] Fixed wild card drawing Added WILD type to API --- src/model/participant.py | 3 ++- src/pages/api.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/model/participant.py b/src/model/participant.py index fccdf37..9a397ea 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -215,7 +215,8 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], # Fill hand to limit for i in range(Participant._HAND_CARDS_PER_TYPE - k_in_hand): pick = mdecks[type].request(ids_banned, mdecks["WILD"], - wilds_in_play, cards_left) + wilds_in_play, cards_left, + banned_wilds) if pick is None: break # Can't fulfill the requirement... ids_banned.add(pick.id) diff --git a/src/pages/api.py b/src/pages/api.py index c6f3178..6a3953a 100644 --- a/src/pages/api.py +++ b/src/pages/api.py @@ -200,7 +200,8 @@ def api_cards(ctx: EndpointContext) -> None: if not part.spectator: hand_cards = { "OBJECT": {}, - "VERB": {} + "VERB": {}, + "WILD": {} } # type: Dict[str, Dict] hand = part.get_hand() for id, hcard in hand.items(): From 277fafe5f94572392c821e5957c40d322898282e Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 14:54:43 +0800 Subject: [PATCH 39/54] Added wild cards to UI (Card set needs resizing) No longer try to replenish wild cards --- src/model/participant.py | 2 +- src/res/js/match/match.js | 14 ++++++++++++-- src/res/tpl/match.html | 6 ++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/model/participant.py b/src/model/participant.py index 9a397ea..7a8c3e5 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -203,7 +203,7 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], banned_wilds.add(hcard.card.id) # Replenish for every type - for type in filter(lambda x: x != "STATEMENT", mdecks): + for type in filter(lambda x: x != "STATEMENT" and x != "WILD", mdecks): # Count cards of that type and fetch IDs k_in_hand = 0 ids_banned = set() # type: Set[int] diff --git a/src/res/js/match/match.js b/src/res/js/match/match.js index a0046d8..de6c4e0 100644 --- a/src/res/js/match/match.js +++ b/src/res/js/match/match.js @@ -35,7 +35,8 @@ let externalUpdateAllowed = true let handResolver = new Map([ ["OBJECT", new Map()], - ["VERB", new Map()] + ["VERB", new Map()], + ["WILD", new Map()] ]) let numSelected = 0 let selectedCards = new Map() @@ -131,7 +132,8 @@ function updateCards(data) { let sentHand = new Map([ ["OBJECT", new Map()], - ["VERB", new Map()] + ["VERB", new Map()], + ["WILD", new Map()] ]) if (data.hasOwnProperty("hand")) { for (let type of sentHand.keys()) { @@ -417,6 +419,13 @@ pickTab("tab-objects") } + /** + * Chooses the wild cards tab. + */ + function chooseWildsTab() { + pickTab("tab-wilds") + } + /** * Sends POST request to skip the remaining time */ @@ -457,6 +466,7 @@ pickTab("tab-actions") $("#tab-actions").click(chooseActionsTab) $("#tab-objects").click(chooseObjectsTab) + $("#tab-wilds").click(chooseWildsTab) $("#skip-button").click(skipTime) $("#toggle-hand-button").click(toggleHand) $("#toggle-chat-button").click(toggleChat) diff --git a/src/res/tpl/match.html b/src/res/tpl/match.html index d2dcd75..b69008f 100644 --- a/src/res/tpl/match.html +++ b/src/res/tpl/match.html @@ -69,6 +69,9 @@
Objects
+
+ Wild cards +
@@ -77,6 +80,9 @@
No cards on your hand.
+
+
No cards on your hand.
+
From 8dc3b71fb49e6e10e767e364e48d6456119381f2 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 17:19:20 +0800 Subject: [PATCH 40/54] Can now send wild cards Server side API can change card text Changed to `is not None` for more Python-y-ness and consistency --- src/model/multideck.py | 2 +- src/model/participant.py | 13 +++++++++++++ src/pages/api.py | 4 ++++ src/res/js/match/match.js | 9 ++++++++- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/model/multideck.py b/src/model/multideck.py index 1c0fb82..0782d84 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -84,7 +84,7 @@ def request(self, banned_ids: Set[U], wilds = None, wilds_in_play = 0, Contract: This method locks the deck's lock. """ - if wilds != None: + if wilds is not None: if randint(1, cards_left) <= len(wilds._backing) - wilds_in_play: return wilds.request(banned_wilds) ptr = 0 diff --git a/src/model/participant.py b/src/model/participant.py index 7a8c3e5..d3387f1 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -260,6 +260,19 @@ def toggle_chosen(self, handid: int, allowance: int) -> None: and other_hcard.chosen >= k): other_hcard.chosen = None + @mutex + def set_card_text(self, handid, text): + """Changes the text on a given card. To be used for wild cards. + + Args: + handid: The ID of the hand card. + text: The new text for the card. + + Contract: + This method locks the participant's lock. + """ + self._hand[handid].card.text = text + @mutex def get_choose_data(self, redacted: bool ) -> List[Optional[Dict[str, Union[bool, str]]]]: diff --git a/src/pages/api.py b/src/pages/api.py index 6a3953a..a4764bc 100644 --- a/src/pages/api.py +++ b/src/pages/api.py @@ -170,6 +170,10 @@ def api_choose(ctx: EndpointContext) -> None: except ValueError: raise HTTPException.forbidden(True, "invalid id") + text = ctx.get_param_as("text", str) + if text != "": + part.set_card_text(handid, text) + part.toggle_chosen(handid, match.count_gaps()) # todo: check for wrong id match.check_choosing_done() ctx.json_ok() diff --git a/src/res/js/match/match.js b/src/res/js/match/match.js index de6c4e0..2eb8272 100644 --- a/src/res/js/match/match.js +++ b/src/res/js/match/match.js @@ -352,10 +352,17 @@ if (!allowChoose) { return } + var data = { + "handId": id, + "text": "" + } + if (id in handResolver.get("WILD")) { + data["text"] = prompt("Enter card text:") + } $.ajax({ method: "POST", url: "/api/choose", - data: {"handId": id}, + data: data, success: () => updateSelection(id), error: (x, e, f) => console.log(`/api/choose error: ${e} ${f}`) }) From ba91c1e6068983b7187587b9fcea0e95df212cd2 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Fri, 30 Mar 2018 17:31:41 +0800 Subject: [PATCH 41/54] No longer ask for custom text when deselecting Increased wild card limit to 100 --- src/res/js/match/match.js | 5 ++++- src/res/tpl/dashboard.html | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/res/js/match/match.js b/src/res/js/match/match.js index 2eb8272..d0776db 100644 --- a/src/res/js/match/match.js +++ b/src/res/js/match/match.js @@ -356,7 +356,10 @@ "handId": id, "text": "" } - if (id in handResolver.get("WILD")) { + // Ask user for custom card text if the card isn't already selected and + // it's a wild card. + if (id in handResolver.get("WILD") && + !handResolver.get("WILD").get(id).hasClass("card-selected")) { data["text"] = prompt("Enter card text:") } $.ajax({ diff --git a/src/res/tpl/dashboard.html b/src/res/tpl/dashboard.html index 720c5e5..8695bb2 100644 --- a/src/res/tpl/dashboard.html +++ b/src/res/tpl/dashboard.html @@ -67,7 +67,7 @@
Number of blank cards: - + Choose a deck... (Maximum file size for decks is 800kB) From b7f7e590c7cf945f4ea4d66b1fcdd08139452f0f Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Sat, 31 Mar 2018 13:52:59 +0800 Subject: [PATCH 42/54] Added copyright --- src/nussschale/handler.py | 1 + src/pages/match.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/nussschale/handler.py b/src/nussschale/handler.py index 401e507..f8661a3 100644 --- a/src/nussschale/handler.py +++ b/src/nussschale/handler.py @@ -2,6 +2,7 @@ MIT License Copyright (c) 2017-2018 LordKorea +Copyright (c) 2018 Arc676/Alessandro Vinciguerra Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/src/pages/match.py b/src/pages/match.py index 02f4b9c..f0b7831 100644 --- a/src/pages/match.py +++ b/src/pages/match.py @@ -2,6 +2,7 @@ MIT License Copyright (c) 2017-2018 LordKorea +Copyright (c) 2018 Arc676/Alessandro Vinciguerra Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From 530070ae9416b95accad7bb78fc7ed88f0dc183d Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Mon, 2 Apr 2018 09:25:56 +0800 Subject: [PATCH 43/54] Better wild card tracking --- src/model/match.py | 3 +-- src/model/multideck.py | 10 ++++++---- src/model/participant.py | 8 ++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index afbac5b..cf0d207 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -101,7 +101,6 @@ class Match: # Wild card data wild_card_count = 0 - wilds_in_play = 0 total_cards = 0 @classmethod @@ -970,7 +969,7 @@ def _replenish_hands(self): method. """ for part in self.get_participants(False): - part.replenish_hand(self._multidecks, self.wilds_in_play, + part.replenish_hand(self._multidecks, self.total_cards) def _select_match_card(self): diff --git a/src/model/multideck.py b/src/model/multideck.py index 0782d84..58c6df4 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -2,6 +2,7 @@ MIT License Copyright (c) 2017-2018 LordKorea +Copyright (c) 2018 Arc676/Alessandro Vinciguerra Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -68,14 +69,15 @@ def _id_of(o: T) -> U: return cast(U, id) @mutex - def request(self, banned_ids: Set[U], wilds = None, wilds_in_play = 0, - cards_left = 0, banned_wilds: Set[U] = None) -> Optional[T]: + def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, + banned_wilds: Set[U] = None) -> Optional[T]: """Requests a card from the multideck. Args: banned_ids: A set of IDs that may not be chosen. wilds: The multideck with the wild cards. Defaults to None - wilds_in_play: The number of wild cards currently in players' hands + cards_left: The number of cards remaining to be drawn + banned_wilds: A set of IDs of wild cards that are aleady drawn Returns: The object that was selected. This might be None if the @@ -85,7 +87,7 @@ def request(self, banned_ids: Set[U], wilds = None, wilds_in_play = 0, This method locks the deck's lock. """ if wilds is not None: - if randint(1, cards_left) <= len(wilds._backing) - wilds_in_play: + if randint(1, cards_left) <= len(wilds._queue): return wilds.request(banned_wilds) ptr = 0 diff --git a/src/model/participant.py b/src/model/participant.py index d3387f1..fa760a3 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -2,6 +2,7 @@ MIT License Copyright (c) 2017-2018 LordKorea +Copyright (c) 2018 Arc676/Alessandro Vinciguerra Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -184,12 +185,12 @@ def choose_count(self) -> int: @mutex def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], - wilds_in_play, cards_left) -> None: + cards_left) -> None: """Replenishes the hand of this participant from the given decks. Args: mdecks: Maps card type to a multideck of the card type. - wilds_in_play: Number of wild cards currently in players' hands + cards_left: Number of cards remaining to be drawn Contract: This method locks the participant's lock. @@ -215,8 +216,7 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], # Fill hand to limit for i in range(Participant._HAND_CARDS_PER_TYPE - k_in_hand): pick = mdecks[type].request(ids_banned, mdecks["WILD"], - wilds_in_play, cards_left, - banned_wilds) + cards_left, banned_wilds) if pick is None: break # Can't fulfill the requirement... ids_banned.add(pick.id) From 22ce8054788c04325104407d6ff9903434c825dd Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Mon, 2 Apr 2018 09:45:32 +0800 Subject: [PATCH 44/54] Refactored MultiDeck.request Moved queue building and picking to separate methods Build queue when creating MultiDeck --- src/model/multideck.py | 61 +++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/src/model/multideck.py b/src/model/multideck.py index 58c6df4..37c482a 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -51,6 +51,7 @@ def __init__(self, deck: List[T]) -> None: self._backing = deck # type: List[T] self._queue = [] # type: List[T] self._contained = set() # type: Set[U] + refillQueue() @staticmethod def _id_of(o: T) -> U: @@ -71,13 +72,15 @@ def _id_of(o: T) -> U: @mutex def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, banned_wilds: Set[U] = None) -> Optional[T]: - """Requests a card from the multideck. + """Requests a card from the multideck. The last three arguments are + optional and should be omitted when requesting a card from the wild + deck. Args: banned_ids: A set of IDs that may not be chosen. - wilds: The multideck with the wild cards. Defaults to None - cards_left: The number of cards remaining to be drawn - banned_wilds: A set of IDs of wild cards that are aleady drawn + wilds: The multideck with the wild cards. Defaults to None. + cards_left: The number of cards remaining to be drawn. + banned_wilds: A set of IDs of wild cards that are aleady drawn. Returns: The object that was selected. This might be None if the @@ -92,36 +95,50 @@ def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, ptr = 0 # Try to find a viable object - while ptr < len(self._queue): - obj = self._queue[ptr] - if MultiDeck._id_of(obj) not in banned_ids: - self._contained.remove(MultiDeck._id_of(obj)) - del self._queue[ptr] - return obj - ptr += 1 + card, ptr = pickFromQueue(ptr) + if card is not None: + return card # No object in the queue works... Need to refill queue! # Note: For backing decks that are only slightly bigger than the # set of banned ids this might result in less than optimal randomness. # However in practice, deck size does exceed the number of banned ids # by at least factor 2. - pool = [] - for obj in self._backing: - if MultiDeck._id_of(obj) not in self._contained: - pool.append(obj) - shuffle(pool) - for obj in pool: - self._contained.add(MultiDeck._id_of(obj)) - self._queue.append(obj) + refillQueue() # Try to find a viable object again + card, ptr = pickFromQueue(ptr) + + # Still no object found: Failure, as the queue is already maximal. + return card + + def pickFromQueue(ptr): + """Pick a card from the queue + + Args: + ptr: The index from which to start looking for viable cards + + Returns: + A tuple containing the selected card and the new value of ptr. The + card will be None if the request can't be fulfilled. + """ while ptr < len(self._queue): obj = self._queue[ptr] if MultiDeck._id_of(obj) not in banned_ids: self._contained.remove(MultiDeck._id_of(obj)) del self._queue[ptr] - return obj + return obj, ptr ptr += 1 + return None, ptr - # Still no object found: Failure, as the queue is already maximal. - return None + def refillQueue(): + """Refill the card queue + """ + pool = [] + for obj in self._backing: + if MultiDeck._id_of(obj) not in self._contained: + pool.append(obj) + shuffle(pool) + for obj in pool: + self._contained.add(MultiDeck._id_of(obj)) + self._queue.append(obj) From d03cf89b3bfc6a8f954270eedabcb4988465bda5 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Mon, 2 Apr 2018 09:58:21 +0800 Subject: [PATCH 45/54] Fixed syntax and arguments --- src/model/multideck.py | 72 ++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/model/multideck.py b/src/model/multideck.py index 37c482a..6315d61 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -40,6 +40,39 @@ class MultiDeck(Generic[T, U]): """A (refilling) deck used to make selection seem more 'random'.""" + def pickFromQueue(self, ptr, banned_ids: Set[U]): + """Pick a card from the queue + + Args: + ptr: The index from which to start looking for viable cards + banned_ids: A set of IDs that may not be chosen. + + Returns: + A tuple containing the selected card and the new value of ptr. The + card will be None if the request can't be fulfilled. + """ + while ptr < len(self._queue): + obj = self._queue[ptr] + if MultiDeck._id_of(obj) not in banned_ids: + self._contained.remove(MultiDeck._id_of(obj)) + del self._queue[ptr] + return obj, ptr + ptr += 1 + return None, ptr + + def refillQueue(self): + """Refill the card queue + """ + pool = [] + for obj in self._backing: + if MultiDeck._id_of(obj) not in self._contained: + pool.append(obj) + shuffle(pool) + for obj in pool: + self._contained.add(MultiDeck._id_of(obj)) + self._queue.append(obj) + + def __init__(self, deck: List[T]) -> None: """Constructor. @@ -51,7 +84,7 @@ def __init__(self, deck: List[T]) -> None: self._backing = deck # type: List[T] self._queue = [] # type: List[T] self._contained = set() # type: Set[U] - refillQueue() + self.refillQueue() @staticmethod def _id_of(o: T) -> U: @@ -95,7 +128,7 @@ def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, ptr = 0 # Try to find a viable object - card, ptr = pickFromQueue(ptr) + card, ptr = self.pickFromQueue(ptr, banned_ids) if card is not None: return card @@ -104,41 +137,10 @@ def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, # set of banned ids this might result in less than optimal randomness. # However in practice, deck size does exceed the number of banned ids # by at least factor 2. - refillQueue() + self.refillQueue() # Try to find a viable object again - card, ptr = pickFromQueue(ptr) + card, ptr = self.pickFromQueue(ptr, banned_ids) # Still no object found: Failure, as the queue is already maximal. return card - - def pickFromQueue(ptr): - """Pick a card from the queue - - Args: - ptr: The index from which to start looking for viable cards - - Returns: - A tuple containing the selected card and the new value of ptr. The - card will be None if the request can't be fulfilled. - """ - while ptr < len(self._queue): - obj = self._queue[ptr] - if MultiDeck._id_of(obj) not in banned_ids: - self._contained.remove(MultiDeck._id_of(obj)) - del self._queue[ptr] - return obj, ptr - ptr += 1 - return None, ptr - - def refillQueue(): - """Refill the card queue - """ - pool = [] - for obj in self._backing: - if MultiDeck._id_of(obj) not in self._contained: - pool.append(obj) - shuffle(pool) - for obj in pool: - self._contained.add(MultiDeck._id_of(obj)) - self._queue.append(obj) From c028937e0e1e802aa4818a30f71dc23f71b0e203 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Mon, 2 Apr 2018 10:23:38 +0800 Subject: [PATCH 46/54] Added wild cards to CSS Added default text to wild cards Now says "Wild card" instead of being empty --- src/model/match.py | 2 +- src/res/css/dark/deck.css | 12 +++++++++++- src/res/css/light/deck.css | 5 +++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index cf0d207..166f851 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -725,7 +725,7 @@ def create_deck(self, data): self._multidecks[type] = MultiDeck[Card, int](self._deck[type]) self.total_cards += len(self._deck[type]) - wilds = [Card(card_id_counter + 1 + i, "WILD", "") + wilds = [Card(card_id_counter + 1 + i, "WILD", "Wild card") for i in range(self.wild_card_count)] self._multidecks["WILD"] = MultiDeck[Card, int](wilds) self.total_cards += self.wild_card_count diff --git a/src/res/css/dark/deck.css b/src/res/css/dark/deck.css index 71d77a4..63338f8 100644 --- a/src/res/css/dark/deck.css +++ b/src/res/css/dark/deck.css @@ -36,6 +36,16 @@ color: #441; } +.wild-card { + color: #001; + background-color: #BBB; +} + +.wild-card a, .wild-card a:link, .wild-card a:visited, .wild-card a:hover, + .wild-card a:active { + color: #441; +} + .knob-statement { background-color: #444; } @@ -51,4 +61,4 @@ .grey-card { filter: grayscale(50%); opacity: 0.5; -} \ No newline at end of file +} diff --git a/src/res/css/light/deck.css b/src/res/css/light/deck.css index 4df3bef..012ccd0 100644 --- a/src/res/css/light/deck.css +++ b/src/res/css/light/deck.css @@ -31,6 +31,11 @@ background-color: #DD9; } +.wild-card { + color: #111; + background-color: #DDD; +} + .knob-statement { background-color: #444; } From 6ae4e976c62bd47f2836b55e70d0fd268da5287d Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Mon, 2 Apr 2018 10:33:54 +0800 Subject: [PATCH 47/54] Fixed wild card choosing --- src/res/js/match/match.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/res/js/match/match.js b/src/res/js/match/match.js index d0776db..f795777 100644 --- a/src/res/js/match/match.js +++ b/src/res/js/match/match.js @@ -358,7 +358,7 @@ } // Ask user for custom card text if the card isn't already selected and // it's a wild card. - if (id in handResolver.get("WILD") && + if (handResolver.get("WILD").has(id) && !handResolver.get("WILD").get(id).hasClass("card-selected")) { data["text"] = prompt("Enter card text:") } @@ -383,6 +383,7 @@ let remove = false let card = handResolver.get("OBJECT").get(id) card = card || handResolver.get("VERB").get(id) + card = card || handResolver.get("WILD").get(id) // Remove card choices for (let i = 0; i < 4; i++) { From e654b42accca6e35fd19b26f49f4c9298df6d44f Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Thu, 5 Apr 2018 11:47:19 +0800 Subject: [PATCH 48/54] Track participant hand size for card count Added method for determining participant hand size Use participant hand size to determine size of available card pool Probability of drawing wild card depends on size of card pool Particpant.replenish_hand now returns number of cards drawn --- src/model/match.py | 7 +++++-- src/model/multideck.py | 3 ++- src/model/participant.py | 22 +++++++++++++++++++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index 166f851..9843196 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -968,9 +968,12 @@ def _replenish_hands(self): The caller ensures that the match's lock is held when calling this method. """ + card_count = self.total_cards for part in self.get_participants(False): - part.replenish_hand(self._multidecks, - self.total_cards) + card_count -= part.get_card_count() + for part in self.get_participants(False): + drawn = part.replenish_hand(self._multidecks, card_count) + card_count -= drawn def _select_match_card(self): """Selects a random statement card for this match. diff --git a/src/model/multideck.py b/src/model/multideck.py index 6315d61..2c18c0a 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -142,5 +142,6 @@ def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, # Try to find a viable object again card, ptr = self.pickFromQueue(ptr, banned_ids) - # Still no object found: Failure, as the queue is already maximal. + # If no object found: Failure, as the queue is already maximal. + # Otherwise, return the given card return card diff --git a/src/model/participant.py b/src/model/participant.py index fa760a3..f6d7340 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -166,6 +166,19 @@ def get_hand(self) -> Dict[int, "HandCard"]: assert not self.spectator, "Trying to get hand for spectator" return deepcopy(self._hand) + @mutex + def get_card_count(self) -> int: + """Determines the number of cards in the participant's hand. + + Returns: + The number of cards in the participant's hand. + + Contract: + This method locks the participant's lock. + """ + assert not self.spectator, "Trying to get hand for spectator" + return len(self._hand.items()) + @mutex def choose_count(self) -> int: """Retrieves the number of chosen cards in the hand of this player. @@ -185,17 +198,21 @@ def choose_count(self) -> int: @mutex def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], - cards_left) -> None: + cards_left) -> int: """Replenishes the hand of this participant from the given decks. Args: mdecks: Maps card type to a multideck of the card type. cards_left: Number of cards remaining to be drawn + Returns: + The number of cards drawn by the participant. + Contract: This method locks the participant's lock. """ assert not self.spectator, "Trying to replenish spectator" + cards_drawn = 0 # Find any wild cards already held banned_wilds = set() @@ -221,8 +238,11 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], break # Can't fulfill the requirement... ids_banned.add(pick.id) self._hand[self._hand_counter] = HandCard(pick) + cards_drawn += 1 self._hand_counter += 1 + return cards_drawn + @mutex def toggle_chosen(self, handid: int, allowance: int) -> None: """Toggles whether the hand card with the given ID is chosen. From 562ddd7516db3e104c01b70beb0d51e8654ce971 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Thu, 5 Apr 2018 12:15:56 +0800 Subject: [PATCH 49/54] Fixed card drawing Draw fewer cards if wild cards in hand --- src/model/participant.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/model/participant.py b/src/model/participant.py index f6d7340..5cbddc4 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -31,6 +31,7 @@ from threading import RLock from time import time from typing import Dict, List, Mapping, Optional, Set, TYPE_CHECKING, Union +from math import floor, ceil from nussschale.util.locks import mutex @@ -216,9 +217,11 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], # Find any wild cards already held banned_wilds = set() + wilds_in_hand = 0 for hcard in self._hand.values(): if hcard.card.type == "WILD": banned_wilds.add(hcard.card.id) + wilds_in_hand += 1 # Replenish for every type for type in filter(lambda x: x != "STATEMENT" and x != "WILD", mdecks): @@ -230,6 +233,12 @@ def replenish_hand(self, mdecks: Mapping[str, "MultiDeck[Card, int]"], ids_banned.add(hcard.card.id) k_in_hand += 1 + # Reduce number of cards to draw if wild cards are in hand + if type == "OBJECT": + k_in_hand += floor(wilds_in_hand / 2) + else: + k_in_hand += ceil(wilds_in_hand / 2) + # Fill hand to limit for i in range(Participant._HAND_CARDS_PER_TYPE - k_in_hand): pick = mdecks[type].request(ids_banned, mdecks["WILD"], From 8a09a444b03d3e63fdef69b17574e7b12da66f9e Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 11 Apr 2018 08:41:37 +0800 Subject: [PATCH 50/54] Defined return types for added methods --- src/model/multideck.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/model/multideck.py b/src/model/multideck.py index 2c18c0a..6d3b7c1 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -40,11 +40,11 @@ class MultiDeck(Generic[T, U]): """A (refilling) deck used to make selection seem more 'random'.""" - def pickFromQueue(self, ptr, banned_ids: Set[U]): + def pickFromQueue(self, ptr, banned_ids: Set[U]) -> Optional[T]: """Pick a card from the queue Args: - ptr: The index from which to start looking for viable cards + ptr: The index from which to start looking for viable cards. banned_ids: A set of IDs that may not be chosen. Returns: @@ -60,7 +60,7 @@ def pickFromQueue(self, ptr, banned_ids: Set[U]): ptr += 1 return None, ptr - def refillQueue(self): + def refillQueue(self) -> None: """Refill the card queue """ pool = [] From c238f56c50dbb5843bcb92668a81d249be59d94f Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 11 Apr 2018 08:55:58 +0800 Subject: [PATCH 51/54] Converted to underscore style method names --- src/model/multideck.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/model/multideck.py b/src/model/multideck.py index 6d3b7c1..b1ae432 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -40,7 +40,7 @@ class MultiDeck(Generic[T, U]): """A (refilling) deck used to make selection seem more 'random'.""" - def pickFromQueue(self, ptr, banned_ids: Set[U]) -> Optional[T]: + def pick_from_queue(self, ptr, banned_ids: Set[U]) -> Optional[T]: """Pick a card from the queue Args: @@ -60,7 +60,7 @@ def pickFromQueue(self, ptr, banned_ids: Set[U]) -> Optional[T]: ptr += 1 return None, ptr - def refillQueue(self) -> None: + def refill_queue(self) -> None: """Refill the card queue """ pool = [] @@ -84,7 +84,7 @@ def __init__(self, deck: List[T]) -> None: self._backing = deck # type: List[T] self._queue = [] # type: List[T] self._contained = set() # type: Set[U] - self.refillQueue() + self.refill_queue() @staticmethod def _id_of(o: T) -> U: @@ -128,7 +128,7 @@ def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, ptr = 0 # Try to find a viable object - card, ptr = self.pickFromQueue(ptr, banned_ids) + card, ptr = self.pick_from_queue(ptr, banned_ids) if card is not None: return card @@ -137,10 +137,10 @@ def request(self, banned_ids: Set[U], wilds = None, cards_left = 0, # set of banned ids this might result in less than optimal randomness. # However in practice, deck size does exceed the number of banned ids # by at least factor 2. - self.refillQueue() + self.refill_queue() # Try to find a viable object again - card, ptr = self.pickFromQueue(ptr, banned_ids) + card, ptr = self.pick_from_queue(ptr, banned_ids) # If no object found: Failure, as the queue is already maximal. # Otherwise, return the given card From 98f6db6b8e6d65f472bc2f6e43adc87139b119d1 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 11 Apr 2018 08:46:34 +0800 Subject: [PATCH 52/54] Added method for adding a card to queue Can be used to put wild cards back --- src/model/multideck.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/model/multideck.py b/src/model/multideck.py index b1ae432..6a29014 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -40,7 +40,21 @@ class MultiDeck(Generic[T, U]): """A (refilling) deck used to make selection seem more 'random'.""" - def pick_from_queue(self, ptr, banned_ids: Set[U]) -> Optional[T]: + @mutex + def putInQueue(self, obj): + """Put a card in the queue (used to allow wild cards to be redrawn + after being played for the first time, but also to build the queue) + + Args: + obj: The card to put in the queue. + + Contract: + This method locks the deck's lock. + """ + self._contained.add(MultiDeck._id_of(obj)) + self._queue.append(obj) + + def pickFromQueue(self, ptr, banned_ids: Set[U]) -> Optional[T]: """Pick a card from the queue Args: @@ -69,9 +83,7 @@ def refill_queue(self) -> None: pool.append(obj) shuffle(pool) for obj in pool: - self._contained.add(MultiDeck._id_of(obj)) - self._queue.append(obj) - + self.putInQueue(obj) def __init__(self, deck: List[T]) -> None: """Constructor. From 29a4c3b45e73ab7c2f4eea6a807a0f9fac00b026 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 11 Apr 2018 09:09:12 +0800 Subject: [PATCH 53/54] Return card list in delete_chosen Fixed underscore style method names in MultiDeck Added wild card replacement mode property to Math Wild card replacement determined by config Try to replace wild cards when deleting chosen cards --- src/model/match.py | 31 +++++++++++++++++++++++++++++-- src/model/multideck.py | 6 +++--- src/model/participant.py | 8 +++++++- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/model/match.py b/src/model/match.py index 9843196..c312b10 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -102,6 +102,7 @@ class Match: # Wild card data wild_card_count = 0 total_cards = 0 + wild_replace_mode = "no" @classmethod @named_mutex("_pool_lock") @@ -240,6 +241,9 @@ def __init__(self): # Which users are allowed to skip the phase self.skip_role = nconfig().get("skip-role", "owner") + # Whether wild cards should be replaced in the queue after being played + self.wild_replace_mode = nconfig().get("wild-replace-mode", "no") + def put_in_pool(self): """Puts this match into the match pool.""" Match.add_match(self.id, self) @@ -404,7 +408,8 @@ def _leave_state(self): if self._state == "COOLDOWN": # Delete all chosen cards from the hands for part in self.get_participants(False): - part.delete_chosen() + cards = part.delete_chosen() + self._replace_wild_cards(cards) def _enter_state(self): """Handles a transition into the current state. @@ -901,7 +906,8 @@ def declare_round_winner(self, order): else: self._set_state("COOLDOWN") else: - part.delete_chosen() + cards = part.delete_chosen() + self._replace_wild_cards(cards) @mutex def check_choosing_done(self): @@ -975,6 +981,27 @@ def _replenish_hands(self): drawn = part.replenish_hand(self._multidecks, card_count) card_count -= drawn + def _replace_wild_cards(self, cards): + """If the match is configured to do so, put any wild cards played during + the last round back in the MultiDeck's queue so they can be redrawn by + other players in future rounds. + + Args: + cards: A list of card objects played during the last round (the ones + deleted at the end of the last round). + + Contract: + The caller ensures that the match's lock is held when calling this + method. + """ + # If the match isn't configured to replace wild cards or there are no + # cards to put back, do nothing + if self.wild_replace_mode == "no" or len(cards) == 0: + return + for card in cards: + if card.type == "WILD": + self.mdecks["WILD"].putInQueue(card) + def _select_match_card(self): """Selects a random statement card for this match. diff --git a/src/model/multideck.py b/src/model/multideck.py index 6a29014..b338084 100644 --- a/src/model/multideck.py +++ b/src/model/multideck.py @@ -41,7 +41,7 @@ class MultiDeck(Generic[T, U]): """A (refilling) deck used to make selection seem more 'random'.""" @mutex - def putInQueue(self, obj): + def put_in_queue(self, obj): """Put a card in the queue (used to allow wild cards to be redrawn after being played for the first time, but also to build the queue) @@ -54,7 +54,7 @@ def putInQueue(self, obj): self._contained.add(MultiDeck._id_of(obj)) self._queue.append(obj) - def pickFromQueue(self, ptr, banned_ids: Set[U]) -> Optional[T]: + def pick_from_queue(self, ptr, banned_ids: Set[U]) -> Optional[T]: """Pick a card from the queue Args: @@ -83,7 +83,7 @@ def refill_queue(self) -> None: pool.append(obj) shuffle(pool) for obj in pool: - self.putInQueue(obj) + self.put_in_queue(obj) def __init__(self, deck: List[T]) -> None: """Constructor. diff --git a/src/model/participant.py b/src/model/participant.py index 5cbddc4..d5d3ce5 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -140,9 +140,13 @@ def unchoose_all(self) -> None: hcard.chosen = None @mutex - def delete_chosen(self) -> None: + def delete_chosen(self) -> List["Card"]: """Deletes all chosen hand cards from this participant. + Returns: + A list of deep-copied Card objects corresponding to the cards + removed from the participant's hand. + Contract: This method locks the participant's lock. """ @@ -151,7 +155,9 @@ def delete_chosen(self) -> None: for hid, hcard in self._hand.items(): if hcard.chosen is not None: del_list.append(hid) + cards = [] for hid in del_list: + cards.append(deepcopy(self._hand[hid].card)) del self._hand[hid] @mutex From 1b617a724e015f308552e1847488b727ca22b3b9 Mon Sep 17 00:00:00 2001 From: Arc676/Alessandro Vinciguerra Date: Wed, 11 Apr 2018 09:21:42 +0800 Subject: [PATCH 54/54] Fixed typos --- src/model/match.py | 2 +- src/model/participant.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/model/match.py b/src/model/match.py index c312b10..abc59fe 100644 --- a/src/model/match.py +++ b/src/model/match.py @@ -1000,7 +1000,7 @@ def _replace_wild_cards(self, cards): return for card in cards: if card.type == "WILD": - self.mdecks["WILD"].putInQueue(card) + self._multidecks["WILD"].put_in_queue(card) def _select_match_card(self): """Selects a random statement card for this match. diff --git a/src/model/participant.py b/src/model/participant.py index d5d3ce5..182dc63 100644 --- a/src/model/participant.py +++ b/src/model/participant.py @@ -159,6 +159,7 @@ def delete_chosen(self) -> List["Card"]: for hid in del_list: cards.append(deepcopy(self._hand[hid].card)) del self._hand[hid] + return cards @mutex def get_hand(self) -> Dict[int, "HandCard"]: