Skip to content

Instantly share code, notes, and snippets.

@okaits
Created January 9, 2026 01:26
Show Gist options
  • Select an option

  • Save okaits/973f47d6e5a91a7c1fa4ebbcc1087152 to your computer and use it in GitHub Desktop.

Select an option

Save okaits/973f47d6e5a91a7c1fa4ebbcc1087152 to your computer and use it in GitHub Desktop.
学校の課題で作った最強じゃんけんプログラム(仮)
#!/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