Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
0511489
Can customize phase skip permission
Arc676 Feb 3, 2018
9a979eb
Implemented majority vote skipping
Arc676 Feb 3, 2018
59eb125
Anyone can skip if picker is chosen and phase is not "choosing"
Arc676 Feb 3, 2018
996150a
Added chat message for majority skipping
Arc676 Feb 7, 2018
87b6b5b
Moved skip property to participant class
Arc676 Feb 7, 2018
94afb18
Fixed typo
Arc676 Feb 7, 2018
d35d1c7
Fixed user_can_skip_phase for 'picker'
Arc676 Feb 8, 2018
4fb70fd
Added wild card count selector
Arc676 Mar 30, 2018
65813a4
Added wild card count properties to Match
Arc676 Mar 30, 2018
c3d5351
Implemented prototype wild card drawing algorithm
Arc676 Mar 30, 2018
997a334
Fixed wild card drawing
Arc676 Mar 30, 2018
77141b7
Added wild cards to UI
Arc676 Mar 30, 2018
57f4570
Can now send wild cards
Arc676 Mar 30, 2018
541bd09
No longer ask for custom text when deselecting
Arc676 Mar 30, 2018
6cbebcf
Added copyright
Arc676 Mar 31, 2018
7547103
Better wild card tracking
Arc676 Apr 2, 2018
072d1e2
Refactored MultiDeck.request
Arc676 Apr 2, 2018
81b52e7
Fixed syntax and arguments
Arc676 Apr 2, 2018
f2c6857
Added wild cards to CSS
Arc676 Apr 2, 2018
790b5d9
Fixed wild card choosing
Arc676 Apr 2, 2018
32d3ed5
Track participant hand size for card count
Arc676 Apr 5, 2018
6b5d5b6
Fixed card drawing
Arc676 Apr 5, 2018
5188216
Defined return types for added methods
Arc676 Apr 11, 2018
0e5db05
Converted to underscore style method names
Arc676 Apr 11, 2018
c8db551
Added method for adding a card to queue
Arc676 Apr 11, 2018
7bb1e44
Return card list in delete_chosen
Arc676 Apr 11, 2018
90c9844
Fixed typos
Arc676 Apr 11, 2018
8923b9c
Can customize phase skip permission
Arc676 Feb 3, 2018
c5dbacf
Implemented majority vote skipping
Arc676 Feb 3, 2018
e2d516f
Anyone can skip if picker is chosen and phase is not "choosing"
Arc676 Feb 3, 2018
f447025
Added chat message for majority skipping
Arc676 Feb 7, 2018
ea8baeb
Moved skip property to participant class
Arc676 Feb 7, 2018
0a9eddd
Fixed typo
Arc676 Feb 7, 2018
0ec373d
Fixed user_can_skip_phase for 'picker'
Arc676 Feb 8, 2018
fb49032
Added wild card count selector
Arc676 Mar 30, 2018
7858ebe
Added wild card count properties to Match
Arc676 Mar 30, 2018
1518fd8
Implemented prototype wild card drawing algorithm
Arc676 Mar 30, 2018
0d07b65
Fixed wild card drawing
Arc676 Mar 30, 2018
277fafe
Added wild cards to UI
Arc676 Mar 30, 2018
8dc3b71
Can now send wild cards
Arc676 Mar 30, 2018
ba91c1e
No longer ask for custom text when deselecting
Arc676 Mar 30, 2018
b7f7e59
Added copyright
Arc676 Mar 31, 2018
530070a
Better wild card tracking
Arc676 Apr 2, 2018
22ce805
Refactored MultiDeck.request
Arc676 Apr 2, 2018
d03cf89
Fixed syntax and arguments
Arc676 Apr 2, 2018
c028937
Added wild cards to CSS
Arc676 Apr 2, 2018
6ae4e97
Fixed wild card choosing
Arc676 Apr 2, 2018
e654b42
Track participant hand size for card count
Arc676 Apr 5, 2018
562ddd7
Fixed card drawing
Arc676 Apr 5, 2018
8a09a44
Defined return types for added methods
Arc676 Apr 11, 2018
c238f56
Converted to underscore style method names
Arc676 Apr 11, 2018
c1f66ec
Merge branch 'blank-cards' of github.com:Arc676/KgF into blank-cards
Arc676 May 26, 2018
98f6db6
Added method for adding a card to queue
Arc676 Apr 11, 2018
29a4c3b
Return card list in delete_chosen
Arc676 Apr 11, 2018
1b617a7
Fixed typos
Arc676 Apr 11, 2018
cb46a43
Merge branch 'config-wild-replace' of github.com:Arc676/KgF into conf…
Arc676 May 26, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 89 additions & 14 deletions src/model/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -94,6 +96,14 @@ class Match:
# Whether matches are currently frozen
frozen = False

