GUI flashcard training

Aim

This program will help you train your memory on facts and trivia by the use of flashcards.

How to use

Type start to start, then type the answer to the question of the flashcard shown above. If the answer is correct, you will automatically progress to the next card. If you do not know the answer, type ??? and the answer will be shown, type it in to continue.

Each time a card is:

• Guessed correctly: The knowledge of the card will INcrease by 1
• Resigned (By typing '???'): The knowledge of the card will DEcrease by 1

When the knowledge goes over the REPEATS_TO_LEARN (currently equal to 3) the card is removed from the deck of cards.

File Format

A deck of flashcards is required in the format of

Question_1,Answer_1,Knowledge_Level_1
...


For example:

Italy?,rome,0
USA?,new york,0
France,paris,0
...


The knowledge level always starts at 0 because it is assumed that the reader knows nothing about the cards when they start to use this program.

Before starting

Click the load button to load a deck of cards you want to practice (a file in the above format).

After finishing

Click the save button to save the new knowledge levels and deleted cards. You can save in a new file, so you can have multiple files representing your progress over multiple days.

Example file

If for example you want to learn about the capital cities of the world, you can use this file:

nations_and_capitals_cards

Afghanistan,Kabul,0
Albania,Tirana,0
Algeria,Algiers,0
Andorra,Andorra la Vella,0
Angola,Luanda,0
Antigua and Barbuda,Saint John's,0
Argentina,Buenos Aires,0
Armenia,Yerevan,0
Australia,Canberra,0
Austria,Vienna,0
Azerbaijan,Baku,0
Bahamas,Nassau,0
Bahrain,Manama,0
Belarus,Minsk,0
Belgium,Brussels,0
Belize,Belmopan,0
Benin,Porto-Novo,0
Bhutan,Thimphu,0
Bolivia,Sucre,0
Bosnia and Herzegovina,Sarajevo,0
Botswana,Gaborone,0
Brazil,Brasilia,0
Brunei Bandar Seri,Begawan,0
Bulgaria,Sofia,0
Burundi,Bujumbura,0
Cabo Verde,Praia,0
Cambodia,Phnom Penh,0
Cameroon,Yaounde,0
Central African Republic,Bangui,0
Chile,Santiago,0
China,Beijing,0
Colombia,Bogotá,0
Comoros,Moroni,0
Democratic Republic of the Congo,Kinshasa,0
Republic of the Congo,Brazzaville,0
Costa Rica,San Jose,0
Cote d'Ivoire,Yamoussoukro,0
Croatia,Zagreb,0
Cuba,Havana,0
Cyprus,Nicosia,0
Czech Republic,Prague,0
Denmark,Copenhagen,0
Djibouti,Djibouti(city),0
Dominica,Roseau,0
Dominican,Republic Santo Domingo,0
Egypt,Cairo,0
Equatorial Guinea,Malabo,0
Eritrea,Asmara,0
Estonia,Tallinn,0
Fiji,Suva,0
Finland,Helsinki,0
France,Paris,0
Gabon,Libreville,0
Gambia,Banjul,0
Georgia,Tbilisi,0
Germany,Berlin,0
Ghana,Accra,0
Greece,Athens,0
Guatemala,Guatemala City,0
Guinea,Conakry,0
Guinea-Bissau,Bissau,0
Guyana,Georgetown,0
Haiti,Port-au-Prince,0
Honduras,Tegucigalpa,0
Hungary,Budapest,0
Iceland,Reykjavik,0
India,New Delhi,0
Indonesia,Jakarta,0
Iran,Tehran,0
Ireland,Dublin,0
Israel,Jerusalem,0
Italy,Rome,0
Jamaica,Kingston,0
Japan,Tokyo,0
Jordan,Amman,0
Kazakhstan,Astana,0
Kenya,Nairobi,0
Kiribati,Tarawa,0
Kosovo,Pristina,0
Kuwait,Kuwait City,0
Kyrgyzstan,Bishkek,0
Laos,Vientiane,0
Latvia,Riga,0
Lebanon,Beirut,0
Lesotho,Maseru,0
Liberia,Monrovia,0
Libya,Tripoli,0
Lithuania,Vilnius,0
Luxembourg,Luxembourg,0
Macedonia(FYROM),Skopje,0
Malawi,Lilongwe,0
Malaysia,Kuala Lumpur,0
Maldives,Male,0
Mali,Bamako,0
Malta,Valletta,0
Marshall Islands,Majuro,0
Mauritania,Nouakchott,0
Mauritius,Port Louis,0
Mexico,Mexico City,0
Micronesia,Palikir,0
Moldova,Chisinau,0
Monaco,Monaco,0
Mongolia,Ulaanbaatar,0
Montenegro,Podgorica,0
Morocco,Rabat,0
Mozambique,Maputo,0
Myanmar(Burma),Naypyidaw,0
Namibia,Windhoek,0
Nauru,Yaren District,0
Nepal,Kathmandu,0
Netherlands,Amsterdam,0
New Zealand,Wellington,0
Nicaragua,Managua,0
Niger,Niamey,0
Nigeria,Abuja,0
North Korea,Pyongyang,0
Norway,Oslo,0
Oman,Muscat,0
Palau,Ngerulmud,0
Palestine,Jerusalem(East),0
Panama,Panama City,0
Papua,New Guinea Port Moresby,0
Paraguay, Asunción,0
Peru,Lima,0
Philippines,Manila,0
Poland,Warsaw,0
Portugal,Lisbon,0
Qatar,Doha,0
Romania,Bucharest,0
Russia,Moscow,0
Rwanda,Kigali,0
Saint,Kitts and Nevis Basseterre,0
Saint Lucia,Castries,0
Samoa,Apia,0
San Marino,San Marino,0
Sao,Tome and Principe São Tomé,0
Senegal,Dakar,0
Seychelles,Victoria,0
Sierra Leone,Freetown,0
Singapore,Singapore,0
Slovakia,Bratislava,0
Slovenia,Ljubljana,0
Solomon Islands,Honiara,0
South Africa,Pretoria,0
South Korea,Seoul,0
South Sudan,Juba,0
Sri Lanka,Sri Jayawardenepura Kotte,0
Sudan,Khartoum,0
Suriname,Paramaribo,0
Swaziland,Mbabane,0
Sweden,Stockholm,0
Switzerland,Bern,0
Syria,Damascus,0
Taiwan,Taipei,0
Tajikistan,Dushanbe,0
Tanzania,Dodoma,0
Thailand,Bangkok,0
Timor-Leste,Dili,0
Togo,Lomé,0
Tonga,Nukuʻalofa,0
Tunisia,Tunis,0
Turkey,Ankara,0
Turkmenistan,Ashgabat,0
Tuvalu,Funafuti,0
Uganda,Kampala,0
Ukraine,Kiev,0
United Arab Emirates,Abu Dhabi,0
United Kingdom,London,0
United States of America,Washington D.C.,0
Uruguay,Montevideo,0
Uzbekistan,Tashkent,0
Vanuatu,Port Vila,0
Vatican City,Vatican City,0
Venezuela,Caracas,0
Vietnam,Hanoi,0
Yemen,Sana'a,0
Zambia,Lusaka,0
Zimbabwe,Harare,0


