Python GUI 020 – Transpo Solver, part 2 – Preparation

I wanted to clean up the GUI interface system, and take out all of the functions that are pretty much common across all of the CON solvers. This includes the file open and save functions, etc. I managed to move some of them into cons_gui_utils.py, but a few others turned out to be problematic. This is preparation for establishing a skeleton GUI framework for copy-pasting at the beginning of each new project, Transpo Solver being the first real one. Just for clarity, I’m not repeating the full Python source files here. If you want the latest versions, let me know.

The stuff already in cons_gui_utils.py was:

ClassConRecords
ConTitleFields
process_cons_processed_file()
get_filename()
initial_parse_cons_processed()
save_con_file()
show_about_box()
popup_msg()

The function targets for the move were:

show_frames()
test_record_endpoints()
select_file()
save_file()
check_have_matching_records()
meets_display_setting()
set_display_settings()
set_hillclimbing_settings()
select_con_no()

Globals to also be moved:

CONS_FILE_PATH = ‘path to CON files’
SAVEFILENAMEADD = ‘_processed’
show_frame_borders = False
display_settings = [1, 1, 1]
hillclimbing_settings = [12, 5000, 500]
edge_conditions = [0, 0, 0]

Most of the functions and global vars that could be moved didn’t require any modifications, and just meant that I’d have to add them to the import list in the main gui.py file. The file open function needed a wrapper though, in order to move the call to display the record data from inside the original function now in the con_gui_utils.py, to be in the main gui.py where the display function remained located.

def select_and_display(root, cfilepath, entries, con_type):
... global loaded_file

... result, loaded_file = select_file(root, cfilepath, entries,
....... con_type, window_title)

... if result:
....... display_entry(root, root.con_records.recno, entries,

.......... root.con_records.records[root.con_records.recno])

The call from the file menu was then changed as follows:

file_menu.add_command(label='Open', command = lambda: root.select_and_display(CONS_FILE_PATH, entries, CON_TYPE))

where CON_TYPE has to change from a string:

CON_TYPE = 'cryptarithm'.lower()

to a list of strings:

CON_TYPE = ['cryptarithm']

This will be required for compatibility with Transpo:

CON_TYPE = ['columnar', 'railfence', 'myszkowski', 'route']

meets_display_setting() then has to change to check whether the current record being checked is of one of the types specified in CON_TYPE:

def meets_display_setting(d, s, ctype, con_type):

... is_type = False
... for c in con_type:

....... # Got matching type
....... if c.lower() in ctype.lower(): is_type = True

... if not is_type: return False

... # Check matching Done status
... if display_settings[1] and d == 1: return True

... # Check matching Skip status
... if display_settings[2] and s == 1: return True

... # Check matching To Process
... if display_settings[0] and d != 1 and s != 1: return True

... return False # Else, not a matching record

check_have_matching_records() is used to find the first legal record in the database, the last legal record, and the total number of legal records (that is, if we’re looking for all cryptarithms that are not marked Done). There aren’t any real changes to this method, outside of the modified call to meets_display_setting(), with the addition of the con_type parameter.

def check_have_matching_records(root, entries, con_type):
... i, first, last, cnt = -1, -1, -1, 0
... for record in root.con_records.records:
....... i += 1
....... done = record[1][ConTitleFields.done.value]
....... skip = record[1][ConTitleFields.skip.value]
....... ctype = record[1][ConTitleFields.con_type.value]
....... if meets_display_setting(done, skip, ctype, con_type):
........... cnt += 1
........... if first == -1: first = i
........... last = i

... return first, last, cnt

I decided to add a little nicety to show if the file needs to be saved or not.

The project name is stored in cryptarithm_gui.py as:

... window_title = 'Crypta Solver'

select_file() accepts window_title as a parameter, and it is displayed in the window bar along with the “_processed.txt” file that the user selects when opening the database.

root.title('%s - %s' % (window_title, root.con_records.filename))

Example:

--> Crypta Solver - MA2023_processed.txt

Then, when the user steps through the CONs with the NEXT and PREV buttons, store_updated_record() checks whether ‘*’ has already been prepended to the window title. If not, it adds it.

... title = root.title()
... if '*' not in title:
....... root.title('%s%s' % ('*', title))

New example out, showing file was modified:

--> *Crypta Solver - MA2023_processed.txt

Finally, when save_file() is called to save the current changes to the “_processed.txt” file, I re-save the title minus the “*” marker.

... save_con_file(root, filename)
... root.title('%s - %s' % (window_title, root.con_records.filename))
... popup_msg(root, 'File saved.')

As mentioned above, there were a few functions that I couldn’t get to work properly in cons_gui_utils.py. These, and their associated global variables, were:

set_display_settings()
set_hillclimbing_settings()
select_con_no()

display_settings = [1, 1, 1]
hillclimbing_settings = [12, 5000, 500]

Each of these three functions have one thing in common. They include custom pop-up boxes. set_display_settings() and set_hillclimbing_settings() both use a simple box widget with checkbox widgets and an ok button. select_con_no() has a listbox widget. After being moved into cons_gui_utils.py, what would happen is that the user (me) would do something that caused the box to close (clicking “Ok” or clicking on the “X” button in the upper right corner), and destroying the widget.

For some reason, tkinter would then prevent interaction with the main gui window, and not allow print output to the Idle window. If I closed the main GUI window, then any print statements in the queue would be dumped to the Idle window. In other words, using set_display_settings(), set_hillclimbing_settings() and select_con_no() would cause the GUI to stop working right.

