3
\$\begingroup\$

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
Question_2,Answer_2,Knowledge_Level_2
...

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
Bangladesh,Dhaka,0
Barbados,Bridgetown,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
Burkina Faso,Ouagadougou,0
Burundi,Bujumbura,0
Cabo Verde,Praia,0
Cambodia,Phnom Penh,0
Cameroon,Yaounde,0
Canada,Ottawa,0
Central African Republic,Bangui,0
Chad,N'Djamena,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
Ecuador,Quito,0
Egypt,Cairo,0
El Salvador,San Salvador,0
Equatorial Guinea,Malabo,0
Eritrea,Asmara,0
Estonia,Tallinn,0
Ethiopia,Addis Ababa,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
Grenada,Saint George's,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
Iraq,Baghdad,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
Liechtenstein,Vaduz,0
Lithuania,Vilnius,0
Luxembourg,Luxembourg,0
Macedonia(FYROM),Skopje,0
Madagascar,Antananarivo,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
Pakistan,Islamabad,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
Saint Vincent and the Grenadines,Kingstown,0
Samoa,Apia,0
San Marino,San Marino,0
Sao,Tome and Principe São Tomé,0
Saudi Arabia,Riyadh,0
Senegal,Dakar,0
Serbia,Belgrade,0
Seychelles,Victoria,0
Sierra Leone,Freetown,0
Singapore,Singapore,0
Slovakia,Bratislava,0
Slovenia,Ljubljana,0
Solomon Islands,Honiara,0
Somalia,Mogadishu,0
South Africa,Pretoria,0
South Korea,Seoul,0
South Sudan,Juba,0
Spain,Madrid,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
Trinidad and Tobago,Port of Spain,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**
This program will help you train your memory on facts and trivia by the use
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

Question_1,Answer_1,Knowledge_Level_1
Question_2,Answer_2,Knowledge_Level_2
...

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
    from tkinter.filedialog import askopenfilename, asksaveasfilename
except ImportError:
    import Tkinter as tk
    from tkFileDialog import askopenfilename, asksaveasfilename
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()

def read_cards(text):
    """
    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)

    >>> read_cards('A,B,0\\nC,D,0')
    [('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'
    >>> format_cards_to_write(read_cards(x)) == x
    True

    """

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

def load_cards():
    global CARDS
    filename = askopenfilename()
    with open(filename) as f:
        CARDS = read_cards(f.read())

def save_progress():
    filename = asksaveasfilename()
    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()

load_button = tk.Button(root, text="Load Cards", command=load_cards)
load_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)
def check_answer_correct(ev):
    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())

root.bind('<Key>', check_answer_correct)

doctest.testmod()
root.mainloop()
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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):
        question, answer, *score = line.split(',')
        return card(question, answer, int(score[0]) if score else 0)
    

    in Python 3 or

    def parse_card(line):
        try:
            question, answer, score = line.split(',')
        except ValueError:
            question, answer = line.split(',')
            score = 0
        return card(question, answer, int(score))
    

    in Python 2.

\$\endgroup\$
2
  • \$\begingroup\$ How could the global CARDS be removed? Taking the cards as arguments would only move the global around. Good points all around. \$\endgroup\$
    – Caridorc
    Apr 9, 2017 at 8:57
  • \$\begingroup\$ @Caridorc What I meant was to avoid using the global keyword. \$\endgroup\$ Apr 9, 2017 at 9:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.