diff --git a/.gitignore b/.gitignore index 15c0e89..9d0f8cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ *.db -*.ttf -./out/*.png \ No newline at end of file +*.ttf \ No newline at end of file diff --git a/README.md b/README.md index 952e259..6797046 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,67 @@ # TrivialPursuitFramework +# Create your own cards This framework creates question cards for the game Trivial Pursuit. -Just create a questions.txt, think about six categories and start the `trivial_pursuit_creator.py` script. +Just create a `questions.txt` file and insert it to the questions folder, think about six categories, put them in a `categories.txt` file in the same folder and start the `trivial_pursuit_creator.py` script. +`python trivial_pursuit_creator.py` + +The script will lead you through the single steps (split up in the python scripts which can be found here) and helps to generate your printable question cards. The single scripts can also be run individually as described below in each section for the script. + +Please read the following sections of the *Create your own cards* chapter to make sure, your `questions.txt` and `categories.txt` are well-formed and you understand, what is happening. Please write an issues, if you have any problems. ![Question card front](readme_res/card.png) -*Example cards for a Star Trek Trivial Pursuit* +*Example cards for a German Star Trek Trivial Pursuit* + +If you execute the script, it will create the `out` folder which contains all question cards `frontX.png` (with six questions each) and their associated answer cards `backX.png`. It also creates printable card sheets in the `prints` folder, where the cards are organized in a way, that you can just print those sheets, cut a card, fold it up as shown below and glue it together, so that your are finished super fast! +This repository already contains both folders with a blank example card and sheet, so that you can better see what the script can do for you. +![Question card front](readme_res/print1.png) + +## questions.txt +Your `questions.txt` file must be look as follows: + +**Q: What is the name of the first president of the United States of America?** +**A: George Washington** +**C: H** + +**Q: What is the longest river on earth called?** +**A: Nile** +**C: G** + +... + +Every questions needs a question after a **Q:**, an answer at **A:** and one letter for the category **C:** + +An example can be found in the `questions.txt` provided in this repository. + +## categories.txt +The `categories.txt` file is much more simple. Just put six categories in there like that: +**H: History** +**G: Geopraphy** +**P: Politics** +**A: Actors** +**I: Inventions** +**M: Mathematics** + + +### Prepare your question cards +You have to prepare your cards with your *categories* and your *edition*, before filling them with questions. You can do this by changing `assets/back.png` and `assets/front.png` with an image editing program of your choice. Simply add the letters for your categories in the coloured ellipses. Use the same order as in the `categories.txt` file. Then add an edition name using the **Balmorall** font. You could for example find it [here](https://www.dafontfree.net/freefonts-balmoral-icg-f114221.htm) + +A question card consists of 6 questions. A questions has an answer and is assigned to a category defined in the `categories.txt` file. Each answer could contain a citation, especially useful for film quizes, which is always written in brackets (). + +# Running scripts individually +In this chapter, the single steps of the `trivial_pursuit_creator.py` script will be explained. + +##### questions_to_database.py +It starts with executing the `questions_to_database.py` script. This script reads the `questions.txt`, creates the SQLite database `python_sqlite.db` and write all questions, answers and their categories to the database. A good first introduction to python and SQLite can be found [here](https://www.sqlitetutorial.net/sqlite-python/sqlite-python-select/). If a questions i already in the database (questions is unique), this question will be skipped and a warning appears. Make sure, your `questions.txt` file is well-formed before starting this process. You can continue by pressing **ENTER** + +##### database_tools.py +Now the script check if there are any issues with questions, answer or the categories. The questions for example could be too long for a card, also the answers. The script takes this lengths from the `constants.py` file. It also checks things like, is there a question mark or a ... (for famous phrases which should be completed) at the end of a question or are the categories valid categories from the `categories.txt` file. The script will then print warnings and errors and will guide you to the next step. + +##### database_statistics.py +If there are no critical errors, it continues running the `database_statistics.py`. This script prints the number of available questions of each category and calculates how many cards can be created. Keep in mind, that each card contains six questions of the six different categories. + +##### create_cards.py +Now the card creation process can be started. The script takes questions from the database (one for each category) and draw them on a card. (They are taken shuffled, to switch of this shuffling, change the shuffle variable in `constants.py` to *False*) If there are no more questions of one category, the script stops and print statistics on what it has done. Check the ignored questions count to understand what category is missing some new questions. You should now see `frontX.png` and `backX.png` files in the `out` folder. In your answers you can provide a citation, which is just done by adding somethin in brackets () at the end of your answer. You can also write a special hint or continuing information in this brackets. Those citations or information will be written to an extra line for the answer. So you can force a new line, if you want to give further information. In the example questions, you can see this at the *Nile (6650km, Wikipedia)* answer. -### Creating question cards -A question card consists of 6 questions. A questions has an answer and is assigned to a category defined in the categories.txt file. Each answer could contain a citation, which is always written in brackets (). \ No newline at end of file +##### print_cards.py +Now all cards from the `out` folder are taken and put on a white piece of paper. Those can now be printed, cut and glued together and voila you have your own question card. \ No newline at end of file diff --git a/assets/back.png b/assets/back.png index 94b9e85..7100f60 100644 Binary files a/assets/back.png and b/assets/back.png differ diff --git a/constants.py b/constants.py index ac0bd47..2722b9c 100644 --- a/constants.py +++ b/constants.py @@ -1,7 +1,9 @@ -CITATIONS = True +CITATIONS = True # Do you use citations in () brackets behind your answers? +SHUFFLE = True # Allow shuffling of all questions when creating the cards -SHUFFLE = True # Allow shuffling of all questions # TODO save which questions were used to continue later +QUESTION_MAX_LINE_LENGTH = 63 # Maximum length of a question on a card for not beeing to long +ANSWER_MAX_LINE_LENGTH = 32 # Maximum length for an answer +NEXT_QUESTION_Y_SKIP = 103 # Pixel skip between two questions and answers -QUESTION_MAX_LINE_LENGTH = 46 -ANSWER_MAX_LINE_LENGTH = 32 -NEXT_QUESTION_Y_SKIP = 103 +CARDS_ON_SHEET_X = 2 # Not in use! +CARDS_ON_SHEET_Y = 3 # Not in use yet! diff --git a/create_cards.py b/create_cards.py index 90a1094..1fa9beb 100644 --- a/create_cards.py +++ b/create_cards.py @@ -22,7 +22,7 @@ def create_connection(db_file): print(e) -def split_lines(text, line_length): # TODO also split at "-" +def split_lines(text, line_length): """ Split a text in mutliple lines at " " and returns the lines as list :param text: Text to be split @@ -33,10 +33,12 @@ def split_lines(text, line_length): # TODO also split at "-" line_ends = [0] # All line endings, first line has to start at index 0 spaces = [m.start() for m in re.finditer(' ', text.strip(" "))] # Find all spaces an write indexes to a list spaces.append(len( - text)) # Append end of text to allow last line go to end of the last word, not the one before the last (the last space) + text)) # Append end of text to allow last line go to end of the last word, not the one before the last (the + # last space) for i in range(1, int(len(text) / line_length) + 2): # For how much lines there have to be line_ends.append(max([s for s in spaces if - s <= i * line_length]) + 1) # Last space which index is less than QUESTION_MAX_LINE_LENGTH + s <= i * line_length]) + 1) # Last space which index is less than + # QUESTION_MAX_LINE_LENGTH lines.append(text[line_ends[i - 1]:line_ends[ i]]) # Append line starting at last line_ends entry and going to line_ends[i] return lines @@ -53,8 +55,8 @@ def get_questions_of_categoriy(conn, category): cur.execute("SELECT * FROM qac WHERE category=?", category) question_rows = cur.fetchall() if SHUFFLE: - shuffle(question_rows) # Shuffle questions, so that there is no order recognizable, otherwise ordered by id - return question_rows # could also use dict_factory, but indexes are ok here + shuffle(question_rows) # Shuffle questions, so that there is no order recognizable, otherwise ordered by id + return question_rows # could also use dict_factory, but indexes are ok here def create_cards(): @@ -70,26 +72,26 @@ def create_cards(): fnt = ImageFont.truetype("arial.ttf", 25) with conn: # Keep connection open, as long as necessary categories, categories_long = get_categories_from_file() # get categories and long name - # TODO Write categories on blank front, back in the program in own method card_count = 1 while True: - vorne = Image.open("assets/front_cat.png") # Load assets - hinten = Image.open("assets/back_cat.png") - dv = ImageDraw.Draw(vorne) - dh = ImageDraw.Draw(hinten) + front = Image.open("assets/front.png") # Load assets + back = Image.open("assets/back.png") + dv = ImageDraw.Draw(front) + dh = ImageDraw.Draw(back) y = 100 for category in categories: # For each category print category questions = get_questions_of_categoriy(conn, category) # Get all questions of those category from database - for question_row in questions: # question_row[ ] represents one question 0 - ID, 1 - question, 2 - answer, 3 - category + for question_row in questions: # question_row[ ] represents one question 0 - ID, 1 - question, + # 2 - answer, 3 - category print question_row # print full question_row if (question_row[0] in used_ids) and ( questions.index(question_row) == (len(questions) - 1)): # if no new question was found print "\nSuccessfully created " + str(card_count - 1) + " cards. Those can be found in ./out" print str(count_questions(conn) - 6 * ( card_count - 1)) + " Questions have been ignored. (Due to inapplicable categories)" - print "No more questions available to fill new card!" # TODO Print size + print "No more questions available to fill new card!" print "Questions with the following id's were skipped: " used_ids.sort() for q_id in used_ids: @@ -117,11 +119,12 @@ def create_cards(): fill=(0, 0, 0)) # Align the line in the middle (at y) answer_lines = split_lines(answer.split("(")[0], - ANSWER_MAX_LINE_LENGTH) # Max. 2 lines (because of answer checker in database_tools), Citation will be rendered in a own line + ANSWER_MAX_LINE_LENGTH) # Max. 2 lines (because of answer checker + # in database_tools), Citation will be rendered in it's own line citation_exists = False if len(answer.split("(")) > 1: # append citation if there is one - citation = answer.split("(")[1] # TODO split citation in own column in database + citation = answer.split("(")[1] answer_lines.append(citation) citation_exists = True if len(answer_lines) == 3: # if there are two lines and a citation @@ -132,19 +135,19 @@ def create_cards(): elif len(answer_lines) == 2: # if there is just one line and a citation (a split was done) dh.text((620, y - 15), answer_lines[0], font=fnt, fill=(0, 0, 0)) # Align the line a the citation to y - if citation_exists: # 2nd line can be citation or just a second line + if citation_exists: # 2nd line can be citation or just a second line dh.text((620, y + 15), "(" + answer_lines[1], font=fnt, fill=(0, 0, 0)) else: - dh.text((620, y + 15), answer_lines[1], font=fnt,fill=(0, 0, 0)) + dh.text((620, y + 15), answer_lines[1], font=fnt, fill=(0, 0, 0)) else: # if there is just a line dh.text((620, y), answer_lines[0], font=fnt, fill=(0, 0, 0)) # Place the answer at y y = y + NEXT_QUESTION_Y_SKIP # go to the next question break - vorne.save( + front.save( "./out/front" + str(card_count) + ".png") # save front and back of the card with texts written on it - hinten.save("./out/back" + str(card_count) + ".png") + back.save("./out/back" + str(card_count) + ".png") card_count = card_count + 1 diff --git a/database_statistics.py b/database_statistics.py index 2ec2abe..0cb65a6 100644 --- a/database_statistics.py +++ b/database_statistics.py @@ -2,9 +2,12 @@ import sqlite3 -# https://www.sqlitetutorial.net/sqlite-python/sqlite-python-select/ def create_connection(db_file): - """ create a database connection to a SQLite database """ + """ + Create a database connection to a SQLite database + :param db_file: SQLite file containing the database + :return: Connection + """ try: connection = sqlite3.connect(db_file) return connection @@ -13,18 +16,33 @@ def create_connection(db_file): def count_categories(conn, category): + """ + Count questions of the given category + :param conn: Connection to the database + :param category: Wished category + :return: Count of questions of category + """ cur = conn.cursor() cur.execute("SELECT COUNT(id) FROM qac WHERE category=?", category) return cur.fetchall() def count_questions(conn): + """ + Count all questions + :param conn: Connection to the database + :return: Just the number of all questions + """ cur = conn.cursor() cur.execute("SELECT COUNT(id) FROM qac") return cur.fetchall()[0][0] def get_categories_from_file(): + """ + Reads the category string from the categories.txt file + :return: Tupel (Categories one letter, Category long name) + """ cat = [] cat_long = [] f = codecs.open("questions/categories.txt", "r", "utf-8") @@ -35,6 +53,10 @@ def get_categories_from_file(): def database_statistics(): + """ + Generates interesting statistics about questions count, database entries, ... + :return: Successfull execution + """ conn = create_connection("python_sqlite.db") categories, categories_long = get_categories_from_file() with conn: diff --git a/database_tools.py b/database_tools.py index 52c2297..30bb5ce 100644 --- a/database_tools.py +++ b/database_tools.py @@ -1,12 +1,15 @@ -# TODO function which can repair corrupted entries -import sqlite3 #TODO run database tools on txt before inserting to database +import sqlite3 import database_statistics from constants import ANSWER_MAX_LINE_LENGTH, QUESTION_MAX_LINE_LENGTH def create_connection(db_file): - """ create a database connection to a SQLite database """ + """ + Create a database connection to a SQLite database + :param db_file: SQLite database file + :return: Connection to the database + """ try: connection = sqlite3.connect(db_file) return connection @@ -15,6 +18,10 @@ def create_connection(db_file): def validate_questions(): + """ + Validates question entries on its length, if they have a questions mark at the end, ... + :return: Warning and Error count + """ print "Validating question entries..." conn = create_connection("python_sqlite.db") with conn: @@ -29,13 +36,13 @@ def validate_questions(): if len( question) > 3 * QUESTION_MAX_LINE_LENGTH: print "\033[91mError: Question of entry with ID: " + str( - question_id) + " is to long! Cards will be ugly! \033[0m" + question + " \033[91mHas length: " + str( - len(question)) + "/" + str(3 * QUESTION_MAX_LINE_LENGTH) + "\033[0m" + question_id) + " is to long! Cards will be ugly! \033[0m" + question + " \033[91mHas length: " + \ + str(len(question)) + "/" + str(3 * QUESTION_MAX_LINE_LENGTH) + "\033[0m" error_count = error_count + 1 any_error_or_warning = True - if question[-1] != "?" and question[-3:] != "...": # Question mark or citing ...s are missing + if question[-1] != "?" and question[-3:] != "...": # Question mark or citing ...s are missing any_error_or_warning = True - warning_count = warning_count + 1 # + warning_count = warning_count + 1 # if question[-1] == " ": print "\033[93mWarning: Question of entry with ID: " + str( question_id) + " has spaces attached to the back. \033[0m" + question @@ -51,6 +58,10 @@ def validate_questions(): def validate_answers(): + """ + Validates the answer entries, if they are to long, ... + :return: Warning and Error count + """ print "Validating answer entries..." conn = create_connection("python_sqlite.db") with conn: @@ -61,26 +72,24 @@ def validate_answers(): for entry in cur.fetchall(): answer_id = entry[0] answer = entry[1] - answer_cat = entry[2] any_error_or_warning = False - if len(answer.split("(")[0]) > 2 * ANSWER_MAX_LINE_LENGTH: # if both lines of the answer are to long in sum + if len(answer.split("(")[0]) > 2 * ANSWER_MAX_LINE_LENGTH: # if both lines of the answer are to long in sum print "\033[91mError: Answer of entry with ID: " + str( - answer_id) + " is too long! Cards will be ugly! \033[0m" + answer.split("(")[0] + " \033[91mHas length: " + str( - len(answer.split("(")[0])) + "/" + str(2 * ANSWER_MAX_LINE_LENGTH) + "\033[0m" # TODO Variable for max length + answer_id) + " is too long! Cards will be ugly! \033[0m" + answer.split("(")[ + 0] + " \033[91mHas length: " + str( + len(answer.split("(")[0])) + "/" + str( + 2 * ANSWER_MAX_LINE_LENGTH) + "\033[0m" error_count = error_count + 1 any_error_or_warning = True if len(answer.split("(")) > 1: - if len("(" + answer.split("(")[1]) > ANSWER_MAX_LINE_LENGTH: # if citation line is too long + if len("(" + answer.split("(")[1]) > ANSWER_MAX_LINE_LENGTH: # if citation line is too long print "\033[91mError: Citation of entry with ID: " + str( - answer_id) + " is too long! Cards will be ugly! \033[0m" + "(" + answer.split("(")[1] + " \033[91mHas length: " + str( - len("(" + answer.split("(")[1])) + "/" + str(ANSWER_MAX_LINE_LENGTH) + "\033[0m" # TODO Variable for max length + answer_id) + " is too long! Cards will be ugly! \033[0m" + "(" + answer.split("(")[ + 1] + " \033[91mHas length: " + str( + len("(" + answer.split("(")[1])) + "/" + str( + ANSWER_MAX_LINE_LENGTH) + "\033[0m" error_count = error_count + 1 any_error_or_warning = True - if answer_cat != "T" and (answer.find("(") < 0 or answer.find(")") < 0): # TODO just for this Star Trek thingy thing - any_error_or_warning = True - warning_count = warning_count + 1 - print "\033[93mWarning: Answer of entry with ID: " + str( - answer_id) + " is missing correct citation and is not of category: Trivia \033[0m" + answer # TODO not everyone will use citations! if any_error_or_warning: print("") # Organize Errors and Warnings in blocks grouped by id @@ -90,6 +99,10 @@ def validate_answers(): def validate_categories(): + """ + Validates if any questions is applied to a valid category from the categories file + :return: Warning and Error count + """ print "Validating category entries..." conn = create_connection("python_sqlite.db") with conn: @@ -99,7 +112,6 @@ def validate_categories(): warning_count = 0 for entry in cur.fetchall(): question_id = entry[0] - question = entry[1] category = entry[2] any_error_or_warning = False if category not in database_statistics.get_categories_from_file()[0]: diff --git a/out/back1.png b/out/back1.png new file mode 100644 index 0000000..9a10ae2 Binary files /dev/null and b/out/back1.png differ diff --git a/out/front1.png b/out/front1.png new file mode 100644 index 0000000..eae5d84 Binary files /dev/null and b/out/front1.png differ diff --git a/print_cards.py b/print_cards.py index 9cc2194..1f7ce14 100644 --- a/print_cards.py +++ b/print_cards.py @@ -1,4 +1,3 @@ -# TODO different paper formats?! variable for x times x cards on same sheet from PIL import Image diff --git a/prints/print1.png b/prints/print1.png new file mode 100644 index 0000000..47ad8e2 Binary files /dev/null and b/prints/print1.png differ diff --git a/questions/categories.txt b/questions/categories.txt index 8c26f45..72798e7 100644 --- a/questions/categories.txt +++ b/questions/categories.txt @@ -1,6 +1,6 @@ -C: Characters -T: Trivia -S: Species -V: Voyager -G: History -M: Missions \ No newline at end of file +H: History +G: Geopraphy +P: Politics +A: Actors +I: Inventions +M: Mathematics \ No newline at end of file diff --git a/questions/questions.txt b/questions/questions.txt index e69de29..9632d6b 100644 --- a/questions/questions.txt +++ b/questions/questions.txt @@ -0,0 +1,27 @@ +Q: What is the name of the first president of the United States of America? +A: George Washington +C: H + +Q: What is the longest river on earth called? +A: Nile (6650 km, Wikipedia) +C: G + +Q: What does NATO stand for? +A: North Atlantic Treaty Organization +C: P + +Q: Who invented the first car +A: Karl Benz +C: I + +Q: What can be found using the Sieve of Eratosthenes? +A: Prime numbers +C: M + +Q: Who played Obi-Wan Kenobi in George Lucas legendary space-opera film, Star Wars: Episode 1 - The Phantom Menace released in May 1999? +A: Ewan McGregor +C: A + +Q: The 2nd World War lasted from... +A: 1939 to 1945 +C: H \ No newline at end of file diff --git a/questions_to_database.py b/questions_to_database.py index 9851bad..019fde4 100644 --- a/questions_to_database.py +++ b/questions_to_database.py @@ -1,10 +1,15 @@ -# coding=utf-8 import codecs import sqlite3 from sqlite3 import Error +from os import path def create_connection(db_file): + """ + Creates a connection to the database + :param db_file: SQLite database file + :return: Connection to the database + """ try: connection = sqlite3.connect(db_file) return connection @@ -13,6 +18,12 @@ def create_connection(db_file): def create_qac(conn, qac): + """ + Inserts a QAC Triple to the Database + :param conn: Connection to the database (file) + :param qac: Question, Answer, Category Triple + :return: Last inserted row id + """ sql = ''' INSERT INTO qac(question,answer,category) VALUES(?,?,?) ''' cur = conn.cursor() @@ -20,8 +31,36 @@ def create_qac(conn, qac): return cur.lastrowid +def create_table(conn): + """ + Creates the database table if not existing and adds a unique constraint for questions + :param conn: Connection to the database + :return: if finished + """ + sql_create_table = '''create table qac ( + id integer primary key, + question text not null, + answer text not null, + category text not null +); +''' + sql_create_unique = '''create unique index qac_question_uindex on qac (question);''' # make question unique + cur = conn.cursor() + cur.execute(sql_create_table) + cur.execute(sql_create_unique) + + def questions_to_database(): + """ + Write the questions from questions.txt to the database + :return: if finished + """ + db_existing = path.exists("python_sqlite.db") + conn = create_connection("python_sqlite.db") + if not db_existing: + print "Created a new database for the questions! --> python_sqlite.db" + create_table(conn) f = codecs.open("questions/questions.txt", "r", "utf-8") question = "" @@ -40,8 +79,7 @@ def questions_to_database(): if line.find("C: ") >= 0: category = (line.split("C: ")[1].split("\r\n")[0]) qac_counter = qac_counter + 1 - if qac_counter == 3: - # on the same card # TODO Verify all questions have a category and are with short enough size etc! + if qac_counter == 3: # on the same card with conn: # Transaction try: print "\033[92mQuestion: \033[1m" + question + "\033[0m \033[92m was saved as ID: " \ diff --git a/readme_res/print1.png b/readme_res/print1.png new file mode 100644 index 0000000..074fb25 Binary files /dev/null and b/readme_res/print1.png differ diff --git a/trivial_pursuit_creator.py b/trivial_pursuit_creator.py index a200d0e..7b522c2 100644 --- a/trivial_pursuit_creator.py +++ b/trivial_pursuit_creator.py @@ -21,10 +21,11 @@ raw_input("Press \033[1mENTER\033[0m to create questions cards (front and back) in the ./out folder") cc.create_cards() raw_input( - "Press \033[1mENTER\033[0m to create printable sheets with 4 questions card, which can be cut, kinked and glued " + "Press \033[1mENTER\033[0m to create printable sheets with 4 questions cards, which can be cut, kinked and " + "glued " "together.") pc.print_cards() print "Congratulations. You can find the result in ./prints." else: print "There are " + str(error_count) + " Errors which have to be fixed to continue." - print "Card creation aborted. Please fix the errors in your database and start again." \ No newline at end of file + print "Card creation aborted. Please fix the errors in your database and start again."