import json from enum import Enum from typing import List, Optional import more_itertools BASE62 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; COLORS = 'rygbp' with open("variants.json", 'r') as f: VARIANTS = json.loads(f.read()) def variant_id(variant): return next(var['id'] for var in VARIANTS if var['name'] == variant) ## Helper method, iterate over chunks of length n in a string def chunks(s: str, n: int): for i in range(0, len(s), n): yield s[i:i+n] class DeckCard(): def __init__(self, suitIndex: int, rank: int): self.suitIndex: int = suitIndex self.rank: int = rank @staticmethod def from_json(deck_card): return DeckCard(**deck_card) def __eq__(self, other): return self.suitIndex == other.suitIndex and self.rank == other.rank def __repr__(self): return COLORS[self.suitIndex] + str(self.rank) class ActionType(Enum): Play = 0 Discard = 1 ColorClue = 2 RankClue = 3 EndGame = 4 class Action(): def __init__(self, type_: ActionType, target: int, value: Optional[int] = None): self.type = type_ self.target = target self.value = value @staticmethod def from_json(action): print("Converting {} to an action".format(action)) return Action( ActionType(action['type']), int(action['target']), action.get('value', None) ) def __repr__(self): match self.type: case ActionType.Play: return "Play card {}".format(self.target) case ActionType.Discard: return "Discard card {}".format(self.target) case ActionType.ColorClue: return "Clue color {} to player {}".format(self.value, self.target) case ActionType.ColorClue: return "Clue rank {} to player {}".format(self.value, self.target) case ActionType.EndGame: return "Player {} ends the game (code {})".format(self.target, self.value) return "Undefined" def compress_actions(actions: List[Action]) -> str: minType = 0 maxType = 0 print(actions) if len(actions) != 0: minType = min(map(lambda a: a.type.value, actions)) maxType = max(map(lambda a: a.type.value, actions)) typeRange = maxType - minType + 1 def compress_action(action): value = 0 if action.value is None else action.value a = BASE62[typeRange * value + (action.type.value - minType)] b = BASE62[action.target] return a + b out = str(minType) + str(maxType) out += ''.join(map(compress_action, actions)) return out def decompress_actions(actions_str: str) -> List[Action]: try: minType = int(actions_str[0]) maxType = int(actions_str[1]) except ValueError: raise ValueError("invalid action string: invalid min/max range") assert(maxType >= minType) if not len(actions_str) % 2 == 0: raise ValueError("Invalid length of action str") typeRange = maxType - minType + 1 def decompress_action(action): actionType = ActionType(BASE62.index(action[0]) % typeRange) value = None if actionType not in [actionType.Play, actionType.Discard]: value = BASE62.index(action[0]) // typeRange target = BASE62.index(action[1]) return Action(actionType, target, value) return [decompress_action(a) for a in chunks(actions_str[2:], 2)] def compress_deck(deck: List[DeckCard]) -> str: assert(len(deck) != 0) minRank = min(map(lambda c: c.rank, deck)) maxRank = max(map(lambda c: c.rank, deck)) rankRange = maxRank - minRank + 1 def compress_card(card): return BASE62[rankRange * card.suitIndex + (card.rank - minRank)] out = str(minRank) + str(maxRank) out += ''.join(map(compress_card, deck)) return out def decompress_deck(deck_str: str) -> List[DeckCard]: assert(len(deck_str) >= 2) minRank = int(deck_str[0]) maxRank = int(deck_str[1]) assert(maxRank >= minRank) rankRange = maxRank - minRank + 1 def decompress_card(card_char): index = BASE62.index(card_char) suitIndex = index // rankRange rank = index % rankRange + minRank return DeckCard(suitIndex, rank) return [decompress_card(c) for c in deck_str[2:]] def compressJSONGame(game_json: dict) -> str: out = "" num_players = len(game_json.get('players', [])) num_players = game_json.get('num_players', num_players) if not 2 <= num_players: raise ValueError("Invalid JSON game: could not parse num players") out = "{}".format(num_players) try: deck = game_json['deck'] except: raise KeyError("JSON game contains no deck") assert(len(deck) > 0) if type(deck[0]) != DeckCard: try: deck = [DeckCard.from_json(card) for card in deck] except: raise ValueError("invalid jSON format: could not convert to deck cards") # now, deck is in the correct form out += compress_deck(deck) out += "," # first part finished actions = game_json.get('actions', []) if len(actions) == 0: print("WARNING: action array is empty") else: if type(actions[0]) != Action: try: actions = [Action.from_json(action) for action in actions] except: raise ValueError("invalid JSON format: could not convert to actions") out += compress_actions(actions) out += "," variant = game_json.get("variant", "No Variant") out += str(variant_id(variant)) return ''.join(more_itertools.intersperse("-", out, 20)) ## test deck = [DeckCard(0,1), DeckCard(2,4), DeckCard(4,5)] c = compress_deck(deck) l = decompress_deck(c) print(deck, l) f = [Action(ActionType.Discard, 2), Action(ActionType.Play, 3, 8)] a = compress_actions(f) x = decompress_actions(a) print(a) print(x) c = '15ywseiijdqgholmnxcqrrxpvppvuukdkacakauswlmntfffbbgh' l = decompress_deck(c) print(l) game = {"deck": [{"rank": 5, "suitIndex": 4}, {"rank": 3, "suitIndex": 4}, {"rank": 4, "suitIndex": 3}, {"rank": 5, "suitIndex": 0}, {"rank": 4, "suitIndex": 1}, {"rank": 4, "suitIndex": 1}, {"rank": 5, "suitIndex": 1}, {"rank": 4, "suitIndex": 0}, {"rank": 2, "suitIndex": 3}, {"rank": 2, "suitIndex": 1}, {"rank": 3, "suitIndex": 1}, {"rank": 5, "suitIndex": 2}, {"rank": 2, "suitIndex": 2}, {"rank": 3, "suitIndex": 2}, {"rank": 4, "suitIndex": 2}, {"rank": 4, "suitIndex": 4}, {"rank": 3, "suitIndex": 0}, {"rank": 2, "suitIndex": 3}, {"rank": 3, "suitIndex": 3}, {"rank": 3, "suitIndex": 3}, {"rank": 4, "suitIndex": 4}, {"rank": 1, "suitIndex": 3}, {"rank": 2, "suitIndex": 4}, {"rank": 1, "suitIndex": 3}, {"rank": 1, "suitIndex": 3}, {"rank": 2, "suitIndex": 4}, {"rank": 1, "suitIndex": 4}, {"rank": 1, "suitIndex": 4}, {"rank": 1, "suitIndex": 2}, {"rank": 4, "suitIndex": 0}, {"rank": 1, "suitIndex": 2}, {"rank": 1, "suitIndex": 0}, {"rank": 3, "suitIndex": 0}, {"rank": 1, "suitIndex": 0}, {"rank": 1, "suitIndex": 2}, {"rank": 1, "suitIndex": 0}, {"rank": 1, "suitIndex": 4}, {"rank": 4, "suitIndex": 3}, {"rank": 3, "suitIndex": 4}, {"rank": 2, "suitIndex": 2}, {"rank": 3, "suitIndex": 2}, {"rank": 4, "suitIndex": 2}, {"rank": 5, "suitIndex": 3}, {"rank": 1, "suitIndex": 1}, {"rank": 1, "suitIndex": 1}, {"rank": 1, "suitIndex": 1}, {"rank": 2, "suitIndex": 0}, {"rank": 2, "suitIndex": 0}, {"rank": 2, "suitIndex": 1}, {"rank": 3, "suitIndex": 1}], "first_player": 0, "options": {"variant": "No Variant"}, "players": ["Alice", "Bob", "Cathy", "Donald", "Emily"], "actions": [{"type": 4, "target": 0, "value": 4}]} print("STARTING TEST") c = compressJSONGame(game) print(c) print("hanab.live/shared-replay-json/" + c)