# Guess The Number Game with GUI

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()


• 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 with str.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 be try_parse_int. Finally: returning 0 on failure is not a good idea. What if valueToCheck (which should be value_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 a self, and refers to a master variable that's undefined. The initialization of result_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 StringVars. Sometimes IntVar or BooleanVar are called for.
• You can simplify your difficulty logic by using 10**x where x 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 no from).
• 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 your Var 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)
self.frame, text='Low', variable=self.level_var, value=1,
).place(x=50, y=110, height=30, width=80)
self.frame, text='Medium', variable=self.level_var, value=2,
).place(x=160, y=110, height=30, width=80)
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)
self.frame, text='Player', variable=self.player_goes_first_var, value=True,
).place(x=100, y=190, height=30, width=80)
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()

• Thank you very much for your review. Corrected typo and removed math straight away. Oct 20, 2021 at 19:58