Concerns

• Not possible to have ',' inside question or answer
• Weird workaround at line CARDS.remove([c for c in CARDS if card[0]==c[0] and card[1]==c[1]][0]) # Workaround to avoid comparing knowledge
• Cards are plain tuples, maybe better as Namedtuples?

And obviously any and all comments are welcome. Also, please note that the question text is the same as the code documentation (only the example file is added) just nicely formatted.

"""

**AIM**
of flashcards.

**HOW TO USE**

Type start (no quotes) to start, then type the answer to the question of the flashcard shown above.
If the answer is correct, you will automatically progress to the next card.
If you do not know the answer, type ??? (no quotes) and the answer will be shown, type it in to continue.

Each time a card is:
* Guessed correctly         : The knowledge of the card will INcrease by 1
* Resigned (By typing '???'): The knowledge of the card will DEcrease by 1

When the knowledge goes over the REPEATS_TO_LEARN (currently equal to {})
the card is removed from the deck of cards.

**FILE FORMAT**
A deck of flashcards is required in the format of

...

For example:

Italy?,rome,0
USA?,new york,0
France,paris,0
...

The knowledge level always starts at 0 because it is assumed that the reader knows nothing about the
cards when they start to use this program.

**BEFORE STARTING**
Click the load button to load a deck of cards you want to practice (a file in the above format).

**AFTER FINISHING**
Click the save button to save the new knowledge levels and deleted cards.
You can save in a new file, so you can have multiple files
representing your progress over multiple days.

"""
# Issues / To Do:
#  # Not possible to have ',' inside question or answer
#  # REPEATS_TO_LEARN and FONT not configurable from inside GUI
#  # Weird workaround at line         CARDS.remove( [ c for c in CARDS if card[0]==c[0] and card[1]==c[1] ][0] ) # WOìorkaround to avoid comparing knowledge
#  # Cards are plain tuples, maybe better as Namedtuples?

