Created
January 9, 2026 01:26
-
-
Save okaits/973f47d6e5a91a7c1fa4ebbcc1087152 to your computer and use it in GitHub Desktop.
学校の課題で作った最強じゃんけんプログラム(仮)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ ぼくのかんがえたさいきょうのじゃんけんプログラム - 乱数生成にrandomモジュールを使う人に勝つためのモジュール | |
| 注: 乱数生成に安全な乱数生成器を使われた場合、CPU負荷が増大する可能性がある。(100ターンくらいなら無視できそう。) | |
| """ | |
| import collections.abc | |
| import typing | |
| import dataclasses | |
| import random | |
| import bisect | |
| import atexit | |
| def stdout_message(msg: str) -> None: | |
| """ stdoutに対して短いメッセージを出力 """ | |
| print(f"\033[2K\033[G{msg}", end="", flush=True) | |
| atexit.register(lambda: stdout_message("")) | |
| @dataclasses.dataclass() | |
| class RandomStateInfo(): | |
| """ 乱数生成器の状態などを保持するdataclass """ | |
| random_generator: random.Random | |
| initial_random_state: typing.Annotated[tuple, "初期状態におけるgetstate()の返り値"] | |
| random_func: typing.Annotated[collections.abc.Callable[[], int], "乱数生成の際に使用する関数"] | |
| @classmethod | |
| def get_initial_state(cls) -> typing.Self: | |
| """ 初期状態のRandomStateInfoを生成する """ | |
| initial_random_state = random.getstate() | |
| random_generator = random.Random() | |
| random_generator.setstate(initial_random_state) | |
| return cls( | |
| random_generator=random_generator, | |
| initial_random_state=initial_random_state, | |
| random_func=lambda: random_generator.randint(0, 2) | |
| ) | |
| def reset_state(self) -> None: | |
| """ random_generatorを初期状態に戻す """ | |
| self.random_generator.setstate(self.initial_random_state) | |
| def win(opponents_hand: int) -> int: | |
| """ opponents_handに勝つ手を返す """ | |
| match opponents_hand: | |
| case 0: | |
| return 2 | |
| case 1: | |
| return 0 | |
| case 2: | |
| return 1 | |
| case _: | |
| raise ValueError("不明な手が指定されました。") | |
| @dataclasses.dataclass | |
| class Jankenner(): | |
| """ 最も勝率の高い手を出力するために必要な情報を保持するdataclass """ | |
| random_state_info: RandomStateInfo | |
| tried_randint_maxparam: typing.Annotated[int, "randint関数の第2引数について、試した中で最大のもの"] = 0 | |
| opponents_history: list[typing.Annotated[int, "相手の出した手"]] = dataclasses.field(default_factory=list) | |
| opponents_number_mapping: dict[typing.Annotated[int, "相手が計算したはずの乱数"], typing.Annotated[int, "相手の出した手"]] = dataclasses.field(default_factory=dict) | |
| turns: int = 0 | |
| def evaluate_opponents_history(self) -> None: | |
| """ opponents_historyを元に、現時点では最も適切だと思われるrandom_state_info.random_funcを設定する。 """ | |
| trying_randint_maxparam = self.tried_randint_maxparam | |
| while True: | |
| self.tried_randint_maxparam = trying_randint_maxparam | |
| self.random_state_info.reset_state() | |
| self.random_state_info.random_func = lambda: self.random_state_info.random_generator.randint(0, trying_randint_maxparam) | |
| self.opponents_number_mapping = {} | |
| failed = False | |
| for opponents_hand in self.opponents_history: | |
| guessed_randint = self.random_state_info.random_func() | |
| if guessed_randint in self.opponents_number_mapping: | |
| if self.opponents_number_mapping[guessed_randint] == opponents_hand: | |
| continue | |
| else: | |
| # random_state_info.random_funcが違った! | |
| failed = True | |
| break | |
| else: | |
| self.opponents_number_mapping[guessed_randint] = opponents_hand | |
| if failed: | |
| trying_randint_maxparam += 1 | |
| continue | |
| else: | |
| break | |
| # 現時点では矛盾しないrandom_state_info.random_funcが見つかった | |
| stdout_message(f"max: {trying_randint_maxparam}") | |
| self.random_state_info.reset_state() | |
| # randomモジュールの関数の結果は乱数生成器が何回乱数を生成したかによって変わるので、数を合わせる | |
| for _ in range(self.turns): | |
| self.random_state_info.random_func() | |
| def get_nearest_opponents_number_mapping(self, guessed_randint: int) -> int: | |
| """ guessed_randintがopponents_number_mappingに無かった時に、opponents_number_mappingの中で最も近い数字を返す """ | |
| opponents_number_mapping_key_list = sorted(list(self.opponents_number_mapping)) | |
| # bisect.bisect_leftは、ソートされたリストの中で、順序を保ったまま要素を追加する際に最も適したインデックスを返す | |
| index = bisect.bisect_left(opponents_number_mapping_key_list, guessed_randint) | |
| if index == 0: | |
| return self.opponents_number_mapping[opponents_number_mapping_key_list[0]] | |
| if index == len(opponents_number_mapping_key_list): | |
| return self.opponents_number_mapping[opponents_number_mapping_key_list[-1]] | |
| if (opponents_number_mapping_key_list[index] - guessed_randint) < (opponents_number_mapping_key_list[index-1] - guessed_randint): | |
| return self.opponents_number_mapping[opponents_number_mapping_key_list[index]] | |
| return self.opponents_number_mapping[opponents_number_mapping_key_list[index-1]] | |
| def janken(self, _, opponents_hand: typing.Optional[int]) -> int: | |
| """ 勝率の最も高い手を出力する """ | |
| if opponents_hand is not None: | |
| self.opponents_history.append(opponents_hand) | |
| self.evaluate_opponents_history() # 矛盾しないrandom_state_info.random_funcを探し出してくれるはず! | |
| self.turns += 1 | |
| guessed_randint = self.random_state_info.random_func() | |
| if guessed_randint in self.opponents_number_mapping: | |
| return win(self.opponents_number_mapping[guessed_randint]) | |
| if self.opponents_number_mapping: | |
| return win(self.get_nearest_opponents_number_mapping(guessed_randint)) | |
| if 0 <= guessed_randint <= 2: | |
| return win(guessed_randint) | |
| return 2 | |
| jankenner = Jankenner(random_state_info=RandomStateInfo.get_initial_state()) | |
| is_first_time = True | |
| def janken(u1s: int, u2s: int) -> int: | |
| """ じゃんけんの手を出力する | |
| Params: | |
| u1s (int): 前回の自分の手 | |
| u2s (int): 前回の相手の手 | |
| Returns: | |
| int: 今回の自分の手 | |
| """ | |
| global is_first_time | |
| if is_first_time: | |
| is_first_time = False | |
| return jankenner.janken(None, None) | |
| return jankenner.janken(u1s, u2s) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment