質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

2回答

1840閲覧

簡単なゲームのクラス設計について

nouken

総合スコア369

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

0クリップ

投稿2020/05/05 02:43

tkinterで簡単なゲームを作っています。

内容としては、二人のプレイヤーで行い、順番にゲームに指定された範囲で好きな数字を入力、ゲームがそれらをリストに保存しておき、その中からランダムに何番目かの数字を思い出させ、正しく答えられなかった方の負け、と言うゲームです。勝敗が決まるまでターンを交代して行います。

質問はゲーム内容に直接は関係ないですが、
現在の設計ではゲームの進行をする部分と、playerとのインターフェース(inputを受け取ったり、質問をしたり)の部分を同じMyGameクラスが担っていますが、これらの機能は別々のクラスに分けた方が、ソフトウェア設計的にはよいですか? つまり、勝ち負けを判定したり、数字を保存したりする部分と入力を受け取ったり表示したりする部分を分けた方がよいですか?

(また、何かソフトウェア設計的にまずそうなところにお気づきになれば教えていただけると幸いです。)

python

1import sys 2import tkinter 3import random 4import time 5import sys 6from tkinter.font import Font 7 8 9class Player: 10 def __init__(self, name): 11 self.name = name 12 self.numbers = [] 13 def __eq__(self, other): 14 return self.name == other 15 16 17class MyGame: 18 def __init__(self, main_label, name_label_1, name_label_2, entry_1, entry_2, button): 19 self.main_label = main_label 20 self.name_label_1 = name_label_1 21 self.name_label_2 = name_label_2 22 self.entry_1 = entry_1 23 self.entry_2 = entry_2 24 self.button = button 25 26 self.idx = None 27 self.rand = None 28 self.done = False 29 self.seed = int(time.time()) 30 random.seed(self.seed) 31 32 # playerの名前をentryから取得, それを元にplayerをインスタンス化 33 def get_names(self): 34 self.button.bind('<Button-1>', self.get_new_number) # 以後はボタンを押すとget_new_numberが呼ばれる 35 root.bind("<Return>", self.get_new_number) 36 self.button.config(text="OK") 37 name_1, name_2 = self.entry_1.get(), self.entry_2.get() 38 self.entry_1.delete(0, "end") 39 self.entry_2.delete(0, "end") 40 self.name_label_1.config(text=name_1, font=Font(family="Courier", size=24)) 41 self.name_label_2.config(text=name_2, font=Font(family="Courier", size=24)) 42 self.player_1 = Player(name_1) 43 self.player_2 = Player(name_2) 44 self.players = [self.player_1, self.player_2] 45 self.cur_player = None 46 self.input_from_player = None 47 48 # どっちのentryに名前を書いても順番がランダムになるように 49 def decide_order(self): 50 if random.uniform(0, 1) < 0.5: 51 self.players = [self.player_2, self.player_1] 52 self.cur_player = self.players[0] 53 54 # 新たな数字の入力を促す 55 def ask_new_number(self): 56 self.rand = int(random.uniform(0, 100)) 57 self.main_label.config( 58 text=f"{self.cur_player.name}のターンです!\n新しい数字を入力してください: {self.rand-4} ~ {self.rand+4}", 59 font=Font(family="Helvetica", size=18)) 60 self.button.bind('<Button-1>', self.get_new_number) 61 root.bind("<Return>", self.get_new_number) 62 63 # 入力の範囲が誤っていた時にもう一度取得 64 def get_correct_new_number(self, event): 65 if self.cur_player == self.player_1: 66 input_1 = entry_1.get() 67 self.entry_1.delete(0, "end") 68 self.input_from_player = int(input_1) 69 else: 70 input_2 = entry_2.get() 71 self.entry_2.delete(0, "end") 72 self.input_from_player = int(input_2) 73 74 # 新たに入力された数字を取得する 75 def get_new_number(self, event): 76 if self.cur_player == self.player_1: 77 input_1 = entry_1.get() 78 self.entry_1.delete(0, "end") 79 self.input_from_player = int(input_1) 80 else: 81 input_2 = entry_2.get() 82 self.entry_2.delete(0, "end") 83 self.input_from_player = int(input_2) 84 85 # TODO: make it not freeze... 86 if self.input_from_player < self.rand-4 or self.rand+4 < self.input_from_player: 87 self.main_label.config( 88 text=f"範囲外です。もう一度入力してください: {self.rand-4} ~ {self.rand+4}\n", 89 font=Font(family="Helvetica", size=18)) 90 self.button.bind('<Button-1>', self.get_correct_new_number) 91 root.bind("<Return>", self.get_correct_new_number) 92 93 self.cur_player.numbers.append(self.input_from_player) 94 95 self.idx = random.choice(range(1, len(self.cur_player.numbers)+1)) 96 self.main_label.config(text=f"{self.idx}番目に入力した数字はなんでしたか?\n",font=Font(family="Helvetica", size=18)) 97 98 self.button.bind('<Button-1>', self.get_past_number) 99 root.bind("<Return>", self.get_past_number) 100 101 # 過去の数字に関する質問の答えを取得し、勝ち負け判断。 102 # 決まらなければ、次のplayerに新たな数字を聞く。以下繰り返し。 103 def get_past_number(self, event): 104 if self.cur_player == self.player_1: 105 input_1 = entry_1.get() 106 self.entry_1.delete(0, "end") 107 self.input_from_player = int(input_1) 108 else: 109 input_2 = entry_2.get() 110 self.entry_2.delete(0, "end") 111 self.input_from_player = int(input_2) 112 113 true = self.cur_player.numbers[self.idx-1] 114 if self.input_from_player != true: 115 self.main_label.config( 116 text=f"{self.cur_player.name}の負けです:(\n\n正しい答えは{true}ですが、{self.cur_player.name}{self.input_from_player}と答えました:(", 117 font=Font(family="Helvetica", size=18)) 118 self.done = True 119 120 if self.done: 121 #self.main_label.config(text=f"Finished!", font=Font(family="Courier", size=18)) 122 self.button.config(text="Exit", font=Font(family="Courier", size=15)) 123 self.button.bind('<Button-1>', self.exit) 124 else: 125 126 self.cur_player = [self.player for self.player in self.players if self.player != self.cur_player][0] 127 self.set_focus_on_entry() 128 self.ask_new_number() 129 130 # windowを閉じる 131 def exit(self, event): 132 sys.exit() 133 134 # 回答を求めるプレイヤーのentryにファーカスを当てる 135 def set_focus_on_entry(self): 136 if self.cur_player == self.player_1: 137 entry_1.focus_set() 138 else: 139 entry_2.focus_set() 140 141 def main(self, event): 142 self.get_names() 143 self.decide_order() 144 145 self.set_focus_on_entry() 146 self.rand = int(random.uniform(0, 100)) 147 self.main_label.config( 148 text=f"{self.cur_player.name}のターンです!\n新しい数字を入力してください: {self.rand-4} ~ {self.rand+4}", 149 font=Font(family="Helvetica", size=18)) 150 151 152if __name__ == '__main__': 153 # 箱を用意 154 root = tkinter.Tk() 155 root.title("kazuoboe") 156 root.geometry("700x500") 157 158 # 各widgetを用意 159 main_label = tkinter.Label(root, text="Welcome to Kazuoboe!", font=Font(family="Courier", size=24)) 160 main_label.place(x=350, y=100, anchor="center") 161 162 name_label_1 = tkinter.Label(root, text="Enter your name :)", font=Font(family="Courier", size=18)) 163 name_label_1.place(x=200, y=200, anchor="center") 164 165 name_label_2 = tkinter.Label(root, text="Enter your name :)", font=Font(family="Courier", size=18)) 166 name_label_2.place(x=500, y=200, anchor="center") 167 168 entry_1 = tkinter.Entry(root, width=10) 169 entry_1.place(x=200, y=250, anchor="center") 170 entry_1.focus_set() 171 172 entry_2 = tkinter.Entry(root, width=10) 173 entry_2.place(x=500, y=250, anchor="center") 174 175 button = tkinter.Button(root, text=u'Start', width=10, font=Font(family="Courier", size=15)) 176 button.place(x=350, y=350, anchor="center") 177 178 # widgetを持ったgameインスタンスを作成 179 mygame = MyGame(main_label, name_label_1, name_label_2, entry_1, entry_2, button) 180 181 # startボタンが押されたらgameインスタンスのmain()が呼ばれて開始 182 button.bind('<Button-1>', mygame.main) 183 root.bind("<Return>", mygame.main) 184 root.mainloop()

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ベストアンサー

