Let’s create a term-based quiz game using Python, SQLite, and PyWebIO. This game allows users to select a subject and guess letters of a term based on hints. The interface is interactive, providing real-time feedback.
Let’s get started by setting up the environment and downloading the necessary database.
Step 1: Create the Project Folder
First, you need a folder to store all your project files. Open a terminal or command prompt and create a folder named game
.
mkdir game
cd game
Step 2: Set Up a Virtual Environment
A virtual environment helps keep your project’s dependencies organized. Follow the instructions below based on your operating system.
For Windows:
python -m venv venv
venv\Scripts\activate
For Linux:
python3 -m venv venv
source venv/bin/activate
After activating the virtual environment, you are ready to install the necessary libraries.
Step 3: Install Required Libraries
This project requires a few Python libraries. You can install them using pip:
pip install pywebio
Now you have the essential tools installed.
Step 4: Set Up the Database
The quiz game will use an SQLite database to store terms and hints. You can download the sql code to build the database and table linked here: terms.sql. (create statement not listed here because there are 1500+ records)
Save that complete file as terms.sql in the root folder then run the python code below to build the database:
import sqlite3
def execute_sql_from_file(sql_file_path, db_name="terms_game.db"):
# Connect to SQLite database (or create it if it doesn't exist)
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
try:
# Open and read the SQL file with utf-8 encoding
with open(sql_file_path, 'r', encoding='utf-8') as sql_file:
sql_script = sql_file.read()
# Execute the SQL script
cursor.executescript(sql_script)
print(f"SQL script executed successfully, table 'terms' created in {db_name}.")
except sqlite3.Error as e:
print(f"An error occurred: {e}")
finally:
# Commit the changes and close the connection
conn.commit()
conn.close()
# Provide the path to the SQL file
sql_file_path = 'terms.sql'
# Execute the SQL script
execute_sql_from_file(sql_file_path)
The above code will use the terms.sql to build out and load the terms table in a database named: terms_game.db.
Once you have the database ready, we can move on to the main Python code.
Step 5: Write the Python Code
Here is the full code for the term-based quiz game. This code uses PyWebIO for the web interface and SQLite to manage the data.
import sqlite3
import random
from pywebio import start_server
from pywebio.input import actions
from pywebio.output import put_text, put_markdown, clear, use_scope, put_html
async def select_subject():
"""Prompt the user to select a subject."""
conn = sqlite3.connect('terms_game.db')
cursor = conn.cursor()
cursor.execute("SELECT DISTINCT subject FROM terms")
subjects = [row[0] for row in cursor.fetchall()]
conn.close()
subject = await actions(label='Select a subject to be quizzed on:', buttons=subjects)
return subject
def get_next_term(subject):
"""Fetch a random term that hasn't been asked yet for the selected subject."""
conn = sqlite3.connect('terms_game.db')
cursor = conn.cursor()
cursor.execute("""
SELECT id, term, hint1, hint2 FROM terms
WHERE subject=? AND asked=0
ORDER BY RANDOM()
LIMIT 1
""", (subject,))
result = cursor.fetchone()
conn.close()
if result:
return {
'id': result[0],
'term': result[1].upper(),
'hint1': result[2],
'hint2': result[3]
}
else:
return None
def update_term_status(term_id, passed):
"""Update the status of the term in the database."""
conn = sqlite3.connect('terms_game.db')
cursor = conn.cursor()
cursor.execute("UPDATE terms SET asked=1, passed=? WHERE id=?", (passed, term_id))
conn.commit()
conn.close()
async def play_round(term_data, current_term_number, total_terms):
"""Handle the logic for guessing each letter of the term."""
term = term_data['term']
term_parts = term.split(' ') # Split the term by spaces
letters_guessed = [''] * len(term) # Keep track of guessed letters
current_letter_index = 0 # Tracks actual letters (excluding spaces)
attempts = 0
max_attempts_before_hint = 2 # Show second hint after 2 incorrect attempts
total_attempts_allowed = 4 # Total attempts per letter
# Define CSS for styling the input boxes like PIN code boxes
css = '''
<style>
h1 {
font-family: helvetica;
text-align:center;
}
.pin-code{
padding: 0;
margin: 0 auto;
display: flex;
justify-content:center;
}
.pin-code input {
border: none;
text-align: center;
width: 48px;
height:48px;
font-size: 36px;
background-color: #F3F3F3;
margin-right:5px;
}
.pin-code input:focus {
border: 1px solid #573D8B;
outline:none;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
</style>
'''
while current_letter_index < len(letters_guessed):
with use_scope('question_scope', clear=True):
# Inject CSS
put_html(css)
# Update question info and hints
put_markdown(f"### Term {current_term_number} of {total_terms}")
put_markdown(f"**Hint:** {term_data['hint1']}")
if attempts >= max_attempts_before_hint:
put_markdown(f"**Second Hint:** {term_data['hint2']}")
# Display the PIN-style input boxes with space between term parts
html_inputs = '<div class="pin-code">'
for i, char in enumerate(term):
if char == ' ':
# Represent spaces with a visual gap
html_inputs += '<div style="width: 20px;"></div>'
else:
if letters_guessed[i] != '':
# Display guessed letters in readonly input boxes
html_inputs += f'<input type="text" maxlength="1" value="{letters_guessed[i]}" readonly>'
elif i == current_letter_index:
# Current letter to guess (editable box)
html_inputs += f'<input type="text" id="letter_{i}" maxlength="1" autofocus>'
else:
# Future letters (disabled)
html_inputs += f'<input type="text" maxlength="1" disabled>'
html_inputs += '</div>'
put_html(html_inputs)
# Display alphabet buttons
alphabet = [chr(i) for i in range(ord('A'), ord('Z')+1)]
letter = await actions(label="Choose a letter", buttons=alphabet)
# Skip spaces in the term
while term[current_letter_index] == ' ':
current_letter_index += 1
# Validate the entered letter
if not letter.isalpha() or len(letter) != 1:
with use_scope('feedback', clear=True):
put_text("Please enter a single alphabet letter.")
attempts += 1
if attempts >= total_attempts_allowed:
# Display message that the term was missed with the hints
put_html(f'<p style="font-size: 12px;">You missed the term for Hints: {term_data["hint1"]} and {term_data["hint2"]}.</p>')
return False
continue
if letter == term[current_letter_index]:
letters_guessed[current_letter_index] = letter
current_letter_index += 1
attempts = 0 # Reset attempts for the next letter
with use_scope('feedback', clear=True):
put_text("Correct! Keep going.")
else:
attempts += 1
with use_scope('feedback', clear=True):
put_text("Incorrect, try again.")
if attempts >= max_attempts_before_hint:
with use_scope('feedback', clear=True):
put_text("Incorrect, showing second hint...")
if attempts >= total_attempts_allowed:
# Display message that the term was missed with the hints
put_html(f'<p style="font-size: 12px;">You missed the term for Hints: {term_data["hint1"]} and {term_data["hint2"]}.</p>')
return False
continue # Retry the current letter
# All letters guessed correctly
put_html(f'<p style="font-size: 12px;">Correct! You\'ve guessed the term: <strong>{term}</strong></p>')
return True
async def play_game():
"""Main game loop."""
clear() # Clear the entire page to ensure fresh start
subject = await select_subject() # Use await to get the subject value
total_terms = 10 # Total number of terms for this session
current_term_number = 0
correct_answers = 0
while current_term_number < total_terms:
term_data = get_next_term(subject)
if not term_data:
put_markdown("### You've completed all terms in this subject!")
break
current_term_number += 1
success = await play_round(term_data, current_term_number, total_terms)
update_term_status(term_data['id'], success)
if success:
correct_answers += 1
with use_scope('feedback', clear=True):
put_markdown(f"**Score:** {correct_answers} out of {current_term_number}")
with use_scope('question_scope', clear=True): # Clear the last term question
put_markdown(f"### Final Score: {correct_answers} out of {current_term_number}")
# Display a button to start over after finishing 10 questions
choice = await actions(label="Game Over! Would you like to start over?", buttons=["Start Over"])
if choice == "Start Over":
await play_game() # Restart the game
if __name__ == '__main__':
# Start the PyWebIO server
start_server(play_game, port=8090, debug=True, address='127.0.0.1')
Breakdown of the above code:
- Function:
select_subject()
async def select_subject():
"""Prompt the user to select a subject."""
conn = sqlite3.connect('terms_game.db')
cursor = conn.cursor()
cursor.execute("SELECT DISTINCT subject FROM terms")
subjects = [row[0] for row in cursor.fetchall()]
conn.close()
subject = await actions(label='Select a subject to be quizzed on:', buttons=subjects)
return subject
- This function connects to the SQLite database (
terms_game.db
) and retrieves all distinct subjects from theterms
table. - It presents the subjects to the user as clickable buttons using
actions()
. - Once the user selects a subject, it is returned.
2. Function: get_next_term(subject)
def get_next_term(subject):
"""Fetch a random term that hasn't been asked yet for the selected subject."""
conn = sqlite3.connect('terms_game.db')
cursor = conn.cursor()
cursor.execute("""
SELECT id, term, hint1, hint2 FROM terms
WHERE subject=? AND asked=0
ORDER BY RANDOM()
LIMIT 1
""", (subject,))
result = cursor.fetchone()
conn.close()
if result:
return {
'id': result[0],
'term': result[1].upper(),
'hint1': result[2],
'hint2': result[3]
}
else:
return None
- This function retrieves a random term from the database for the chosen subject. The query ensures that the term hasn’t been asked before (
asked=0
). - The term is returned along with its two hints and its ID. If no unasked term is found,
None
is returned.
3. Function: update_term_status(term_id, passed)
def update_term_status(term_id, passed):
"""Update the status of the term in the database."""
conn = sqlite3.connect('terms_game.db')
cursor = conn.cursor()
cursor.execute("UPDATE terms SET asked=1, passed=? WHERE id=?", (passed, term_id))
conn.commit()
conn.close()
- This function updates the
asked
andpassed
fields in the database for a given term. asked=1
indicates that the term has been presented to the user.passed
is a boolean value that indicates whether the user guessed the term correctly.
4. Function: play_round(term_data, current_term_number, total_terms)
async def play_round(term_data, current_term_number, total_terms):
"""Handle the logic for guessing each letter of the term."""
# Logic for handling guesses
- This is where the main gameplay logic takes place. The user tries to guess each letter of the term based on the hints.
- The term is split into individual letters, and the user can guess letters using alphabet buttons.
Key Concepts:
- CSS for Input Boxes: Custom styling for the letter input boxes, giving the look and feel of a PIN input:
css = '''
<style>
h1 { font-family: helvetica; text-align:center; }
.pin-code { padding: 0; margin: 0 auto; display: flex; justify-content:center; }
.pin-code input { border: none; text-align: center; width: 48px; height:48px; font-size: 36px; background-color: #F3F3F3; margin-right:5px; }
.pin-code input:focus { border: 1px solid #573D8B; outline:none; }
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
</style>
'''
- User Guesses:
- The user clicks buttons representing letters (A-Z).
- After each guess, the game checks if the letter is correct.
- If incorrect, the user gets a second hint after a few failed attempts.
- The game also handles space characters in multi-word terms, automatically skipping them.
Example of Term Input Logic:
html_inputs = '<div class="pin-code">'
for i, char in enumerate(term):
if char == ' ':
html_inputs += '<div style="width: 20px;"></div>'
else:
if letters_guessed[i] != '':
html_inputs += f'<input type="text" maxlength="1" value="{letters_guessed[i]}" readonly>'
elif i == current_letter_index:
html_inputs += f'<input type="text" id="letter_{i}" maxlength="1" autofocus>'
else:
html_inputs += f'<input type="text" maxlength="1" disabled>'
html_inputs += '</div>'
put_html(html_inputs)
This snippet handles the display of input boxes and spaces for multi-word terms.
5. Function: play_game()
async def play_game():
"""Main game loop."""
clear() # Clear the entire page to ensure fresh start
subject = await select_subject() # Use await to get the subject value
total_terms = 10 # Total number of terms for this session
current_term_number = 0
correct_answers = 0
while current_term_number < total_terms:
term_data = get_next_term(subject)
if not term_data:
put_markdown("### You've completed all terms in this subject!")
break
current_term_number += 1
success = await play_round(term_data, current_term_number, total_terms)
update_term_status(term_data['id'], success)
if success:
correct_answers += 1
with use_scope('feedback', clear=True):
put_markdown(f"**Score:** {correct_answers} out of {current_term_number}")
with use_scope('question_scope', clear=True): # Clear the last term question
put_markdown(f"### Final Score: {correct_answers} out of {current_term_number}")
# Display a button to start over after finishing 10 questions
choice = await actions(label="Game Over! Would you like to start over?", buttons=["Start Over"])
if choice == "Start Over":
await play_game() # Restart the game
- Main Game Loop:
- This is where the game runs. It starts by allowing the user to choose a subject.
- For each term, the game fetches the next unasked term and calls
play_round()
to allow the user to guess the term. - After each round, the status of the term is updated in the database.
- The loop continues for a total of 10 terms.
- Scorekeeping: The user’s score is updated after each round, and the final score is displayed at the end of the game.
- Restart Option: After 10 rounds, the user can choose to start over and play again.
6. Server Start
if __name__ == '__main__':
# Start the PyWebIO server
start_server(play_game, port=8090, debug=True, address='127.0.0.1')
This line starts the PyWebIO server, allowing you to access the game in a browser. The server runs locally at http://127.0.0.1:8090.

Thank you for following along with this tutorial. We hope you found it helpful and informative. If you have any questions, or if you would like to suggest new Python code examples or topics for future tutorials/articles, please feel free to join and comment. Your feedback and suggestions are always welcome!
You can find the same tutorial on Medium.com.