1
+ """
2
+ This file defines the GUI for running Karel programs.
3
+
4
+ Original Author: Nicholas Bowman
5
+ Credits: Kylie Jue
6
+ License: MIT
7
+ Version: 1.0.0
8
+ Email: nbowman@stanford.edu
9
+ Date of Creation: 10/1/2019
10
+ Last Modified: 3/31/2020
11
+ """
12
+
13
+ import tkinter as tk
14
+ from karel .kareldefinitions import *
15
+ from karel .KarelCanvas import KarelCanvas
16
+ from time import sleep
17
+ from tkinter .filedialog import askopenfilename
18
+ from tkinter .messagebox import showerror , showwarning
19
+ import os
20
+ import traceback as tb
21
+ import inspect
22
+ import importlib .util
23
+ import sys
24
+
25
+
26
+ class KarelApplication (tk .Frame ):
27
+ def __init__ (self , karel , world , code_file , master = None , window_width = 800 , window_height = 600 , canvas_width = 600 , canvas_height = 400 ):
28
+ # set window background to contrast white Karel canvas
29
+ master .configure (background = LIGHT_GREY )
30
+
31
+ # configure location of canvas to expand to fit window resizing
32
+ master .rowconfigure (0 , weight = 1 )
33
+ master .columnconfigure (1 , weight = 1 )
34
+
35
+ # set master geometry
36
+ master .geometry (str (window_width ) + "x" + str (window_height ))
37
+
38
+ super ().__init__ (master , background = LIGHT_GREY )
39
+
40
+ self .karel = karel
41
+ self .world = world
42
+ self .code_file = code_file
43
+ if not self .load_student_module ():
44
+ master .destroy ()
45
+ return
46
+ self .icon = DEFAULT_ICON
47
+ self .window_width = window_width
48
+ self .window_height = window_height
49
+ self .canvas_width = canvas_width
50
+ self .canvas_height = canvas_height
51
+ self .master = master
52
+ self .master .title (self .module_name )
53
+ self .set_dock_icon ()
54
+ self .inject_namespace ()
55
+ self .grid (row = 0 , column = 0 )
56
+ self .create_menubar ()
57
+ self .create_canvas ()
58
+ self .create_buttons ()
59
+ self .create_slider ()
60
+ self .create_status_label ()
61
+
62
+ def set_dock_icon (self ):
63
+ # make Karel dock icon image
64
+ img = tk .Image ("photo" , file = "./karel/icon.png" )
65
+ self .master .tk .call ('wm' , 'iconphoto' , self .master ._w , img )
66
+
67
+ def load_student_module (self ):
68
+ # This process is used to extract a module from an arbitarily located
69
+ # file that contains student code
70
+ # Adapted from https://stackoverflow.com/questions/67631/how-to-import-a-module-given-the-full-path
71
+ self .base_filename = os .path .basename (self .code_file )
72
+ self .module_name = os .path .splitext (self .base_filename )[0 ]
73
+ spec = importlib .util .spec_from_file_location (self .module_name , os .path .abspath (self .code_file ))
74
+ try :
75
+ self .mod = importlib .util .module_from_spec (spec )
76
+ spec .loader .exec_module (self .mod )
77
+ except Exception as e :
78
+ # Handle syntax errors and only print location of error
79
+ print ("here" )
80
+ print ("\n " .join (tb .format_exc (limit = 0 ).split ("\n " )[1 :]))
81
+ return False
82
+
83
+ # Do not proceed if the student has not defined a main function
84
+ if not hasattr (self .mod , "main" ):
85
+ print ("Couldn't find the main() function. Are you sure you have one?" )
86
+ return False
87
+
88
+ return True
89
+
90
+ def create_menubar (self ):
91
+ menubar = tk .Menu (self .master )
92
+
93
+ fileMenu = tk .Menu (menubar , tearoff = False )
94
+ menubar .add_cascade (label = "File" , menu = fileMenu )
95
+ fileMenu .add_command (label = "Exit" , underline = 1 ,
96
+ command = self .master .quit , accelerator = "Cmd+W" )
97
+
98
+ iconmenu = tk .Menu (menubar , tearoff = 0 )
99
+ menubar .add_cascade (label = "Select Icon" , menu = iconmenu )
100
+
101
+ iconmenu .add_command (label = "Karel" , command = lambda : self .set_icon ("karel" ))
102
+ iconmenu .add_command (label = "Simple" , command = lambda : self .set_icon ("simple" ))
103
+
104
+ self .bind_all ("<Command-w>" , self .quit )
105
+
106
+ self .master .config (menu = menubar )
107
+
108
+ def quit (self , event ):
109
+ sys .exit (0 )
110
+
111
+ def set_icon (self , icon ):
112
+ self .canvas .set_icon (icon )
113
+ self .canvas .redraw_karel ()
114
+
115
+ def create_slider (self ):
116
+ """
117
+ This method creates a frame containing three widgets:
118
+ two labels on either side of a scale slider to control
119
+ Karel execution speed.
120
+ """
121
+ self .slider_frame = tk .Frame (self , bg = LIGHT_GREY )
122
+ self .slider_frame .grid (row = 3 , column = 0 , padx = PAD_X , pady = PAD_Y , sticky = "ew" )
123
+
124
+ self .fast_label = tk .Label (self .slider_frame , text = "Fast" , bg = LIGHT_GREY )
125
+ self .fast_label .pack (side = "right" )
126
+
127
+ self .slow_label = tk .Label (self .slider_frame , text = "Slow" , bg = LIGHT_GREY )
128
+ self .slow_label .pack (side = "left" )
129
+
130
+ self .speed = tk .IntVar ()
131
+
132
+ self .scale = tk .Scale (self .slider_frame , orient = tk .HORIZONTAL , variable = self .speed , showvalue = 0 )
133
+ self .scale .set (self .world .init_speed )
134
+ self .scale .pack ()
135
+
136
+ def create_canvas (self ):
137
+ """
138
+ This method creates the canvas on which Karel and Karel's
139
+ world are drawn.
140
+ """
141
+ self .canvas = KarelCanvas (self .canvas_width , self .canvas_height , self .master , world = self .world , karel = self .karel )
142
+ self .canvas .grid (column = 1 , row = 0 , sticky = "NESW" )
143
+ self .canvas .bind ("<Configure>" , lambda t : self .canvas .redraw_all ())
144
+
145
+ def create_buttons (self ):
146
+ """
147
+ This method creates the three buttons that appear on the left
148
+ side of the screen. These buttons control the start of Karel
149
+ execution, resetting Karel's state, and loading new worlds.
150
+ """
151
+ self .program_control_button = tk .Button (self , highlightthickness = 0 , highlightbackground = 'white' )
152
+ self .program_control_button ["text" ] = "Run Program"
153
+ self .program_control_button ["command" ] = self .run_program
154
+ self .program_control_button .grid (column = 0 , row = 0 , padx = PAD_X , pady = PAD_Y , sticky = "ew" )
155
+
156
+ self .load_world_button = tk .Button (self , highlightthickness = 0 , text = "Load World" , command = self .load_world )
157
+ self .load_world_button .grid (column = 0 , row = 2 , padx = PAD_X , pady = PAD_Y , sticky = "ew" )
158
+
159
+ def create_status_label (self ):
160
+ """
161
+ This function creates the status label at the bottom of the window.
162
+ """
163
+ self .status_label = tk .Label (self .master , text = "Welcome to Karel!" , bg = LIGHT_GREY )
164
+ self .status_label .grid (row = 1 , column = 0 , columnspan = 2 )
165
+
166
+ def karel_action_decorator (self , karel_fn ):
167
+ def wrapper ():
168
+ # execute Karel function
169
+ karel_fn ()
170
+ # redraw canavs with updated state of the world
171
+ self .canvas .redraw_karel ()
172
+ # delay by specified amount
173
+ sleep (1 - self .speed .get () / 100 )
174
+ return wrapper
175
+
176
+ def beeper_action_decorator (self , karel_fn ):
177
+ def wrapper ():
178
+ # execute Karel function
179
+ karel_fn ()
180
+ # redraw canavs with updated state of the world
181
+ self .canvas .redraw_beepers ()
182
+ self .canvas .redraw_karel ()
183
+ # delay by specified amount
184
+ sleep (1 - self .speed .get () / 100 )
185
+ return wrapper
186
+
187
+ def corner_action_decorator (self , karel_fn ):
188
+ def wrapper (color ):
189
+ # execute Karel function
190
+ karel_fn (color )
191
+ # redraw canvas with updated state of the world
192
+ self .canvas .redraw_corners ()
193
+ self .canvas .redraw_beepers ()
194
+ self .canvas .redraw_karel ()
195
+ # delay by specified amount
196
+ sleep (1 - self .speed .get () / 100 )
197
+ return wrapper
198
+
199
+ def inject_namespace (self ):
200
+ """
201
+ This function is responsible for doing some Python hackery
202
+ that associates the generic commands the student wrote in their
203
+ file with specific commands relating to the Karel object that exists
204
+ in the world.
205
+ """
206
+
207
+ self .mod .turn_left = self .karel_action_decorator (self .karel .turn_left )
208
+ self .mod .move = self .karel_action_decorator (self .karel .move )
209
+ self .mod .pick_beeper = self .beeper_action_decorator (self .karel .pick_beeper )
210
+ self .mod .put_beeper = self .beeper_action_decorator (self .karel .put_beeper )
211
+ self .mod .facing_north = self .karel .facing_north
212
+ self .mod .facing_south = self .karel .facing_south
213
+ self .mod .facing_east = self .karel .facing_east
214
+ self .mod .facing_west = self .karel .facing_west
215
+ self .mod .not_facing_north = self .karel .not_facing_north
216
+ self .mod .not_facing_south = self .karel .not_facing_south
217
+ self .mod .not_facing_east = self .karel .not_facing_east
218
+ self .mod .not_facing_west = self .karel .not_facing_west
219
+ self .mod .front_is_clear = self .karel .front_is_clear
220
+ self .mod .beepers_present = self .karel .beepers_present
221
+ self .mod .no_beepers_present = self .karel .no_beepers_present
222
+ self .mod .beepers_in_bag = self .karel .beepers_in_bag
223
+ self .mod .no_beepers_in_bag = self .karel .no_beepers_in_bag
224
+ self .mod .front_is_blocked = self .karel .front_is_blocked
225
+ self .mod .left_is_clear = self .karel .left_is_clear
226
+ self .mod .left_is_blocked = self .karel .left_is_blocked
227
+ self .mod .right_is_clear = self .karel .right_is_clear
228
+ self .mod .right_is_blocked = self .karel .right_is_blocked
229
+ self .mod .paint_corner = self .corner_action_decorator (self .karel .paint_corner )
230
+ self .mod .corner_color_is = self .karel .corner_color_is
231
+
232
+ def disable_buttons (self ):
233
+ self .program_control_button .configure (state = "disabled" )
234
+ self .load_world_button .configure (state = "disabled" )
235
+
236
+ def enable_buttons (self ):
237
+ self .program_control_button .configure (state = "normal" )
238
+ self .load_world_button .configure (state = "normal" )
239
+
240
+ def display_error_traceback (self , e ):
241
+ print ("Traceback (most recent call last):" )
242
+ display_frames = []
243
+ # walk through all the frames in stack trace at time of failure
244
+ for frame , lineno in tb .walk_tb (e .__traceback__ ):
245
+ frame_info = inspect .getframeinfo (frame )
246
+ # get the name of the file corresponding to the current frame
247
+ filename = frame_info .filename
248
+ # Only display frames generated within the student's code
249
+ if self .base_filename in filename :
250
+ display_frames .append ((frame , lineno ))
251
+
252
+ print (("" .join (tb .format_list (tb .StackSummary .extract (display_frames )))).strip ())
253
+ print (f"{ type (e ).__name__ } : { str (e )} " )
254
+
255
+ def run_program (self ):
256
+ # Error checking for existence of main function completed in prior file
257
+ try :
258
+ self .status_label .configure (text = "Running..." , fg = "brown" )
259
+ self .disable_buttons ()
260
+ self .mod .main ()
261
+ self .status_label .configure (text = "Finished running." , fg = "green" )
262
+
263
+ except (KarelException , NameError ) as e :
264
+ # Generate popup window to let the user know their program crashed
265
+ self .status_label .configure (text = "Program crashed, check console for details." , fg = "red" )
266
+ self .display_error_traceback (e )
267
+ self .update ()
268
+ showwarning ("Karel Error" , "Karel Crashed!\n Check the terminal for more details." )
269
+
270
+ finally :
271
+ # Update program control button to force user to reset world before running program again
272
+ self .program_control_button ["text" ] = "Reset World"
273
+ self .program_control_button ["command" ] = self .reset_world
274
+ self .enable_buttons ()
275
+
276
+ def reset_world (self ):
277
+ self .karel .reset_state ()
278
+ self .world .reset_world ()
279
+ self .canvas .redraw_all ()
280
+ self .status_label .configure (text = "Reset to initial state." , fg = "black" )
281
+ # Once world has been reset, program control button resets to "run" mode
282
+ self .program_control_button ["text" ] = "Run Program"
283
+ self .program_control_button ["command" ] = self .run_program
284
+ self .update ()
285
+
286
+ def load_world (self ):
287
+ filename = askopenfilename (initialdir = "../worlds" , title = "Select Karel World" , filetypes = [("Karel Worlds" , "*.w" )], parent = self .master )
288
+ # User hit cancel and did not select file, so leave world as-is
289
+ if filename == "" : return
290
+ self .world .reload_world (filename = filename )
291
+ self .karel .reset_state ()
292
+ self .canvas .redraw_all ()
293
+ # Reset speed slider
294
+ self .scale .set (self .world .init_speed )
295
+ self .status_label .configure (text = f"Loaded world from { os .path .basename (filename )} ." , fg = "black" )
296
+
297
+ # Make sure program control button is set to 'run' mode
298
+ self .program_control_button ["text" ] = "Run Program"
299
+ self .program_control_button ["command" ] = self .run_program
0 commit comments