diff --git a/practice-exam/aws-ccp-all-practice-exams.apkg b/practice-exam/aws-ccp-all-practice-exams.apkg
new file mode 100644
index 0000000..ffcdca8
Binary files /dev/null and b/practice-exam/aws-ccp-all-practice-exams.apkg differ
diff --git a/practice-exam/generate_anki_deck.py b/practice-exam/generate_anki_deck.py
new file mode 100644
index 0000000..54a8c03
--- /dev/null
+++ b/practice-exam/generate_anki_deck.py
@@ -0,0 +1,781 @@
+"""
+AWS Certified Cloud Practitioner Practice Exam to Anki Deck Converter
+
+This script parses practice exam markdown files and generates Anki decks.
+"""
+
+import re
+import os
+import glob
+import random
+import genanki
+
+
+def generate_unique_id():
+ """Generate a unique ID for Anki models and decks."""
+ return random.randrange(1 << 30, 1 << 31)
+
+
+# Define the Anki card model with interactive quiz functionality
+AWS_MODEL = genanki.Model(
+ generate_unique_id(),
+ 'AWS Practice Exam Interactive Model',
+ fields=[
+ {'name': 'Question'},
+ {'name': 'OptionsHTML'},
+ {'name': 'Answer'},
+ {'name': 'Source'},
+ {'name': 'IsMultiple'}, # "true" or "false" - whether multiple answers are expected
+ ],
+ templates=[
+ {
+ 'name': 'Interactive Quiz Card',
+ 'qfmt': '''
+
{{Question}}
+
+
+{{OptionsHTML}}
+
+
+
+
+
+ ''',
+ 'afmt': '''
+{{Question}}
+
+{{OptionsHTML}}
+
+
+
+ Richtige Antwort: {{Answer}}
+
+
+
+
+ Quelle: {{Source}}
+
+
+
+
Bewerte deine Antwort:
+
+
+
+
+
+
+
+
+
+ ''',
+ },
+ ],
+ css='''
+.card {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
+ font-size: 16px;
+ text-align: left;
+ color: #333;
+ background-color: #f8f9fa;
+ padding: 20px;
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.question {
+ font-weight: 600;
+ margin-bottom: 20px;
+ font-size: 18px;
+ color: #232f3e;
+ line-height: 1.5;
+}
+
+.options-container {
+ margin: 15px 0;
+}
+
+.option-item {
+ display: flex;
+ align-items: flex-start;
+ margin: 10px 0;
+ padding: 12px 15px;
+ background-color: #fff;
+ border-radius: 8px;
+ border: 2px solid #e0e0e0;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.option-item:hover {
+ border-color: #ff9900;
+ background-color: #fff8f0;
+}
+
+.options-container.answered .option-item:hover {
+ border-color: inherit;
+ background-color: inherit;
+}
+
+.option-item input {
+ margin-right: 12px;
+ margin-top: 3px;
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+ accent-color: #ff9900;
+}
+
+.option-item label {
+ cursor: pointer;
+ flex: 1;
+ line-height: 1.4;
+}
+
+.option-item.correct-option {
+ border-color: #28a745;
+ background-color: #d4edda;
+}
+
+.option-item.incorrect-option {
+ border-color: #dc3545;
+ background-color: #f8d7da;
+}
+
+.hint-text {
+ display: none;
+ background-color: #e3f2fd;
+ color: #1565c0;
+ padding: 10px 15px;
+ border-radius: 8px;
+ margin-bottom: 15px;
+ font-weight: 500;
+}
+
+.button-container {
+ margin: 20px 0;
+ text-align: center;
+}
+
+button {
+ padding: 12px 30px;
+ font-size: 16px;
+ font-weight: 600;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ margin: 5px;
+}
+
+#checkBtn {
+ background: linear-gradient(135deg, #ff9900, #ffad33);
+ color: #232f3e;
+}
+
+#checkBtn:hover {
+ background: linear-gradient(135deg, #ec8b00, #ff9900);
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(255, 153, 0, 0.4);
+}
+
+.result {
+ padding: 15px 20px;
+ border-radius: 8px;
+ margin: 15px 0;
+ font-size: 18px;
+ font-weight: 600;
+ text-align: center;
+}
+
+.result.correct {
+ background-color: #d4edda;
+ color: #155724;
+ border: 2px solid #28a745;
+}
+
+.result.incorrect {
+ background-color: #f8d7da;
+ color: #721c24;
+ border: 2px solid #dc3545;
+}
+
+.correct-answer {
+ padding: 15px 20px;
+ background-color: #e8f5e9;
+ border-radius: 8px;
+ border-left: 4px solid #28a745;
+ margin: 15px 0;
+ font-size: 16px;
+}
+
+.source {
+ color: #666;
+ font-size: 12px;
+ margin-top: 10px;
+ text-align: right;
+}
+
+.rating-container {
+ margin-top: 20px;
+ padding: 20px;
+ background-color: #fff;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.rating-hint {
+ text-align: center;
+ font-size: 16px;
+ margin-bottom: 15px;
+ color: #333;
+}
+
+.rating-buttons {
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.rating-btn {
+ padding: 15px 20px;
+ min-width: 80px;
+ font-size: 14px;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ line-height: 1.3;
+}
+
+.rating-btn .rating-sub {
+ font-size: 11px;
+ opacity: 0.8;
+}
+
+.rating-btn.again {
+ background: #dc3545;
+ color: white;
+}
+
+.rating-btn.again:hover {
+ background: #c82333;
+ transform: translateY(-2px);
+}
+
+.rating-btn.hard {
+ background: #fd7e14;
+ color: white;
+}
+
+.rating-btn.hard:hover {
+ background: #e96b02;
+ transform: translateY(-2px);
+}
+
+.rating-btn.good {
+ background: #28a745;
+ color: white;
+}
+
+.rating-btn.good:hover {
+ background: #218838;
+ transform: translateY(-2px);
+}
+
+.rating-btn.easy {
+ background: #17a2b8;
+ color: white;
+}
+
+.rating-btn.easy:hover {
+ background: #138496;
+ transform: translateY(-2px);
+}
+
+hr {
+ border: none;
+ border-top: 1px solid #dee2e6;
+ margin: 20px 0;
+}
+
+/* Hide custom rating buttons on AnkiDroid/mobile (uses built-in buttons) */
+.mobile .rating-container,
+.android .rating-container {
+ display: none !important;
+}
+
+/* Dark Mode Styles */
+.night_mode .card {
+ background-color: #1e1e2e;
+ color: #cdd6f4;
+}
+
+.night_mode .question {
+ color: #cdd6f4;
+}
+
+.night_mode .option-item {
+ background-color: #313244;
+ border-color: #45475a;
+ color: #cdd6f4;
+}
+
+.night_mode .option-item:hover {
+ border-color: #fab387;
+ background-color: #45475a;
+}
+
+.night_mode .option-item label {
+ color: #cdd6f4;
+}
+
+.night_mode .option-item.correct-option {
+ border-color: #a6e3a1;
+ background-color: rgba(166, 227, 161, 0.2);
+}
+
+.night_mode .option-item.incorrect-option {
+ border-color: #f38ba8;
+ background-color: rgba(243, 139, 168, 0.2);
+}
+
+.night_mode .hint-text {
+ background-color: rgba(137, 180, 250, 0.2);
+ color: #89b4fa;
+}
+
+.night_mode #checkBtn {
+ background: linear-gradient(135deg, #fab387, #f9e2af);
+ color: #1e1e2e;
+}
+
+.night_mode #checkBtn:hover {
+ background: linear-gradient(135deg, #f9e2af, #fab387);
+ box-shadow: 0 4px 12px rgba(250, 179, 135, 0.4);
+}
+
+.night_mode .result.correct {
+ background-color: rgba(166, 227, 161, 0.2);
+ color: #a6e3a1;
+ border-color: #a6e3a1;
+}
+
+.night_mode .result.incorrect {
+ background-color: rgba(243, 139, 168, 0.2);
+ color: #f38ba8;
+ border-color: #f38ba8;
+}
+
+.night_mode .correct-answer {
+ background-color: rgba(166, 227, 161, 0.15);
+ border-left-color: #a6e3a1;
+ color: #cdd6f4;
+}
+
+.night_mode .source {
+ color: #a6adc8;
+}
+
+.night_mode .rating-container {
+ background-color: #313244;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+}
+
+.night_mode .rating-hint {
+ color: #cdd6f4;
+}
+
+.night_mode hr {
+ border-top-color: #45475a;
+}
+ '''
+)
+
+
+def parse_markdown_file(filepath: str) -> list[dict]:
+ """
+ Parse a practice exam markdown file and extract questions.
+
+ Args:
+ filepath: Path to the markdown file
+
+ Returns:
+ List of dictionaries containing question data
+ """
+ with open(filepath, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ questions = []
+
+ # Pattern to match each question block
+ # Questions start with a number followed by a period
+ question_pattern = r'(\d+)\.\s+(.+?)(?=\n\s*-\s+A\.)'
+
+ # Split content by question numbers at the start of lines
+ question_blocks = re.split(r'\n(?=\d+\.\s)', content)
+
+ for block in question_blocks:
+ if not block.strip():
+ continue
+
+ # Match question number and text
+ q_match = re.match(r'(\d+)\.\s+(.+?)(?=\n\s*-\s+A\.)', block, re.DOTALL)
+ if not q_match:
+ continue
+
+ question_num = q_match.group(1)
+ question_text = q_match.group(2).strip()
+
+ # Extract options (A, B, C, D, E)
+ options = []
+ option_pattern = r'-\s+([A-E])\.(.+?)(?=(?:\n\s*-\s+[A-E]\.)|\n\s* block
+ answer_pattern = r'Correct answer:\s*([A-E](?:,\s*[A-E])*)'
+ answer_match = re.search(answer_pattern, block)
+
+ if answer_match and options:
+ answer = answer_match.group(1).strip()
+
+ # Check if multiple answers are expected
+ is_multiple = ',' in answer
+
+ questions.append({
+ 'number': question_num,
+ 'question': question_text,
+ 'options': options,
+ 'answer': answer,
+ 'is_multiple': is_multiple
+ })
+
+ return questions
+
+
+def create_anki_deck(questions: list[dict], deck_name: str, source_file: str) -> genanki.Deck:
+ """
+ Create an Anki deck from parsed questions.
+
+ Args:
+ questions: List of question dictionaries
+ deck_name: Name for the Anki deck
+ source_file: Source file name for reference
+
+ Returns:
+ genanki.Deck object
+ """
+ deck = genanki.Deck(generate_unique_id(), deck_name)
+
+ for q in questions:
+ # Format options as interactive HTML with checkboxes or radio buttons
+ is_multiple = q.get('is_multiple', False)
+ input_type = 'checkbox' if is_multiple else 'radio'
+
+ options_html_parts = []
+ for opt in q['options']:
+ letter = opt[0] # First character is the option letter
+ text = opt[3:] # Skip "X. " prefix
+ option_html = f'''
+
+
+
'''
+ options_html_parts.append(option_html)
+
+ options_html = '\n'.join(options_html_parts)
+
+ note = genanki.Note(
+ model=AWS_MODEL,
+ fields=[
+ q['question'],
+ options_html,
+ q['answer'],
+ f"{source_file} - Q{q['number']}",
+ "true" if is_multiple else "false"
+ ]
+ )
+ deck.add_note(note)
+
+ return deck
+
+
+def process_single_file(filepath: str, output_dir: str = None):
+ """
+ Process a single practice exam file and create an Anki deck.
+
+ Args:
+ filepath: Path to the markdown file
+ output_dir: Output directory for the .apkg file (defaults to same as input)
+ """
+ if output_dir is None:
+ output_dir = os.path.dirname(filepath)
+
+ filename = os.path.basename(filepath)
+ deck_name = f"AWS CCP - {filename.replace('.md', '').replace('-', ' ').title()}"
+
+ print(f"Processing: {filename}")
+ questions = parse_markdown_file(filepath)
+ print(f" Found {len(questions)} questions")
+
+ if questions:
+ deck = create_anki_deck(questions, deck_name, filename)
+ output_path = os.path.join(output_dir, filename.replace('.md', '.apkg'))
+ genanki.Package(deck).write_to_file(output_path)
+ print(f" Created: {output_path}")
+ return deck
+ else:
+ print(f" No questions found in {filename}")
+ return None
+
+
+def process_all_files(directory: str, output_dir: str = None, combined: bool = True):
+ """
+ Process all practice exam files in a directory.
+
+ Args:
+ directory: Directory containing practice exam markdown files
+ output_dir: Output directory for .apkg files
+ combined: If True, create a single combined deck; if False, create separate decks
+ """
+ if output_dir is None:
+ output_dir = directory
+
+ pattern = os.path.join(directory, 'practice-exam-*.md')
+ files = sorted(glob.glob(pattern))
+
+ if not files:
+ print(f"No practice exam files found matching: {pattern}")
+ return
+
+ print(f"Found {len(files)} practice exam files")
+
+ if combined:
+ # Create a combined deck
+ all_questions = []
+ for filepath in files:
+ filename = os.path.basename(filepath)
+ print(f"Processing: {filename}")
+ questions = parse_markdown_file(filepath)
+
+ # Add source file info to each question
+ for q in questions:
+ q['source_file'] = filename
+
+ all_questions.extend(questions)
+ print(f" Found {len(questions)} questions")
+
+ print(f"\nTotal questions: {len(all_questions)}")
+
+ if all_questions:
+ deck = genanki.Deck(
+ generate_unique_id(),
+ 'AWS Certified Cloud Practitioner - All Practice Exams'
+ )
+
+ for q in all_questions:
+ # Format options as interactive HTML with checkboxes or radio buttons
+ is_multiple = q.get('is_multiple', False)
+ input_type = 'checkbox' if is_multiple else 'radio'
+
+ options_html_parts = []
+ for opt in q['options']:
+ letter = opt[0] # First character is the option letter
+ text = opt[3:] # Skip "X. " prefix
+ option_html = f'''
+
+
+
'''
+ options_html_parts.append(option_html)
+
+ options_html = '\n'.join(options_html_parts)
+
+ note = genanki.Note(
+ model=AWS_MODEL,
+ fields=[
+ q['question'],
+ options_html,
+ q['answer'],
+ f"{q['source_file']} - Q{q['number']}",
+ "true" if is_multiple else "false"
+ ]
+ )
+ deck.add_note(note)
+
+ output_path = os.path.join(output_dir, 'aws-ccp-all-practice-exams.apkg')
+ genanki.Package(deck).write_to_file(output_path)
+ print(f"\nCreated combined deck: {output_path}")
+ else:
+ # Create separate decks for each file
+ for filepath in files:
+ process_single_file(filepath, output_dir)
+
+
+def main():
+ """Main entry point."""
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ description='Convert AWS Practice Exam markdown files to Anki decks'
+ )
+ parser.add_argument(
+ 'input',
+ nargs='?',
+ default='.',
+ help='Input file or directory (default: current directory)'
+ )
+ parser.add_argument(
+ '-o', '--output',
+ help='Output directory for .apkg files (default: same as input)'
+ )
+ parser.add_argument(
+ '-s', '--separate',
+ action='store_true',
+ help='Create separate decks for each file instead of a combined deck'
+ )
+ parser.add_argument(
+ '-f', '--file',
+ help='Process a single specific file'
+ )
+
+ args = parser.parse_args()
+
+ if args.file:
+ # Process single file
+ process_single_file(args.file, args.output)
+ elif os.path.isfile(args.input):
+ # Input is a file
+ process_single_file(args.input, args.output)
+ else:
+ # Input is a directory
+ process_all_files(args.input, args.output, combined=not args.separate)
+
+
+if __name__ == '__main__':
+ main()