# Which users are allowed to skip the phase
skip_role = "owner"

# Wild card data
wild_card_count = 0
total_cards = 0
wild_replace_mode = "no"

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

# 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)
Expand Down Expand Up @@ -305,15 +321,17 @@ 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.
def user_can_skip_phase(self, participant):
"""Determine whether a user can skip to the next phase

Args:
obj: The participant in question.
participant: The participant that made the request
to skip the phase

Returns:
bool: Whether the given participant can skip to the next phase
"""

# The match must not be ending
if self._state == "ENDING":
return False
Expand All @@ -322,23 +340,47 @@ 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":
if self._state == "CHOOSING":
return participant.picking
else:
return True
elif self.skip_role == "anyone":
return True
elif self.skip_role == "majority":
participant.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 skip_count > majority:
for part in self.get_participants(False):
part.wants_skip = False
return True
else:
self._chat.append(("SYSTEM",
"<b>" + participant.nickname +
" wants to skip the phase. " +
str(majority - skip_count + 1) +
" request(s) left to reach majority.</b>"))
return False
else:
return self.get_owner_nick() == participant.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",
"<b>" + self.get_owner_nick()
+ " skipped to the next phase.</b>"))
"<b>" + nick + " skipped to next phase</b>"))

def _set_state(self, state):
"""Updates the state for this match.
Expand Down Expand Up @@ -366,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.
Expand Down Expand Up @@ -685,6 +728,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", "Wild card")
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"

Expand Down Expand Up @@ -857,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):
Expand Down Expand Up @@ -924,8 +974,33 @@ 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):
card_count -= part.get_card_count()
for part in self.get_participants(False):
part.replenish_hand(self._multidecks)
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._multidecks["WILD"].put_in_queue(card)

def _select_match_card(self):
"""Selects a random statement card for this match.
Expand Down
94 changes: 67 additions & 27 deletions src/model/multideck.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,7 +26,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

Expand All @@ -39,6 +40,51 @@
class MultiDeck(Generic[T, U]):
"""A (refilling) deck used to make selection seem more 'random'."""

@mutex
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)

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 pick_from_queue(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.
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 refill_queue(self) -> None:
"""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.put_in_queue(obj)

def __init__(self, deck: List[T]) -> None:
"""Constructor.

Expand All @@ -50,6 +96,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.refill_queue()

@staticmethod
def _id_of(o: T) -> U:
Expand All @@ -68,11 +115,17 @@ def _id_of(o: T) -> U:
return cast(U, id)

@mutex
def request(self, banned_ids: Set[U]) -> Optional[T]:
"""Requests a card from the multideck.
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. 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.

Returns:
The object that was selected. This might be None if the
Expand All @@ -81,39 +134,26 @@ def request(self, banned_ids: Set[U]) -> Optional[T]:
Contract:
This method locks the deck's lock.
"""
if wilds is not None:
if randint(1, cards_left) <= len(wilds._queue):
return wilds.request(banned_wilds)
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 = self.pick_from_queue(ptr, banned_ids)
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)
self.refill_queue()

# Try to find a viable object again
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 = self.pick_from_queue(ptr, banned_ids)

# Still no object found: Failure, as the queue is already maximal.
return None
# If no object found: Failure, as the queue is already maximal.
# Otherwise, return the given card
return card
Loading