"""
thekraf.gamemodels
==================
Models for use by the game controller
"""
import thekraf.config as cfg
import thekraf.diceutils as diceutils
from thekraf.score import Score
[docs]class Cycle(object):
"""A cycle or round of the game
Each player gets one turn per cycle
Attributes:
pids (tuple[int]): IDs of players
turns(list[Turn]): Turns in the cycle
"""
def __init__(self, **kwargs):
self.pids = tuple(kwargs.get('pids', ()))
self.turns = list(kwargs.get('turns', []))
if self.turns and not isinstance(self.turn, Turn):
self.turns = [Turn(**d) for d in kwargs['turns']]
@property
def turn(self):
"""The current :class:`.Turn`"""
if self.turns:
return self.turns[-1]
else:
return None
@property
def complete(self):
"""True if all players have completed their turn for this round"""
if len(self.pids) == len(self.turns) and self.turn.complete:
return True
else:
return False
[docs]class Game(object):
"""Model for a game
Attributes:
mode (str): Game mode. Possible modes are:
* 'rounds' -- The most points after a certain number of rounds
* 'points' -- First player to reach a certain number of points
goal (int): Value associated withe the game mode to determine the end
of the game
min_first_bank (int): The minimum running total of a turn required to
bank the first time
min_bank (int): The minimum running total of a turn required to
bank after already being on the board
pids (tuple[int]): IDs of players in the game
cycles(list[Cycle]): Rounds of the game
"""
MODES = cfg.GAME_MODES
def __init__(self, **kwargs):
# Config game mode
self.is_anonymous = kwargs.get('is_anonymous', False)
self.mode = kwargs.get('mode', 'rounds')
assert self.mode in cfg.GAME_MODES
self.goal = self.MODES[self.mode]['goal']
self.min_first_bank = self.MODES[self.mode]['min_first_bank']
self.min_bank = self.MODES[self.mode]['min_bank']
self._players = ()
self.users = []
if self.is_anonymous:
self.player_names = kwargs.get('users', ())
else:
self.users = list(kwargs.get('users', ()))
# If item not Cycle, create cycle from dictionary representation
self.cycles = list(Cycle(**item)
for item in kwargs.get('cycles', [])
if not isinstance(item, Cycle))
self.opts = kwargs.get('opts')
self._description = ''
@property
def description(self):
chunks = []
if self.mode == 'rounds':
chunks.append('Score the most points in {goal} rounds.')
elif self.mode == 'points':
chunks.append('Score {goal} points first.')
if self.min_first_bank > 50:
chunks.append('The minimum score to bank')
if self.min_bank == self.min_first_bank:
chunks.append('is {min_bank} points.')
else:
chunks.append('the first time is {min_first_bank}')
if self.min_bank > 50:
chunks.append(
'points, and {min_bank} points thereafter.'
)
else:
chunks.append('points.')
d = {k: getattr(self, k) for k in self.MODES['rounds'].keys()}
return ' '.join(chunks).format(**d)
@property
def players(self):
if self.is_anonymous:
return self._players
else:
return self.users
@property
def player(self):
turn = self.turn
if turn:
for player in self.players:
if player.id == turn.pid:
return player
return None
@property
def pids(self):
return tuple(player.id for player in self.players)
@property
def cycle(self):
"""Cycle: The current cycle"""
if self.cycles:
return self.cycles[-1]
else:
return None
@property
def turn(self):
"""Turn: The current turn
Returns:
thekraf.gamemodels.Turn:
"""
if self.cycles:
return self.cycles[-1].turn
else:
return None
@property
def player_names(self):
return tuple(player.nickname for player in self.players)
@player_names.setter
def player_names(self, nicknames):
self._players = tuple(Player(id=i, nickname=nickname)
for i, nickname in enumerate(nicknames,
start=-len(nicknames)))
@property
def turns(self):
"""collections.Generator[Turn]: All turns of all rounds"""
return sum((cycle.turns for cycle in self.cycles), [])
@property
def score_per_turns(self):
"""dict[int, int]: Player ID -> total for each turn"""
return {pid: tuple(turn.total for turn in self.turns if turn.pid == pid)
for pid in self.pids}
@property
def totals(self):
"""dict[int, int]: Player ID -> running total for game"""
return {pid: sum(turn.total for turn in self.turns
if turn.pid == pid and turn.complete)
for pid in self.pids}
@property
def complete(self):
"""bool: True if the goal of the game has been reached"""
# At least, the round must be complete
if not self.cycle.complete:
return False
if self.mode == 'rounds':
if self.goal == len(self.cycles):
return True
elif self.mode == 'points':
if any(total >= self.goal for total in self.totals.values()):
return True
return False
[docs]class Player(object):
"""A player of a game
Attributes:
id (int): Unique player ID
name(str): Name of player
"""
def __init__(self, **kwargs):
self.id = kwargs.get('id', 0)
default_name = 'player-{}'.format(self.id)
self.nickname = kwargs.get('nickname', default_name)
[docs]class Turn(object):
"""Series of rolls and scores until bank or bust
Each player will have one Turn per Round. A turn consists of a series
of rolls and scores. After scoring, the player may choose to continue
rolling the remaining dice (or all dice if all dice have been scored), or
bank the running total for the turn.
After a roll, the player must score with at least one die. If the player
is unable to score with any of the dice, the turn is busted. A busted turn
results in the turned being scored as 0.
Rolls and scores will usually be balanced; The dice are rolled, and then
a portion of the rolled dice a scored. The exception is a busted turn.
Since there is no way to score, the final roll, the number of rolls will be
one more than the number of scores.
Attributes:
complete (bool): True of the turn is finished (either banked or busted)
pid (int): Indicates the player playing the turn
dice_score_pairs (list[tuple[tuple[int], int]]): Items in this list are
tuples of the dice that were scored after each roll and the
corresponding score
roll_hist (list[tuple[int]]): History of rolls. Scored dice must be a
subset of rolled dice.
min_bank (int): Minimum running total for a turn required to bank
"""
FIRST_DICE_COUNT = 6
"""int: Number of dice to roll at the beginning of a turn"""
def __init__(self, **kwargs):
self.complete = False
self.pid = kwargs.get('pid', -1)
# If reconstructing with a dict, make sure everything is the right type
self.dice_score_pairs = [
(tuple(dice), score)
for dice, score in kwargs.get('dice_score_pairs', [])
]
self.roll_hist = list(map(tuple, kwargs.get('roll_hist', [])))
self.min_bank = kwargs.get('min_bank', -1)
self.opts_name = kwargs.get('opts_name')
@property
def busted(self):
"""bool: True if there is no way to score with the last roll"""
if self.rolled and not Score.subscores(cfg.GAME_OPTS[self.opts_name],
self.rolled):
return True
else:
return False
@property
def subscores(self):
"""dict[str, int]: Scores for sub-combos of rolled"""
return Score.subscores(cfg.GAME_OPTS[self.opts_name], self.rolled)
@property
def min_scorable(self):
"""tuple[tuple[int], int]: Best score with the least number of dice"""
d = self.subscores
if d:
# Sort by the secondary key first (score)
sorted_by_score = sorted(self.subscores.items(),
key=lambda p: p[1], reverse=True)
# Then, sort by num of dice
dice_str, score = sorted(sorted_by_score,
key=lambda p: len(p[0]))[0]
return diceutils.DiceUtils.create_dice(dice_str), score
else:
return None
@property
def total(self):
"""int: Total of the scored dice, or 0 if busted"""
if self.busted:
return 0
else:
return sum(self.scores)
@property
def rolled(self):
"""tuple[int]: The last roll of the dice"""
if self.roll_hist:
return self.roll_hist[-1]
else:
return None
@rolled.setter
@diceutils.norm_dice(1)
def rolled(self, dice):
if self.roll_hist:
del self.roll_hist[-1]
self.roll_hist.append(dice)
@property
def score(self):
"""int: The score for the last scored dice"""
if self.dice_score_pairs:
return self.dice_score_pairs[-1][1]
else:
return None
@property
def scored_dice(self):
"""int: The score for the last scored dice"""
if self.dice_score_pairs:
return self.dice_score_pairs[-1][0]
else:
return None
@property
def scores(self):
"""int: The score for the last scored dice"""
return tuple(p[1] for p in self.dice_score_pairs)