FINAL問題 みんなでしりとり
みんなでしりとり
from collections import deque
class Player:
word_set = None
num_player = 0
already_said = set()
last_letter = ''
def __init__(self):
self.num = self.join()
self.s = None # shiritori() で代入
def shiritori(self):
self.s = input()
if any(self.is_rule_violated()):
self.leave_game()
else:
Player.last_letter = self.s[-1]
self.already_said.add(self.s)
def is_rule_violated(self):
is_already_said = (self.s in self.already_said)
is_not_in_word_set = (self.s not in Player.word_set)
is_last_z = (self.s[-1] == 'z')
is_not_shiritori = self.is_not_shiritori()
return is_already_said, is_not_in_word_set, is_last_z, is_not_shiritori
def is_not_shiritori(self):
is_not_first_say = Player.last_letter
is_not_shiritori = (Player.last_letter != self.s[0])
return all((is_not_first_say, is_not_shiritori))
def leave_game(self):
Player.num_player -= 1
Player.last_letter = ''
def is_gaming(self):
return self.last_letter
def join(self):
Player.num_player += 1
return Player.num_player
def output_winner(self):
print(self.num)
def output_result(players):
print(Player.num_player)
for player in sorted(players, key=lambda x:x.num):
player.output_winner()
return
def main(M, players):
for _ in range(M):
now_player = players.popleft()
now_player.shiritori()
if now_player.is_gaming():
players.append(now_player)
output_result(players)
def setup():
N, K, M = map(int, input().split())
word_set = set(input() for _ in range(K))
Player.word_set = word_set
players = deque([Player() for _ in range(N)])
return M, players
if __name__ == '__main__':
main(*setup())
複雑でなかなかの難易度だと思います。クラス型で組むと長くなってしまいますが、それぞれの関数やメソッドが何をしているのか見てわかると思います。
クラス変数には「発言で使える単語リスト」「参加中のプレイヤー人数」「発言済み単語リスト」「前者が発言した単語の最後の文字」を持たせています。
インスタンス変数には「プレイヤー番号」を持たせています。self.s
は、発言中のプレイヤーの発言ワードです。
インスタンスを作成すると、自動でプレイヤー番号が与えられるようになっています。ゲームから脱落すると脱落者のインスタンスを消すので、残っているプレイヤーが何番のプレイヤーかを最後に出力するのに使います。このインスタンスは双方向キューの deque に格納します。訳については後程説明します。
- 発言は、単語リストの単語のみ可
- 直前に出た単語の末尾の文字を頭文字とした単語を選ぶこと
- 直前のプレイヤーが脱落したら、ルール2 は不履行
- 発言済みの単語は使えない
- z で終わる単語は使えない
セットアップが終わったら main() 関数です。
def main(M, players):
for _ in range(M):
now_player = players.popleft()
now_player.shiritori()
まず players
の先頭のプレイヤー(インスタンス)を取り出して、変数 now_player
に移します。以降、このプレイヤーのターンとなります。
def shiritori(self):
self.s = input()
if any(self.is_rule_violated()):
self.leave_game()
else:
Player.last_letter = self.s[-1]
self.already_said.add(self.s)
ルールは4+1個です。
if any(self.is_rule_violated()):
の内容はこちらです。
def is_rule_violated(self):
is_already_said = (self.s in self.already_said)
is_not_in_word_set = (self.s not in Player.word_set)
is_last_z = (self.s[-1] == 'z')
is_not_shiritori = self.is_not_shiritori()
return is_already_said, is_not_in_word_set, is_last_z, is_not_shiritori
ルール違反をしている場合は True と判定しています。
次のプログラムではルールを1つでも違反している場合は True、違反なく全てパスしている場合は False で処理されます。
if any(self.is_rule_violated()):
self.leave_game()
else:
Player.last_letter = self.s[-1]
ルール2 と ルール3 の判定は、
def is_not_shiritori(self):
is_not_first_say = Player.last_letter
is_not_shiritori = (Player.last_letter != self.s[0])
return all((is_not_first_say, is_not_shiritori))
このメソッドで行なわれています。最後に発言された単語の末尾の文字は、クラス変数の last_letter
に入っています。この文字と今回発言した単語の頭文字が不一致ならば True を返します。(ルール違反をしたということです)last_letter
の文字列が ''
ヌルストリング(null string) の時は ルール3 が適用され、頭文字は考慮しなくてよくなります。
この判定がややこしくて困りますね。評価をルールに違反していない場合で組むよりも、このプログラム例や paiza の解答コード例のように、評価を「ルールに違反した場合」にしたほうが組みやすい気がします。
これらルールをオールグリーンでパスしたら Player.last_letter
に、今回発言した単語の末尾の文字を代入します。
そして、発言した単語は発言済みリスト(セットだけど)に追加します。
※ ここだけの話、ルール2 の判定をしなくても 100点が取れてしまいます。もはやしりとりではない。😓
- main() 関数
次に、失言で脱落したプレイヤーの行く末を処理します。😱
def leave_game(self):
Player.num_player -= 1
Player.last_letter = ''
Player.num_player
から 1 を引いてプレイヤー人数を減らします。
Player.last_letter
を ''
にするのですが、これは、
def is_gaming(self):
return self.last_letter
ここで ''
を脱落者とみなしている為です。''
は False、文字が入っていれば True です。
この判定については「2章 bool型 True と False」で学習できます。bool型の学習は重要度大です!
同時に、
def is_not_shiritori(self):
is_not_first_say = Player.last_letter # ← ココ
is_not_shiritori = (Player.last_letter != self.s[0])
return all((is_not_first_say, is_not_shiritori))
この部分でも ルール3 の履行判定として使っています。''
の時は直前のプレイヤーが脱落したので、頭文字は何でもよいということです。
if now_player.is_gaming():
players.append(now_player)
脱落者の処分処理はまだ終わりではありません。
プレイヤーのインスタンスは players
の先頭からポップして now_player
に移しています。現在 players
にあるインスタンスは待機中のプレイヤーで、now_player
は現在のターン中のプレイヤーです。
オールグリーンでパスした now_player
はゲーム続行可能な為、players
の末尾(順番の最後)に再び加えるという二行です。
しかし脱落者は players
に追加されず、そのまま消されます。😱コワイ
こうして players
には常にゲームに参加中のプレイヤーのみが残り、順序が回転しながらゲームが継続されていきます。
def output_result(players):
print(Player.num_player) # 出力 1
for player in sorted(players, key=lambda x:x.num):
player.output_winner() # 出力 2
return
最後に結果を画面に出力します。1つは勝ち残っているプレイヤーの人数です。これはクラス変数の num_player
を出力するだけです。ここから直接クラス変数にアクセスして画面に出力しています。
もう1つは勝ち残ったプレイヤーの番号です。昇順で出力することになっていますので、昇順ソートして順に画面に出力していきます。lambda で直接 num
をターゲットに指定します。num
とは、インスタンス変数の self.num
、インスタンス作成時に各プレイヤーに与えられた番号です。
これだけで num
をターゲットにインスタンスを昇順ソートしてくれます。簡単だ。😁
この問題は「プレイヤー」を複数作成させるということでクラス型で作りました。他にもキューを使っています。Bランク問題は時々あまりに複雑すぎてクラスで作ったほうがわかりやすくなる問題もあります。特に人や物が持つ値が多い時はインスタンスがものすごく便利です。
オブジェクト指向をまったく知らなくても、まったく使わなくてもプログラムは組めますが、複雑なプログラムほどクラス型は重宝します。ぜひ頑張って勉強してクラス型で書けるようになっておきましょう。
わかるようになると全然難しくなかったりするんですよね、これが。😅 あまりに構造が違うために最初に難しく考えすぎてしまうから、実は簡単であることに気付けないでいるだけなんです。またイチからの学び直しと感じて気分が憂鬱になってしまうのもあります。手続型で不便はないと感じていても、クラス型で組めるようになると今度はクラス型でないと不便だと感じるようになったりします。
私がオブジェクト指向を理解したきっかけが、私はとあるメーカーの事務機器の製造や修理に関わる工場勤務をしていたのですが、「あ、アレと同じじゃない?」と気付いたら一気に組めるようになりました。フレーム(本体)にアッセンブリと呼ばれる組み立て済みのユニットを次々嵌めていく様がモジュールと一致します。そのアッセンブリを組み立てる時に使う部品1つ1つがメソッドと一致します。コンストラクタはコネクタで、引数は機能設定で、メソッドの呼び出しは操作ボタンで、スタートボタンを1回押すだけで全てを自動でやってくれるところがオブジェクト指向のしくみや思想と一致します。「カプセル化はカバーじゃね?だったらカプセル化は大事だよ!」とか思ったらなんだか可笑しくなりました。🤣
スキルチェックBランク問題は手続型でも組めますが、クラス型を学んだらもう一度そのプログラムをクラス型で組みなおしてみるとメキメキと上達していくと思います。私がそうでした。今は ChatGPT があるからとても学びやすい!
初歩の部分だけですが「3章 オブジェクト指向」でクラス型の基本形を学習できます。
問題集の「クラス・構造体メニュー」もまだの方はぜひ修了しておきましょう!
オブジェクト指向は面白いよ!