これらの機能は別々のクラスに分けた方が、ソフトウェア設計的にはよいですか? 

プログラムの規模や状況次第ですが、
何のためにクラスを別けるか、何を目的とするかで最適解は変わります。

例えば、敢えて反例を出すと、
小さなプログラムでコンパクトにまとめたい場合においては、
クラスの細分化は逆に冗長になるといった具合に。

勿論、ここで質問されているからには、そういう事ではなく、
綺麗な設計にするにはどうするかという事を知りたいのだと思いますが。

とりえあずは、個人的に設計による恩恵の大きいと思う部分、
「コードの再利用性」や「単体テストのしやすさ」等を目安にすると良いです。


つまり、勝ち負けを判定したり、数字を保存したりする部分
入力を受け取ったり表示したりする部分を分けた方がよいですか?

一般的には、Model と View といった風に分離します。

そして Model と View を繋ぐ部分で色々なモデルがあったりしますが、
そこは割愛させてもらって、まずはこの2点の分離

View はこの場合 Tkinter ですが、
例えば、UI を Tkinter から Qt へ移植しようとなった場合、
View の差し替えのみで行えるのが理想的な設計です。

Model に関しては、単体テスト(unittest)を導入してみて
テストをしやすいクラスになるか、等を指標とするとよいでしょう。

一例を挙げると
Model では View (Tkinter) に依存するコードは一切排除します、
import tkinter は勿論、値所得の為のメソッドの呼び出し等も
Model 内では行わず、外部から必要な値だけを貰うように設計します。

そうすることで、このモデルのテストが
tkinter を import することなく実行できるようになります。

投稿2020/05/05 03:16

teamikl

総合スコア8760

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

nouken

2020/05/05 03:20

ぼんやりしていたところが明確になりました。ありがとうございます。
guest

0

設計の「良い・悪い」に明確な基準は存在しません。

きちんと仕様通り動いていて、要求された品質(保守性を含む)を満たしているのであれば、そのコードを触る人にとってそれが良い設計です。なのであなた一人が書いてメンテするコードなら、あなたが満足していればそれが「良い設計」になります。

一般論としては「MyXXX」とかいう名付けが良くないとか、クラスの責務が複数あるのは良くないとか、メソッドが複数のことをやっていると良くないとか、そりゃもう色々な考え方がありますので、オブジェクト指向分析設計をちゃんと学んでみて、それに合致しているのか、意図してそういった指針に反する設計とするのか、などを決めるのが開発者の役割であり、腕の差が出る部分です。

それをご自身でよく考えて「今のままで良い」と思うかどうか決めてください。
他人に「この設計の悪い部分を指摘してくれ」というのはただの作業の丸投げです。

投稿2020/05/05 02:56

gentaro

総合スコア8947

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

nouken

2020/05/05 03:16

よくわかりました。自分で学習してみて、考えてみます。ありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問