bingo/models.py
2020-01-26 15:51:16 -05:00

262 lines
6.8 KiB
Python

from enum import Enum
from uuid import uuid4
import random
import math
import redis
import json
import common
class BingoCell:
def __init__(self, text, checked=False, free=False, shake=False):
self.text = text
self.free = free
self.checked = checked
self.shake = shake
def serialize(self):
return self.__dict__
def __repr__(self):
return self.text
@staticmethod
def deserialize(j):
return BingoCell(
text=j["text"],
free=bool(j["free"]),
checked=bool(j["checked"]),
shake=bool(j["shake"])
)
class BingoCard:
def __init__(self, size, cells=None, oid=None, last_cell=None):
if cells is None:
cells = []
self.cells = cells
if oid is None:
oid = uuid4().hex
self.oid = oid
self.size = size
self.last_cell = last_cell
def serialize(self):
return {
"oid": self.oid,
"cells": tuple(c.serialize() for c in self.cells),
"size": self.size,
"last_cell": self.last_cell,
"moves_until_win": self.moves_until_win(),
}
def moves_until_win(self):
return min(sum(1 for c in line if not c.checked) for line in self._lines())
def _lines(self):
return [
*(self._row(row) for row in range(0, self.size)),
*(self._col(col) for col in range(0, self.size)),
self._diag_left(),
self._diag_right(),
]
def check_cell(self, cell_idx):
cell = self.cells[cell_idx]
cell.checked = not cell.checked
self._update_shaking(cell)
def _update_shaking(self, cell):
for c in self.cells:
c.shake = False
if cell.checked:
for line in self._lines():
moves_remaining = sum(1 for c in line if not c.checked)
if cell in line and moves_remaining <= self.size / 2 and moves_remaining <= 3:
for c in line:
c.shake = not c.checked
def _row(self, idx):
return self.cells[idx * self.size:idx * self.size + self.size]
def _col(self, idx):
return [self.cells[c] for c in range(0, len(self.cells)) if c % self.size == idx]
def _diag_left(self):
return [self.cells[c] for c in range(0, len(self.cells), self.size + 1)]
def _diag_right(self):
return [self.cells[c] for c in range(self.size - 1, len(self.cells) - 1, self.size - 1)]
@staticmethod
def deserialize(j):
return BingoCard(
cells=tuple(BingoCell.deserialize(c) for c in j["cells"]),
size=j["size"],
oid=j["oid"],
last_cell=j["last_cell"]
)
class GameMode(Enum):
FREE = "free"
ADMIN = "admin"
CPU = "cpu"
class GameState(Enum):
CREATING = "creating"
PLAYING = "playing"
ENDED = "ended"
class BingoGame:
_default_style = {
"cell": {
"base": 0xBD93F9,
"checked": 0xFF5555,
"hover": 0xFF79C6,
"text": 0x111111
},
"card": {
"base": 0xFF5555,
"text": 0x50FA7B
},
"background": 0x282A36,
"message": 0xF8F8F2,
"font": "Hack, 'Roboto Mono', 'Lucida Console'"
}
def __init__(self, room, admin, mode=GameMode.FREE, pool=None, state=GameState.CREATING,
players=None, winners=None, style=None, valid_cells=None):
self.room = room
self.mode = mode
self.admin = admin
if pool is None:
pool = []
self.pool = pool
self.state = state
if players is None:
players = set()
self.players = players
if winners is None:
winners = []
self.winners = winners
if style is None:
style = BingoGame._default_style
self.style = style
if valid_cells is None:
valid_cells = []
self.valid_cells = valid_cells
def should_end(self):
# TODO: add winner count
return len(self.winners) > 0
def generate_card(self):
# TODO: customizable maximum size
size = math.floor(math.sqrt(len(self.pool)))
items = random.sample(self.pool, k=size * size)
return BingoCard(size, cells=[BingoCell(x) for x in items])
def serialize(self):
return {
"room": self.room,
"mode": self.mode.name,
"admin": self.admin,
"state": self.state.name,
"pool": self.pool,
"players": list(self.players),
"winners": self.winners,
"style": self.style,
"valid_cells": self.valid_cells,
}
@staticmethod
def deserialize(j):
return BingoGame(
room=j["room"],
mode=GameMode[j["mode"]],
pool=j["pool"],
admin=j["admin"],
state=GameState[j["state"]],
players=set(j["players"]),
winners=j["winners"],
style=j["style"],
valid_cells=j["valid_cells"],
)
class User:
def __init__(self, name, oid=None, cards=None):
if cards is None:
cards = {}
if oid is None:
oid = uuid4().hex
self.name = name
self.oid = oid
self.cards = cards
@staticmethod
def deserialize(j):
return User(
name=j["name"],
oid=j["oid"],
cards=j["cards"],
)
def serialize(self):
return self.__dict__
class DB:
_prefix = "bingo:"
def __init__(self):
self._rdb = redis.Redis(
host=common.config["REDIS_HOST"],
port=common.config["REDIS_PORT"]
)
def flush(self):
keys = self._rdb.keys(DB._prefix + "*")
if keys:
self._rdb.delete(*keys)
def _get(self, name):
text = self._rdb.get(DB._prefix + name)
# print("<GET> %s = %s" % (name, text))
if text:
return json.loads(text)
def _set(self, name, value):
self._rdb.set(DB._prefix + name, json.dumps(value, separators=(",", ":")))
# print("<SET> %s -> %s" % (name, value))
# self._rdb.expire(DB._prefix + name, 3600 * 24 * 14)
def get_card(self, oid):
j = self._get(oid)
if j:
return BingoCard.deserialize(j)
def save_card(self, card):
self._set(card.oid, card.serialize())
def get_game(self, room):
j = self._get("game:" + room)
if j:
return BingoGame.deserialize(j)
def save_game(self, game: BingoGame):
self._set("game:" + game.room, game.serialize())
def get_user(self, oid):
j = self._get(oid)
if j:
return User.deserialize(j)
def save_user(self, user):
self._set(user.oid, user.serialize())