Since I don’t understand the problem and can’t fix it, my only option (as I see it), is to leave those functions and global variables in the gui.py files, and allow them to be redundant across all of the different projects.

Oddly, there’s no problem with the other functions that use the built-in tkinter window widgets like filedialog and showinfo.

Then, as I was going through the code with an eye towards making changes, I realized that I could simplify the “close window” procedure by just having it call the function for exiting the program.

Was:

... self.protocol('WM_DELETE_WINDOW', self.on_closing)

... def on_closing(root):
....... if loaded_file:
........... root.confirm_exit()
....... else:
........... root.destroy()

Is now:

... self.protocol('WM_DELETE_WINDOW', self.confirm_exit)

Of course, while testing the Crypta Solver to make sure everything worked right, I caught some existing bugs, and ended up introducing a new one that completely corrupted the “_processed.txt” files when they got saved. This meant reparsing the two corrupted files, and using them for making sure I corrected the older bugs. None of those bugs were particularly important or need to be documented here. But, there are two bugs I haven’t tried fixing yet that show up in the normal cryptarithm parser for division and roots. The one for division somehow slipped past me when I wrote about it in the normal parser document. The one in the roots section is known, but a bit tricky to fix. Fortunately, I can edit the parsed instructions from the GUI window before clicking the Solve button, so they’re not insurmountable issues. I’ll get to both bugs when I get more sleep.

The one other bug worth mentioning that I did fix was in the function has_leading_zero(), which checks whether a number has a leading zero digit. The problem comes in when the number is only one digit long, in which case it’s ok for it to be “0”. I could turn this into a for-loop (in range(1, 6, 2)), test for the length of the number first, and then if it’s greater than 1 test for the leading 0, and I may do that when I come back to Crypta Solver for additional testing. Right now, the new version of the function is:

def has_leading_zero(list_val, key):

... l1 = key[list_val[1][0]]
... l2 = key[list_val[3][0]]
... l3 = key[list_val[5][0]]

... if len(list_val[1]) > 1 and l1 == 0: return True
... if len(list_val[3]) > 1 and l2 == 0: return True
... if len(list_val[5]) > 1 and l3 == 0: return True

... if list_val[0] == 3:
....... if len(list_val[7]) > 0 and key[list_val[7][0]] == 0:

........... return True

... return False

Proposed version:

def has_leading_zero(list_val, key):

... for i in range(1, 6, 2):
....... if len(list_val[i]) > 1:
........... if key[list_val[i][0]] == 0: return True

... if list_val[0] == 3:
....... if len(list_val[7]) > 0 and key[list_val[7][0]] == 0:

........... return True

... return False

Oh, ok, one more thing. I got tired of constantly choosing the wrong file to open when testing Crypta Tester. That is, say I have two files, one is the raw CONS, and the other is the parsed version:

MA2023.txt
MA2023_processed.txt

I need to open MA2023_processed.txt, but I’d keep selecting MA2023.txt, and then get errors because the expected formatting is missing. So, I changed the following line:

file_types = (('text files', '*.txt'), ('All files', '*.*'))

to:

SAVEFILENAMEADD = '_processed'
file_types = (('text files', '*'+SAVEFILENAMEADD+'.txt'),

............. ('All files', '*.*'))

—-

Yeah, this is getting chaotic. I need help.

The cryptarithm division bug works like this:

Say I have the following equation:

CREOR / ART = SUN; - CRU = EOR; - ECE = RT

There should be one thing that pops out after a little inspection. That is, while the quotient (SUN) has three “digits”, there are only two “subtractive elements.” Reconstructing the original division problem, we get this:

........SUN
ART / -----
..... CREOR
..... CRU
..... -----
....... EOR
....... ECE
..... -----
........ RT

By process of elimination, we can tell that U = 0. But, how can we determine this programmatically?

I want to say that if I test for the matching of the final letters of CREOR and EOR, I’d get an overlap of 2, and that would be the end of it. But, what if I get this kind of situation?

.....LYYM
/--------
. CCCCCCC
. BTYR
---------
.... CCCC
.... DDDD
---------
...... XX

Now, Y = 0, but I don’t know that from within the program, and doing a pure letter match I can’t easily tell the different between the above, this,

.....LYYM
/--------
. CCCCCCC
. BTYR
---------
.. CCCC
.. DDDD
---------
...... XX

and,

.....LYYM
/--------
. CCCCCCC
. BTYR
---------
... CCCC
... DDDD
---------
...... XX

and,

.....LYYM
/--------
. CCCCCCC
. BTYR
---------
.... CCCC
.... DDDD
---------
...... XX

Ultimately, I want to parse my above example:

CREOR / ART = SUN; - CRU = EOR; - ECE = RT

to:

2|ART|*|S|=|CRU
2|ART|*|U|=|U
2|ART|*|N|=|ECE
2|CRE|-|CRU|=|E
2|EOR|-|ECE|=|RT

Where U = 0, and ART * 0 = 0.

I’m fine if the quotient doesn’t contain a 0, but I’m stuck otherwise. The thing is, this is an issue with the parser section, which will give me:

2|ART|*|U|=|ECE

I can edit this fairly easily in the solver before clicking the “Solve” button to get the correct solution, so I can live with the bug. I can live with it, but I’d prefer a more elegant solution to the problem.

Next up: I don’t know.

Published by The Chief

Who wants to know?

Leave a comment

Design a site like this with WordPress.com
Get started