So I've decided to practice I will turn all my scripts etc into GUI apps. So my 36 loc guess the number game turned into 136 loc. It's not the best looking, but it does work and on hard it's actually hard to beat computer sometimes. What are your thoughts guys? After finishing it I am not sure if my approach was right. Thank you for any comments and advice. No offence if they will be harsh ;)
import math
import tkinter as tk
import random
from tkinter import Frame, PhotoImage, Radiobutton, StringVar, ttk
from typing import Any
from math import ceil
class Game(tk.Tk):
def __init__(self) -> None:
super().__init__()
self.geometry('400x440+400+300')
self.title('Guess The Number')
self.iconphoto(True, PhotoImage(file='icon.png'))
self.level_var = StringVar(value='medium')
self.player_var = StringVar(value='Player')
self.number_var = StringVar(value='0')
self.player_attempts = 0
self.computer_attempts = 0
self.turn = StringVar(value='0')
self.winner = ''
self.switch_frame(StartFrame)
def switch_frame(self, frameClass: Any) -> None:
new_frame: Frame = frameClass(self)
self._frame = new_frame
self._frame.place(x=0, y=0, width=400, height=440)
# function to ensure user input is actually a number nothing else.
def is_number(self, valueToCheck: str) -> int:
try:
return int(valueToCheck)
except ValueError:
return 0
# difficulty level scope
def game_level(self, level: str) -> int:
if level == 'low':
return 10
elif level == 'medium':
return 100
else:
return 1000
def new_game(self) -> None:
self.player_attempts = 0
self.computer_attempts = 0
self.turn.set('0')
self.winner = ''
class StartFrame(tk.Frame):
def __init__(self, master: Game) -> None:
tk.Frame.__init__(self, master)
tk.Frame.configure(self, bg='#9aaeb6')
main_font = ('Arial', 12, 'bold')
secondary_font = ('Arial', 10)
main_label = ttk.Label(self, text='Game Settings', anchor='center', background='#eee', font=main_font)
main_label.place(x=10, y=10, width=380, height=30)
level_label = ttk.Label(self, text='Difficulty Level', anchor='center', background='#eee', font=secondary_font)
level_label.place(x=10, y=70, width=380, height=30)
level_1 = Radiobutton(self, text='Low', variable=master.level_var, value='low')
level_1.place(x=50, y=110, height=30, width=80)
level_2 = Radiobutton(self, text='Medium', variable=master.level_var, value='medium')
level_2.place(x=160, y=110, height=30, width=80)
level_3 = Radiobutton(self, text='Hard', variable=master.level_var, value='hard')
level_3.place(x=270, y=110, height=30, width=80)
player_label = ttk.Label(self, text='Who will start the game? You or Computer?', anchor='center', background='#eee', font=secondary_font)
player_label.place(x=10, y=150, width=380, height=30)
player_1 = Radiobutton(self, text='Player', variable=master.player_var, value='Player')
player_1.place(x=100, y=190, height=30, width=80)
player_2 = Radiobutton(self, text='Computer', variable=master.player_var, value='Computer')
player_2.place(x=220, y=190, height=30, width=80)
number_label = ttk.Label(self, text='What is your secret number?', anchor='center', background='#eee', font=secondary_font)
number_label.place(x=10, y=230, width=380, height=30)
number_entry = ttk.Entry(self, textvariable=master.number_var, justify='center')
number_entry.place(x=100, y=270, width=200, height=40)
play_button = ttk.Button(self, text='PLAY', command=lambda: master.switch_frame(GameFrame))
play_button.place(x=100, y=350, width=200, height=60)
class GameFrame(tk.Frame):
def __init__(self, master: Game) -> None:
tk.Frame.__init__(self, master)
tk.Frame.configure(self, bg='#9aaeb6')
main_font = ('Arial', 12, 'bold')
secondary_font = ('Arial', 10)
self.com_out = StringVar(value='Let\'s start.')
if master.player_var.get() == 'Computer':
master.turn.set('1')
self.player_secret_number = master.is_number(master.number_var.get())
game_lvl = master.game_level(master.level_var.get())
self.computer_secret_number = random.randint(0, game_lvl)
self.computer_low = 0
self.computer_high = game_lvl
# ensure player number can't be higher than choosen difficulty lvl scope
if self.player_secret_number > game_lvl:
self.player_secret_number = random.randint(0, game_lvl)
main_label = ttk.Label(self, text='Game Time', anchor='center', background='#eee', font=main_font)
main_label.place(x=10, y=10, width=380, height=30)
turn_label = ttk.Label(self, text='Turn:', anchor='center', background='#eee', font=main_font)
turn_label.place(x=10, y=60, width=190, height=30)
self.turn_display = StringVar(value='1')
turn_label_val = ttk.Label(self, textvariable=self.turn_display, anchor='center', background='#eee', font=main_font)
turn_label_val.place(x=200, y=60, width=190, height=30)
player_label = ttk.Label(self, text='Player', anchor='center', background='#eee', font=main_font)
player_label.place(x=50, y=105, width=100, height=30)
computer_label = ttk.Label(self, text='Computer', anchor='center', background='#eee', font=main_font)
computer_label.place(x=250, y=105, width=100, height=30)
command_out = ttk.Label(self, textvariable=self.com_out, anchor='center', font=secondary_font)
command_out.place(x=115, y=180, height=40, width=180)
self.player_guess = StringVar()
player_guess_entry = ttk.Entry(self, textvariable=self.player_guess, justify='center', font=main_font)
player_guess_entry.place(x=160, y=250, height=40, width=90)
guess_button = ttk.Button(self, text='Guess', command=lambda: self.play(master=master))
guess_button.place(x=160, y=300, height=60, width=90)
def play(self, master: Game) -> None:
if int(master.turn.get()) % 2 != 0:
computer_choice = random.randint(self.computer_low, self.computer_high)
if computer_choice == self.player_secret_number:
master.winner = 'computer'
master.switch_frame(WinnerFrame)
else:
if computer_choice > self.player_secret_number:
self.computer_high = computer_choice
else:
self.computer_low = computer_choice
master.computer_attempts += 1
master.turn.set(str(int((master.turn.get())) + 1))
self.turn_display.set(str(math.ceil(int(master.turn.get()) / 2)))
else:
try:
player_choice = int(self.player_guess.get())
if player_choice == self.computer_secret_number:
master.winner = 'player'
master.switch_frame(WinnerFrame)
else:
if player_choice > self.computer_secret_number:
self.com_out.set('Number too high.')
else:
self.com_out.set('Number too low.')
except ValueError:
pass
master.player_attempts += 1
master.turn.set(str(int((master.turn.get())) + 1))
self.player_guess.set('0')
self.turn_display.set(str(math.ceil(int(master.turn.get()) / 2)))
self.play(master)
class WinnerFrame(tk.Frame):
def __init__(self, master: Game) -> None:
tk.Frame.__init__(self, master)
main_font = ('Arial', 12, 'bold')
def winner_looser():
if master.winner == 'computer':
return 'red'
else:
return 'green'
self.configure(background=winner_looser())
main_label = ttk.Label(self, text='Do you want to play again?', anchor='center', background='#eee', font=main_font)
main_label.place(x=10, y=120, width=380, height=30)
yes_button = ttk.Button(self, text='YES', command=lambda: [master.switch_frame(StartFrame),
master.new_game()])
yes_button.place(x=100, y=180, width=90, height=70)
no_button = ttk.Button(self, text='NO', command=master.destroy)
no_button.place(x=200, y=180, width=90, height=70)
def attempt_result() -> str:
if master.winner == 'computer':
return f'{master.computer_attempts} moves for Computer to beat you.'
else:
return f'{master.player_attempts} moves, all you needed to win.'
result_label = ttk.Label(self, text=attempt_result(), anchor='center')
result_label.place(x=10, y=270, width=380)
if __name__ == '__main__':
Game().mainloop()
1 Answer 1
- You're not using
math
, so delete that import is_number
has nothing to do with the class, so move it to global scope. Also, this can be simplified withstr.isnumeric
if the function did what it's named to do. However, it doesn't do what it's named to do - it doesn't check whether a string is numeric; it attempts to parse an integer; so a more appropriate name would betry_parse_int
. Finally: returning 0 on failure is not a good idea. What ifvalueToCheck
(which should bevalue_to_check
) is itself'0'
? How can you distinguish between that and a failure?- It's unlikely that
StartFrame
should exist as a class. It has no members, doesn't access the members of its parent, and doesn't override anything. It can just be a function. Better, keep it and others as classes, but don't inherit; instantiate the frame or window as appropriate ("has-a", not "is-a"). 'Let\'s start.'
would not require an escape if enclosed in double quotes.GameFrame
has poor separation of concerns and bakes in a bunch of game logic when all it should do is display.- Typo:
looser
->loser
attempt_result
is missing aself
, and refers to amaster
variable that's undefined. The initialization ofresult_label
is done in the static namespace, which is not appropriate.- Consider representing player state in a class - one for both the user and computer - and differentiating behaviour using polymorphism. This is not the only way to do things, but it's one way.
- You only have
StringVar
s. SometimesIntVar
orBooleanVar
are called for. - You can simplify your difficulty logic by using
10**x
wherex
is a difficulty between 1 and 3. - Move your main and secondary fonts to global constants for reuse.
- Many of the declarations that you make for
tk
variables don't actually need to keep references, locally or otherwise. - If you want your computer to be harder to beat, call into
bisect
. - Consider using a spinbox instead of an unconstrained entry box for the guess, so that lower and upper limits are automatically enforced.
- Consider telling the user what their minimum and maximum guess is permitted to be.
- For quality of life, after the user has clicked the
Guess
button, re-select the guess entry. I haven't shown this below. - You have a mix of qualified and de-qualified
tk
imports. Choose one (probably the qualified version with nofrom
). - Don't repeat your geometric references (the 400 pixels) - instead, you can format the geometry string based on integer members of your view class.
- Give an owner and
name=
to all of yourVar
instances. - Where possible, avoid writing "master".
Suggested
I messed around with coroutines a little (see blocked_on_user_coro
) - they're a convenient way to "pause" code, for example when waiting for player input. This also relies on function reference hooks to enforce loose coupling.
import tkinter as tk
import random
from itertools import count
from tkinter import ttk
from typing import Callable, Optional, ClassVar, Tuple
MAIN_FONT = ('Arial', 12, 'bold')
SECONDARY_FONT = ('Arial', 10)
class Player:
NAME: ClassVar[str]
INTERACTIVE: ClassVar[bool]
WIN_SUFFIX: ClassVar[str]
def __init__(self, secret: int) -> None:
self.attempts = 0
# The secret the player is guessing about, not the secret the other
# player has chosen
self.secret = secret
def play(self) -> int:
self.attempts += 1
guess = self.guess()
delta = guess - self.secret
self.strategise(guess, delta)
return delta
def guess(self) -> int:
raise NotImplementedError()
def strategise(self, guess: int, delta: int) -> None:
pass
@property
def result(self) -> str:
return f'{self.attempts}{self.WIN_SUFFIX}'
class User(Player):
INTERACTIVE = True
NAME = 'Player'
WIN_SUFFIX = ' moves, all you needed to win.'
def __init__(self, max_number: int, get_guess: Callable[[], int]):
super().__init__(secret=random.randrange(max_number))
self.guess = get_guess
class Computer(Player):
INTERACTIVE = False
NAME = 'Computer'
WIN_SUFFIX = ' moves for Computer to beat you.'
def __init__(self, max_number: int, secret: int) -> None:
super().__init__(secret)
self.lower, self.upper = 0, max_number
def guess(self) -> int:
guess = random.randrange(start=self.lower, stop=self.upper)
# print(f'{self.NAME}: {self.lower} <= {guess} < {self.upper}')
return guess
def strategise(self, guess: int, delta: int) -> None:
if delta > 0: # too high
self.upper = guess
elif delta < 0: # too low
self.lower = guess
class Game:
def __init__(
self, max_number: int, user_secret: int, user_goes_first: bool,
parent: tk.Tk, win_hook: Callable[[Player], None],
) -> None:
self.view = GameView(parent, max_number, self.user_played)
self.players: Tuple[Player, ...] = (
User(max_number=max_number, get_guess=self.view.player_guess_var.get),
Computer(max_number=max_number, secret=user_secret),
)
if not user_goes_first:
self.players = self.players[::-1]
self.win_hook = win_hook
self.blocked_on_user = None
def start(self) -> None:
self.blocked_on_user = self.blocked_on_user_coro()
self.user_played()
def user_played(self) -> None:
try:
next(self.blocked_on_user)
except StopIteration:
pass
def blocked_on_user_coro(self):
for turn_index in count():
self.view.show_turn(turn_index)
for player in self.players:
if player.INTERACTIVE:
yield # wait for a user to play
if self.do_turn(player):
return
def do_turn(self, player: Player) -> bool:
delta = player.play()
if delta == 0:
self.win_hook(player)
return True
if player.INTERACTIVE:
if delta < 0:
desc = 'low'
else:
desc = 'high'
self.view.show_command(f'Number too {desc}.')
return False
class GameProgram:
def __init__(self):
self.parent_view = ParentView()
def run(self) -> None:
def play():
game = Game(
max_number=start_view.max_number,
user_secret=start_view.number_var.get(),
user_goes_first=start_view.player_goes_first_var.get(),
parent=self.parent_view.window,
win_hook=self.win,
)
self.parent_view.switch_frame(game.view.frame)
game.start()
start_view = StartView(self.parent_view.window, play)
self.parent_view.switch_frame(start_view.frame)
self.parent_view.window.mainloop()
def win(self, winner: Player) -> None:
winner_view = WinnerView(
parent=self.parent_view.window,
user_won=winner.INTERACTIVE,
result=winner.result,
restart_hook=self.run,
)
self.parent_view.switch_frame(winner_view.frame)
class ParentView:
def __init__(self) -> None:
self.window = tk.Tk()
self.width = 400
self.height = 440
self.window.geometry(f'{self.width}x{self.height}+400+300')
self.window.title('Guess The Number')
# self.parent.iconphoto(True, PhotoImage(file='icon.png'))
self.child: Optional[tk.Frame] = None
def switch_frame(self, child: tk.Frame) -> None:
if self.child is not None:
self.child.destroy()
child.place(
x=0, width=self.width,
y=0, height=self.height,
)
self.child = child
class StartView:
def __init__(self, parent: tk.Tk, done_hook: Callable[[], None]):
self.frame = tk.Frame(parent, background='#9aaeb6')
self.level_var = tk.IntVar(self.frame, name='level', value=1)
self.player_goes_first_var = tk.BooleanVar(self.frame, name='player_goes_first', value=True)
self.number_var = tk.IntVar(self.frame, name='number', value=0)
ttk.Label(
self.frame, text='Game Settings', anchor='center',
background='#eee', font=MAIN_FONT,
).place(x=10, y=10, width=380, height=30)
ttk.Label(
self.frame, text='Difficulty Level', anchor='center',
background='#eee', font=SECONDARY_FONT,
).place(x=10, y=70, width=380, height=30)
tk.Radiobutton(
self.frame, text='Low', variable=self.level_var, value=1,
).place(x=50, y=110, height=30, width=80)
tk.Radiobutton(
self.frame, text='Medium', variable=self.level_var, value=2,
).place(x=160, y=110, height=30, width=80)
tk.Radiobutton(
self.frame, text='Hard', variable=self.level_var, value=3,
).place(x=270, y=110, height=30, width=80)
ttk.Label(
self.frame, text='Who will start the game? You or Computer?',
anchor='center', background='#eee', font=SECONDARY_FONT,
).place(x=10, y=150, width=380, height=30)
tk.Radiobutton(
self.frame, text='Player', variable=self.player_goes_first_var, value=True,
).place(x=100, y=190, height=30, width=80)
tk.Radiobutton(
self.frame, text='Computer', variable=self.player_goes_first_var, value=False,
).place(x=220, y=190, height=30, width=80)
ttk.Label(
self.frame, text='What is your secret number?', anchor='center',
background='#eee', font=SECONDARY_FONT,
).place(x=10, y=230, width=380, height=30)
ttk.Entry(
self.frame, textvariable=self.number_var, justify='center',
).place(x=100, y=270, width=200, height=40)
ttk.Button(
self.frame, text='PLAY', command=self.play,
).place(x=100, y=350, width=200, height=60)
self.done_hook = done_hook
@property
def max_number(self) -> int:
return 10**self.level_var.get()
@property
def is_valid(self) -> bool:
try:
return 0 <= self.number_var.get() < self.max_number
except tk.TclError:
return False
def play(self) -> None:
if self.is_valid:
self.done_hook()
class GameView:
def __init__(self, parent: tk.Tk, max_number: int, play_hook: Callable[[], None]) -> None:
self.frame = tk.Frame(parent, background='#9aaeb6')
self.make_label(text='Game Time').place(x=10, y=10, width=380, height=30)
self.turn_var = tk.IntVar(self.frame, name='turn', value=0)
self.make_label(text='Turn:').place(x=10, y=60, width=190, height=30)
self.make_label(textvariable=self.turn_var).place(x=200, y=60, width=190, height=30)
self.make_label(text='Player').place(x=50, y=105, width=100, height=30)
self.make_label(text='Computer').place(x=250, y=105, width=100, height=30)
self.command_var = tk.StringVar(self.frame, name='command', value="Let's start.")
self.make_label(
textvariable=self.command_var, font=SECONDARY_FONT,
).place(x=115, y=180, height=40, width=180)
self.player_guess_var = tk.IntVar(self.frame, name='player_guess')
ttk.Spinbox(
self.frame, justify='center', font=MAIN_FONT,
textvariable=self.player_guess_var, from_=0, to=max_number-1,
).place(x=160, y=250, height=40, width=90)
ttk.Button(
self.frame, text='Guess', command=play_hook,
).place(x=160, y=300, height=60, width=90)
def show_command(self, command: str) -> None:
self.command_var.set(command)
def show_turn(self, index: int) -> None:
self.turn_var.set(index)
def make_label(self, **kwargs) -> tk.Label:
label_kwargs = {
'master': self.frame,
'anchor': 'center',
'background': '#eee',
'font': MAIN_FONT,
**kwargs,
}
return ttk.Label(**label_kwargs)
class WinnerView:
def __init__(
self, parent: tk.Tk, user_won: bool, result: str, restart_hook: Callable[[], None],
) -> None:
background = 'green' if user_won else 'red'
self.frame = tk.Frame(parent, background=background)
ttk.Label(
self.frame, text='Do you want to play again?',
anchor='center', background='#eee', font=MAIN_FONT,
).place(x=10, y=120, width=380, height=30)
ttk.Button(self.frame, text='YES', command=restart_hook,).place(x=100, y=180, width=90, height=70)
ttk.Button(self.frame, text='NO', command=parent.destroy).place(x=200, y=180, width=90, height=70)
ttk.Label(self.frame, text=result, anchor='center').place(x=10, y=270, width=380)
if __name__ == '__main__':
GameProgram().run()
-
1\$\begingroup\$ Thank you very much for your review. Corrected typo and removed math straight away. \$\endgroup\$Jakub– Jakub2021年10月20日 19:58:15 +00:00Commented Oct 20, 2021 at 19:58
Explore related questions
See similar questions with these tags.