try:
import tkinter as tk
except ImportError:
import Tkinter as tk
import doctest
import random

FONT = (None, 50)
REPEATS_TO_LEARN = 3

def flatten(xss):
"""
>>> flatten([ [1,2], [3,4] ])
[1, 2, 3, 4]
"""
return [x for xs in xss for x in xs]

def pop_up(title, content):
"""
# http://stackoverflow.com/questions/17261028/how-do-i-make-a-pop-up-in-tkinter-when-a-button-is-clicked
Shows a pop-up.
"""
toplevel = tk.Toplevel()
label1 = tk.Label(toplevel, text=title, height=0, font=FONT)
label1.pack()
label2 = tk.Label(toplevel, text=content, height=0)
label2.pack()

"""
Converts from a multi-line string, where each line represents a card,
to a list of tuples, each representing a card.

The tuple is (Question:str, Answer:str, Knowledge:int)

[('A', 'B', 0), ('C', 'D', 0)]
"""
return [ (line.split(',')[0], line.split(',')[1], int(line.split(',')[2])) for line in text.split('\n') if line]

def format_cards_to_write(cards):
"""
Converts from a a list of tuples,
to a multi-line string, where each line represents a card,

The opposite of read_cards
>>> format_cards_to_write([('A', 'B', 0), ('C', 'D', 0)])
'A,B,0\\nC,D,0'
>>> x = 'A,B,0\\nC,D,0'
True

"""

return '\n'.join(','.join(card[:2])+','+str(card[2]) for card in cards)

global CARDS
with open(filename) as f:

def save_progress():
with open(filename, 'w+') as f:
f.write(format_cards_to_write(CARDS))

root = tk.Tk()
root.title("Typing trainer")

help_button = tk.Button(root, text="Help", command=lambda: pop_up("Flashcard training", __doc__.format(REPEATS_TO_LEARN)))
help_button.pack()

save_button = tk.Button(root, text="Save Progress", command=save_progress)
save_button.pack()

text_to_copy = tk.Label(root, text = "Load, then type \"start\"", font=FONT)
text_to_copy.pack()

typing_ground = tk.Entry(root, font=FONT)
typing_ground.pack()
typing_ground.focus_set()

points = tk.Label(root, text = "Score: 0", font=FONT)
points.pack()

card = ('Type start to start (after loading a deck of cards)', "start", None)
try:
CARDS
except: # Not defined
pop_up("Load Deck Before Usage", "Please load a deck of cards before starting, press the Help button for more information.")
if not CARDS:
text_to_copy['text'] = "Congratulations! All cards learned!"
return
global card
if typing_ground.get() == card[1]:
#flatten([c * (4-c[2]) for c in CARDS])
card = random.choice( flatten([[c] * (4-c[2]) for c in CARDS]) ) # Inversely weight on knowledge
typing_ground.delete(0, 'end')
text_to_copy['text'] = card[0]
points['text'] = "Score: " + str(int(points['text'].split(': ')[-1]) + 1)
CARDS.remove( card )
if card[2] <= REPEATS_TO_LEARN:
CARDS.append( (card[0], card[1], card[2] + 1) )
elif typing_ground.get() == "???":
text_to_copy['text'] = card[0] + ' -> ' + card[1]
print(CARDS, card)
CARDS.remove( [ c for c in CARDS if card[0]==c[0] and card[1]==c[1] ][0] ) # WOìorkaround to avoid comparing knowledge
CARDS.append( (card[0], card[1], card[2] - 1) )
print(card, typing_ground.get())

doctest.testmod()
root.mainloop()


Some random remarks that are too much to fit in a comment:

• You could return the CARDS and take them as parameters when loading/saving instead of relying on a global.
• If you don't mind working with generators, flatten = itertools.chain.from_iterable
• namedtuples can indeed simplify some of the code as they can be extended to provide "fancy" formating:

class card(namedtuple('card', 'question answer score')):
def __str__(self):
return '{}, {}, {}'.format(*self)


You can even add other helpers lile increase_knowledge or check_answer.

• You can use the shlex module to allow more characters in your card. You'll need to adapt a bit card formatting though, to quote things properly.

• Why require all those ugly 0 score in the original file, where it should be mandatory. I think trying to parse 2 or 3 items will help simplify writing cards:

def parse_card(line):
return card(question, answer, int(score[0]) if score else 0)


in Python 3 or

def parse_card(line):
try:

• How could the global CARDS be removed? Taking the cards as arguments would only move the global around. Good points all around. – Caridorc Apr 9 '17 at 8:57
• @Caridorc What I meant was to avoid using the global keyword. – Mathias Ettinger Apr 9 '17 at 9:10