1
\$\begingroup\$

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

screenshot

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()
Reinderien
70.9k5 gold badges76 silver badges256 bronze badges
asked Oct 20, 2021 at 14:18
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$
  • 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)
 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()
answered Oct 20, 2021 at 19:09
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thank you very much for your review. Corrected typo and removed math straight away. \$\endgroup\$ Commented Oct 20, 2021 at 19:58

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.