diff --git a/main.py b/main.py index 5b6f11a..217dfc4 100644 --- a/main.py +++ b/main.py @@ -1,125 +1,21 @@ -from collections import defaultdict import random -from typing import Callable +from typing import Literal BAT = "🦇" SNAKE = "🐍" -ROCK = "🗿" -PAPER = "🗞" -SCISSORS = "🔪" -CHOICES = ROCK, PAPER, SCISSORS -VERBS = { - ROCK: "THUD!", - PAPER: "WHACK!", - SCISSORS: "STAB!", -} +Choice = Literal["🗿", "🗞", "🔪"] +ROCK: Choice = "🗿" +PAPER: Choice = "🗞" +SCISSORS: Choice = "🔪" -def input_choice[T](prompt: str, options: dict[str, T]) -> T: - hint = f"({", ".join(options.keys())})" - choice = input(f"{prompt} {hint}: ").upper() - while choice not in options: - choice = input(f"{hint}): ").upper() - return options[choice] +CHOICES: tuple[Choice, ...] = ROCK, PAPER, SCISSORS -def beats(x: str, y: str): +def beats(x: Choice, y: Choice): return (x, y) in {(PAPER, ROCK), (SCISSORS, PAPER), (ROCK, SCISSORS)} -class Predictor(object): - def __init__(self, context_size: int, include_player_moves: bool, include_adversary_moves: bool): - self._context_size = context_size - self._include_player_moves = include_player_moves - self._include_adversary_moves = include_adversary_moves - - def predict(self, history: list[tuple[str, str]]) -> dict[str, float]: - context_size = self._context_size - - def _pluck_context(i: int) -> tuple[str, ...]: - raw_context = history[i:i + context_size] - context_items: list[str] = [] - for player, adversary in raw_context: - if self._include_player_moves: - context_items.append(player) - if self._include_player_moves: - context_items.append(adversary) - - return tuple(context_items) - - observations: dict[str, int] = {c: 1 for c in CHOICES} - - final_context = _pluck_context(len(history) - context_size) - # print(f"final context {self._context_size, self._include_player_moves, self._include_adversary_moves}: {final_context}") - # print(f"history: {history} context size: {context_size}") - - for i in range(len(history) - context_size): - context = _pluck_context(i) - observation, _ = history[i + context_size] - # print(f"context: {context} final context: {final_context} observation: {observation}") - if context == final_context: - observations[observation] += 1 - - sum_count = 0 - for r in CHOICES: - sum_count += observations[r] - - return { - r: observations[r] / sum_count - for r in CHOICES - } - -class Adversary(object): - def __init__(self): - self._history: list[tuple[str, str]] = [] - - def notify(self, player: str, adversary: str): - self._history.append((player, adversary)) - - def pick(self): - player_probability = self.predict() - expected_value = {c: 0.0 for c in CHOICES} - for adversary in CHOICES: - for player in CHOICES: - expected_value[adversary] += ( - 1.0 if beats(adversary, player) else - -1.0 if beats(player, adversary) else - 0.0 - ) * player_probability[player] - print(f"SNAKE PREDICTIONS: {player_probability}") - print(f"SNAKE EV: {expected_value}") - max_expected_value = max(expected_value.values()) - best_choices = [c for c, v in expected_value.items() if v == max_expected_value] - return random.choice(best_choices) - - def predict(self) -> dict[str, float]: - def _merge(tables: list[dict[str, float]]): - out = {} - for c in CHOICES: - sum_ = 0.0 - for t in tables: - sum_ += t[c] - sum_ /= len(tables) - out[c] = sum_ - return out - - - tables = [] - for p in [ - Predictor(0, include_player_moves=False, include_adversary_moves=False), - Predictor(1, include_player_moves=True, include_adversary_moves=False), - Predictor(1, include_player_moves=False, include_adversary_moves=True), - Predictor(1, include_player_moves=True, include_adversary_moves=True), - # Predictor(2, include_player_moves=True, include_adversary_moves=False), - # Predictor(2, include_player_moves=False, include_adversary_moves=True), - # Predictor(2, include_player_moves=True, include_adversary_moves=True), - Predictor(1, include_player_moves=False, include_adversary_moves=True), - ]: - tables.append(p.predict(self._history)) - - return _merge(tables) - - def main(): counter = 0 @@ -133,8 +29,8 @@ def main(): print(f"SCORE: {player_score}:{adversary_score}") print() print(f"{SNAKE}: Hsss!") - player_choice = input_choice(f"{BAT}", {"R": ROCK, "P": PAPER, "S": SCISSORS}) - adversary_choice = adversary.pick() + player_choice: Choice = input_choice(f"{BAT}", {"R": ROCK, "P": PAPER, "S": SCISSORS}) + adversary_choice: Choice = adversary.pick() if beats(player_choice, adversary_choice): player_score += 1 @@ -155,6 +51,123 @@ def main(): counter += 1 +VERBS = { + ROCK: "THUD!", + PAPER: "WHACK!", + SCISSORS: "STAB!", +} + + +def input_choice[T](prompt: str, options: dict[str, T]) -> T: + hint = f"({", ".join(options.keys())})" + choice = input(f"{prompt} {hint}: ").upper() + while choice not in options: + choice = input(f"{hint}: ").upper() + return options[choice] + + +class Adversary(object): + def __init__(self): + self._history: list[tuple[str, str]] = [] + + def notify(self, player: str, adversary: str): + self._history.append((player, adversary)) + + def pick(self) -> Choice: + return random.choice(CHOICES) + + +class Predictor(object): + def __init__(self, context_size: int, include_player_moves: bool, include_adversary_moves: bool): + self._context_size = context_size + self._include_player_moves = include_player_moves + self._include_adversary_moves = include_adversary_moves + + def predict(self, history: list[tuple[str, str]]) -> dict[str, float]: + context_size = self._context_size + + context_size_in_items = context_size + if self._include_player_moves and self._include_adversary_moves: + context_size_in_items /= 2 + + def _pluck_context(i: int) -> str: + raw_context = history[i:i + context_size] + context: str = "" + for player, adversary in raw_context: + if self._include_player_moves: + context += player + if self._include_adversary_moves: + context += adversary + + return context[-self._context_size:] + + observations: dict[str, int] = {c: 1 for c in CHOICES} + + final_context = _pluck_context(len(history) - context_size) + + for i in range(len(history) - context_size): + context = _pluck_context(i) + observation, _ = history[i + context_size] + if context == final_context: + observations[observation] += 1 + + sum_count = 0 + for r in CHOICES: + sum_count += observations[r] + + return { + r: observations[r] / sum_count + for r in CHOICES + } + + +class Adversary(object): + def __init__(self): + self._history: list[tuple[str, str]] = [] + + def notify(self, player: str, adversary: str): + self._history.append((player, adversary)) + + def pick(self) -> Choice: + player_probability = self.predict() + expected_value: dict[Choice, float] = {c: 0.0 for c in CHOICES} + for adversary in CHOICES: + for player in CHOICES: + expected_value[adversary] += ( + 1.0 if beats(adversary, player) else + -1.0 if beats(player, adversary) else + 0.0 + ) * player_probability[player] + print(f"SNAKE PREDICTIONS: {player_probability}") + print(f"SNAKE EV: {expected_value}") + max_expected_value = max(expected_value.values()) + best_choices: list[Choice] = [c for c, v in expected_value.items() if v == max_expected_value] + return random.choice(best_choices) + + def predict(self) -> dict[Choice, float]: + def _merge(tables: list[dict[Choice, float]]): + out = {} + for c in CHOICES: + sum_ = 0.0 + for t in tables: + sum_ += t[c] + sum_ /= len(tables) + out[c] = sum_ + return out + + + tables = [] + for p in [ + Predictor(0, include_player_moves=False, include_adversary_moves=False), + Predictor(1, include_player_moves=True, include_adversary_moves=False), + Predictor(1, include_player_moves=False, include_adversary_moves=True), + Predictor(2, include_player_moves=True, include_adversary_moves=True), + ]: + tables.append(p.predict(self._history)) + + return _merge(tables) + + if __name__ == "__main__": main() \ No newline at end of file