#!/usr/bin/env python3
"""
Activity Builders for Math Curriculum
Data-driven activity builders that generate HTML for worksheets.
Used with monday.py, tuesday.py, etc. day generators.
Usage:
from activity_builders import build_number_bonds, build_fact_families
html = build_number_bonds({"whole": 10, "pairs": [(3, 7), (4, 6)]})
"""
import os
import sys
sys.path.insert(0, os.path.dirname(__file__))
from math_helpers import b, BLUE, GREEN, RED, YELLOW, ORANGE, PURPLE, PINK, TEAL, GOLD
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# FOUNDATIONAL: Place Value Builders
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def build_tens_ones_chart(data: dict) -> str:
"""
Tens and ones place value chart.
Data:
- numbers: list of numbers to decompose
- blank_tens: bool (blank tens column, default True)
- blank_ones: bool (blank ones column, default True)
- note: str (optional instruction)
"""
numbers = data.get("numbers", [])
blank_tens = data.get("blank_tens", True)
blank_ones = data.get("blank_ones", True)
note = data.get("note", "Fill in the tens and ones for each number.")
html = f'<p class="note">{note}</p>'
html += '<table class="tbl">\n'
html += '<tr><th>Number</th><th>Tens</th><th>Ones</th></tr>\n'
for num in numbers:
tens = num // 10
ones = num % 10
html += f'<tr><td>{num}</td>'
if blank_tens:
html += f'<td>{b("5em")}</td>'
else:
html += f'<td>{tens}</td>'
if blank_ones:
html += f'<td>{b("5em")}</td>'
else:
html += f'<td>{ones}</td>'
html += '</tr>\n'
html += '</table>'
return html
def build_expanded_form(data: dict) -> str:
"""
Expanded form practice (number = tens + ones).
Data:
- numbers: list of numbers to write in expanded form
- show_examples: bool (show example first, default True)
- note: str (optional instruction)
"""
numbers = data.get("numbers", [])
show_examples = data.get("show_examples", True)
note = data.get("note", "Write each number as tens + ones.")
cols = data.get("cols", 2)
html = f'<p class="note">{note}</p>'
if show_examples:
html += '<div class="abox" style="background:#e8eaf6; margin-bottom:8px;">'
html += '<p><b>Expanded form</b> shows a number as tens + ones.</p>'
html += '<p style="font-size:14px;">34 = 30 + 4 (example) | 52 = 50 + 2 (example)</p>'
html += '</div>'
html += f'<div class="mgrid c{cols}">\n'
for num in numbers:
html += f'<div class="abox"><p>{num} = {b("4em")} + {b("4em")}</p></div>\n'
html += '</div>'
return html
def build_build_number(data: dict) -> str:
"""
Build the number from expanded form.
Data:
- additions: list of (tens, ones) tuples
- note: str (optional instruction)
"""
additions = data.get("additions", [])
note = data.get("note", "What number does the expanded form make?")
cols = data.get("cols", 3)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for tens, ones in additions:
html += f'<div class="abox" style="text-align:center;"><p>{tens} + {ones} = {b("4em")}</p></div>\n'
html += '</div>'
return html
def build_compare_place_value(data: dict) -> str:
"""
Compare place values using >, <, =.
Data:
- comparisons: list of dicts with "left", "right", and optional "type"
- note: str (optional instruction)
"""
comparisons = data.get("comparisons", [])
note = data.get("note", "Write >, <, or =.")
cols = data.get("cols", 2)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for comp in comparisons:
left = comp.get("left", "")
right = comp.get("right", "")
html += f'<div class="abox"><p>{left} {b("4em")} {right}</p></div>\n'
html += '</div>'
return html
def build_read_tens_ones(data: dict) -> str:
"""
Read tens and ones, write the number.
Data:
- phrases: list of strings like "3 tens and 4 ones"
- note: str (optional instruction)
"""
phrases = data.get("phrases", [])
note = data.get("note", "Write the number each set of tens and ones makes.")
cols = data.get("cols", 2)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for phrase in phrases:
html += f'<div class="abox"><p>{phrase} = {b("4em")}</p></div>\n'
html += '</div>'
return html
def build_write_tens_ones(data: dict) -> str:
"""
Write numbers as tens and ones.
Data:
- numbers: list of numbers to decompose
- note: str (optional instruction)
"""
numbers = data.get("numbers", [])
note = data.get("note", "Break each number apart into tens and ones.")
cols = data.get("cols", 2)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for num in numbers:
html += f'<div class="abox"><p>{num} = {b("4em")} tens and {b("4em")} ones</p></div>\n'
html += '</div>'
return html
def build_decompose_two_ways(data: dict) -> str:
"""
Decompose a number in two different ways.
Data:
- numbers: list of numbers to decompose
- note: str (optional instruction)
"""
numbers = data.get("numbers", [])
note = data.get("note", "Show each number as two different addends.")
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c2">\n'
for num in numbers:
html += f'<div class="abox"><p style="font-size:16px; font-weight:700;">{num}</p>'
html += f'<p>{b("5em")} + {b("5em")}</p>'
html += f'<p>{b("5em")} + {b("5em")}</p></div>\n'
html += '</div>'
return html
def build_place_value_table_full(data: dict) -> str:
"""
Full place value table with expanded form column.
Data:
- numbers: list of numbers
- note: str (optional instruction)
"""
numbers = data.get("numbers", [])
note = data.get("note", "Fill in the table.")
html = f'<p class="note">{note}</p>'
html += '<table class="tbl">\n'
html += '<tr><th>Number</th><th>Tens</th><th>Ones</th><th>Expanded Form</th></tr>\n'
for num in numbers:
html += f'<tr><td>{num}</td>'
html += f'<td>{b("4em")}</td>'
html += f'<td>{b("4em")}</td>'
html += f'<td>{b("4em")} + {b("4em")}</td>'
html += '</tr>\n'
html += '</table>'
return html
def build_compare_numbers(data: dict) -> str:
"""
Compare numbers using <, >, = symbols.
Data:
- pairs: list of (left, right) tuples
- note: str (optional instruction)
- cols: int (number of columns, default 3)
- show_alligator: bool (mention alligator mnemonic, default False)
"""
pairs = data.get("pairs", [])
note = data.get("note", "Fill in <, >, or =.")
cols = data.get("cols", 3)
show_alligator = data.get("show_alligator", False)
if show_alligator:
note = "The alligator always eats the bigger number! Fill in <, >, or =."
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for left, right in pairs:
html += f'<div class="abox"><p style="font-size:16px;">{left} ___ {right}</p></div>\n'
html += '</div>'
return html
def build_order_numbers(data: dict) -> str:
"""
Order numbers from least to greatest or greatest to least.
Data:
- sets: list of number lists to order
- direction: str ("least_to_greatest" or "greatest_to_least")
- note: str (optional instruction)
- cols: int (number of columns, default 2)
"""
sets = data.get("sets", [])
direction = data.get("direction", "least_to_greatest")
cols = data.get("cols", 2)
if direction == "least_to_greatest":
note = data.get("note", "Write the numbers from least to greatest.")
else:
note = data.get("note", "Write the numbers from greatest to least.")
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for nums in sets:
blank_html = ", ".join([b("5em") for _ in nums])
html += f'<div class="abox"><p style="font-size:14px;">{", ".join(map(str, nums))}</p>'
html += f'<p>{blank_html}</p></div>\n'
html += '</div>'
return html
def build_true_false_comparison(data: dict) -> str:
"""
True or false comparison statements.
Data:
- statements: list of comparison strings (e.g., "34 > 28")
- note: str (optional instruction)
- cols: int (number of columns, default 2)
"""
statements = data.get("statements", [])
note = data.get("note", "Write T for true, F for false.")
cols = data.get("cols", 2)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for stmt in statements:
html += f'<div class="abox"><p style="font-size:16px;">{stmt} ___</p></div>\n'
html += '</div>'
return html
def build_compare_word_problems(data: dict) -> str:
"""
Word problems involving comparison.
Data:
- problems: list of dicts with "text" and "answer_type" (who/which)
- note: str (optional instruction)
"""
problems = data.get("problems", [])
note = data.get("note", "Read carefully and answer the question.")
html = f'<div class="wp">\n'
for problem in problems:
text = problem.get("text", "")
answer_type = problem.get("answer_type", "name")
html += f'<p>{text} {b("6em")}</p>\n'
html += '</div>\n'
return html
def build_circle_greater(data: dict) -> str:
"""
Circle the greater number in pairs.
Data:
- pairs: list of (left, right) tuples
- note: str (optional instruction)
- cols: int (number of columns, default 2)
"""
pairs = data.get("pairs", [])
note = data.get("note", "Circle the greater number.")
cols = data.get("cols", 2)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for left, right in pairs:
html += f'<div class="abox"><p style="font-size:16px;">{left} or {right}</p></div>\n'
html += '</div>'
return html
def build_circle_less(data: dict) -> str:
"""
Circle the lesser number in pairs.
Data:
- pairs: list of (left, right) tuples
- note: str (optional instruction)
- cols: int (number of columns, default 2)
"""
pairs = data.get("pairs", [])
note = data.get("note", "Circle the lesser number.")
cols = data.get("cols", 2)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for left, right in pairs:
html += f'<div class="abox"><p style="font-size:16px;">{left} or {right}</p></div>\n'
html += '</div>'
return html
def build_compare_with_explanation(data: dict) -> str:
"""
Compare numbers with step-by-step explanation prompts.
Data:
- comparisons: list of dicts with "left", "right", "explanation"
- note: str (optional instruction)
- cols: int (number of columns, default 2)
"""
comparisons = data.get("comparisons", [])
note = data.get("note", "Look at the tens place first. The number with more tens is greater.")
cols = data.get("cols", 2)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for comp in comparisons:
html += f'<div class="abox"><p>{comp.get("explanation", "")}</p>'
html += f'<p>{comp.get("left", "")} ___ {comp.get("right", "")}</p></div>\n'
html += '</div>'
return html
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# PHASE 1: Addition & Subtraction Builders
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def build_number_bonds(data: dict) -> str:
"""
Number bonds (circles with connecting lines).
Data:
- whole: int (the total, e.g., 10)
- pairs: list of (part1, part2) or (part1, None) for blanks
- note: str (optional instruction)
- show_visual: bool (include visual circles, default True)
"""
whole = data.get("whole", 10)
pairs = data.get("pairs", [])
note = data.get("note", "Fill in the missing part of each number bond.")
show_visual = data.get("show_visual", True)
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c2">\n'
for part1, part2 in pairs:
if show_visual:
# Visual number bond with circles
p1_display = str(part1) if part1 is not None else b("2em")
p2_display = str(part2) if part2 is not None else b("2em")
html += f'<div class="abox" style="text-align:center;">'
html += f'<div style="display:flex; justify-content:center; gap:20px; margin-bottom:8px;">'
html += f'<div style="border:2px solid #3f51b5; border-radius:50%; width:40px; height:40px; display:flex; align-items:center; justify-content:center; font-weight:700;">{p1_display}</div>'
html += f'<div style="border:2px solid #3f51b5; border-radius:50%; width:40px; height:40px; display:flex; align-items:center; justify-content:center; font-weight:700;">{p2_display}</div>'
html += f'</div>'
html += f'<div style="border:2px solid #3f51b5; border-radius:50%; width:40px; height:40px; display:flex; align-items:center; justify-content:center; font-weight:700; margin:0 auto;">{whole}</div>'
html += f'</div>\n'
else:
# Text-only format
p1_display = str(part1) if part1 is not None else b("3em")
p2_display = str(part2) if part2 is not None else b("3em")
html += f'<div class="abox" style="text-align:center;"><p>{whole}</p><p>{p1_display} and {p2_display}</p></div>\n'
html += '</div>'
return html
def build_fact_families(data: dict) -> str:
"""
Fact family triangles/houses.
Data:
- numbers: list of 3 numbers that form a fact family (e.g., [3, 7, 10])
- note: str (optional instruction)
- show_triangle: bool (visual triangle layout, default True)
- include_subtraction: bool (include subtraction facts, default True)
"""
families = data.get("families", []) # List of [a, b, sum] triples
note = data.get("note", "Write all the fact family equations.")
show_triangle = data.get("show_triangle", True)
include_subtraction = data.get("include_subtraction", True)
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c2">\n'
for family in families:
num_a, num_b, total = family[0], family[1], family[2]
if show_triangle:
# Triangle/house visual
html += f'<div class="abox" style="padding:10px;">'
html += f'<div style="text-align:center; margin-bottom:8px;">'
html += f'<div style="display:inline-block; border:2px solid #3f51b5; border-radius:50%; width:36px; height:36px; line-height:36px; font-weight:700; margin:0 4px;">{num_a}</div>'
html += f'<div style="display:inline-block; border:2px solid #3f51b5; border-radius:50%; width:36px; height:36px; line-height:36px; font-weight:700; margin:0 4px;">{num_b}</div>'
html += f'<div style="display:inline-block; border:2px solid #3f51b5; border-radius:50%; width:36px; height:36px; line-height:36px; font-weight:700; margin:0 4px;">{total}</div>'
html += f'</div>'
html += f'<div style="font-size:11px;">'
html += f'<p>{num_a} + {num_b} = {b("4em")}</p>'
html += f'<p>{num_b} + {num_a} = {b("4em")}</p>'
if include_subtraction:
html += f'<p>{total} - {num_a} = {b("4em")}</p>'
html += f'<p>{total} - {num_b} = {b("4em")}</p>'
html += f'</div></div>\n'
else:
# Text-only format
html += f'<div class="abox">'
html += f'<p><b>Fact Family: {num_a}, {num_b}, {total}</b></p>'
html += f'<p>{num_a} + {num_b} = {b("4em")}</p>'
html += f'<p>{num_b} + {num_a} = {b("4em")}</p>'
if include_subtraction:
html += f'<p>{total} - {num_a} = {b("4em")}</p>'
html += f'<p>{total} - {num_b} = {b("4em")}</p>'
html += f'</div>\n'
html += '</div>'
return html
def build_make_10_strategy(data: dict) -> str:
"""
Make-a-10 strategy step-by-step activities.
Data:
- problems: list of (a, b) pairs where a >= 5
- show_steps: bool (show step-by-step blanks, default True)
- note: str (optional instruction)
"""
problems = data.get("problems", [])
show_steps = data.get("show_steps", True)
note = data.get("note", "Use make-a-10 strategy. Break apart the second number.")
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c2">\n'
for num_a, num_b in problems:
if show_steps:
# Calculate the make-a-10 steps
needed_for_10 = 10 - num_a
remainder = num_b - needed_for_10
html += f'<div class="abox">'
html += f'<p style="font-size:14px; font-weight:700;">{num_a} + {num_b} = {b("4em")}</p>'
html += f'<p style="font-size:11px;">{num_a} + {b("1.5em")} = 10, 10 + {b("1.5em")} = {b("4em")}</p>'
html += f'</div>\n'
else:
html += f'<div class="abox" style="text-align:center;"><p>{num_a} + {num_b} = {b("4em")}</p></div>\n'
html += '</div>'
return html
def build_pairs_to_make(data: dict) -> str:
"""
Pairs that make a target number.
Data:
- target: int (the target sum, e.g., 10)
- pairs: list of (a, b) or (a, None) for blanks
- note: str (optional instruction)
"""
target = data.get("target", 10)
pairs = data.get("pairs", [])
note = data.get("note", f"Fill in the missing number to make {target}.")
cols = data.get("cols", 3)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for num_a, num_b in pairs:
a_display = str(num_a) if num_a is not None else b("3em")
b_display = str(num_b) if num_b is not None else b("3em")
html += f'<div class="abox" style="text-align:center;"><p>{a_display} + {b_display} = {target}</p></div>\n'
html += '</div>'
return html
def build_missing_addend(data: dict) -> str:
"""
Missing addend problems.
Data:
- problems: list of (known, target) where unknown + known = target
- note: str (optional instruction)
"""
problems = data.get("problems", [])
note = data.get("note", "What number is missing?")
cols = data.get("cols", 2)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for known, target in problems:
html += f'<div class="abox" style="text-align:center;"><p>{b("4em")} + {known} = {target}</p></div>\n'
html += '</div>'
return html
def build_missing_subtrahend(data: dict) -> str:
"""
Missing subtrahend problems.
Data:
- problems: list of (minuend, difference) where minuend - unknown = difference
- note: str (optional instruction)
"""
problems = data.get("problems", [])
note = data.get("note", "What number is missing?")
cols = data.get("cols", 2)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for minuend, difference in problems:
html += f'<div class="abox" style="text-align:center;"><p>{minuend} - {b("4em")} = {difference}</p></div>\n'
html += '</div>'
return html
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# PHASE 2: Column Math Builders
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def build_column_addition(data: dict) -> str:
"""
Vertical column addition with regrouping.
Data:
- problems: list of (a, b) tuples
- digits: int (2 or 3 digit problems, default 2)
- show_regrouping: bool (show regrouping circles, default False)
- note: str (optional instruction)
"""
from math_helpers import col_add
problems = data.get("problems", [])
digits = data.get("digits", 2)
note = data.get("note", "Add using columns. Regroup if needed.")
cols = data.get("cols", 3)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}" style="gap:10px;">\n'
for a, b_val in problems:
html += f'<div class="abox" style="text-align:center;">{col_add(a, b_val, digits)}</div>\n'
html += '</div>'
return html
def build_column_subtraction(data: dict) -> str:
"""
Vertical column subtraction with regrouping.
Data:
- problems: list of (a, b) tuples
- digits: int (2 or 3 digit problems, default 2)
- note: str (optional instruction)
"""
from math_helpers import col_sub
problems = data.get("problems", [])
digits = data.get("digits", 2)
note = data.get("note", "Subtract using columns. Regroup if needed.")
cols = data.get("cols", 3)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}" style="gap:10px;">\n'
for a, b_val in problems:
html += f'<div class="abox" style="text-align:center;">{col_sub(a, b_val, digits)}</div>\n'
html += '</div>'
return html
def build_regrouping_visual(data: dict) -> str:
"""
Visual regrouping with base-10 blocks explanation.
Data:
- problems: list of (a, b, operation) where operation is "add" or "subtract"
- note: str (optional instruction)
"""
problems = data.get("problems", [])
note = data.get("note", "Show the regrouping with base-10 blocks.")
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c2">\n'
for a, b, operation in problems:
html += f'<div class="abox">'
if operation == "add":
html += f'<p style="font-size:14px; font-weight:700;">{a} + {b}</p>'
html += f'<p style="font-size:11px;">Regroup 10 ones as 1 ten</p>'
html += f'<div class="draw-box" style="min-height:50px;"></div>'
else:
html += f'<p style="font-size:14px; font-weight:700;">{a} - {b}</p>'
html += f'<p style="font-size:11px;">Regroup 1 ten as 10 ones</p>'
html += f'<div class="draw-box" style="min-height:50px;"></div>'
html += f'</div>\n'
html += '</div>'
return html
def build_two_step_problems(data: dict) -> str:
"""
Two-step word problems.
Data:
- problems: list of dicts with "text", "step1", "step2", "blanks"
- note: str (optional instruction)
"""
problems = data.get("problems", [])
note = data.get("note", "Read carefully. These problems have two steps!")
html = f'<p class="note">{note}</p>'
html += '<div class="wp">\n'
for i, problem in enumerate(problems, 1):
text = problem.get("text", "")
blanks = problem.get("blanks", 2)
blank_html = " ".join([b("4em") for _ in range(blanks)])
html += f'<div style="margin-bottom:8px;">'
html += f'<p><b>{i}.</b> {text}</p>'
html += f'<p>{blank_html}</p>'
html += f'<div class="draw-box" style="min-height:40px;"></div>'
html += f'</div>\n'
html += '</div>'
return html
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# PHASE 3: Measurement, Time & Money Builders
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def build_ruler_measurement(data: dict) -> str:
"""
Ruler measurement activities.
Data:
- lines: list of {"length_px": int, "label": str}
- unit: str ("inches" or "cm")
- include_ruler: bool (show ruler graphic, default True)
- note: str (optional instruction)
"""
from math_helpers import ruler_inches, ruler_cm, measure_line
lines = data.get("lines", [])
unit = data.get("unit", "inches")
include_ruler = data.get("include_ruler", True)
note = data.get("note", "Use the ruler to measure each line.")
html = f'<p class="note">{note}</p>'
if include_ruler:
html += ruler_inches() if unit == "inches" else ruler_cm()
html += '<br><br>'
html += '<div class="mgrid c2">\n'
for line in lines:
length_px = line.get("length_px", 100)
label = line.get("label", "")
html += f'<div class="abox">'
html += f'<div style="display:flex; align-items:center; gap:6px;">'
html += f'<div class="measure-line" style="width:{length_px}px;"></div>'
html += f'<span>{label if label else ""} {b("2em")} {unit}</span>'
html += f'</div></div>\n'
html += '</div>'
return html
def build_clock_telling(data: dict) -> str:
"""
Analog clock time-telling activities.
Data:
- times: list of {"hour": int, "minute": int, "blank": bool}
- show_digital: bool (include digital time blank, default True)
- note: str (optional instruction)
"""
from math_helpers import clock_svg
times = data.get("times", [])
show_digital = data.get("show_digital", True)
note = data.get("note", "Write the time shown on each clock.")
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c3">\n'
for time in times:
hour = time.get("hour", 12)
minute = time.get("minute", 0)
blank = time.get("blank", False)
html += f'<div class="abox" style="text-align:center;">'
html += f'{clock_svg(hour, minute, size=50)}'
html += f'<p style="margin-top:6px;">'
if blank:
html += f'{b("3em")}:{b("3em")}'
else:
html += f'{hour:02d}:{minute:02d}'
html += f'</p></div>\n'
html += '</div>'
return html
def build_coin_identification(data: dict) -> str:
"""
Coin identification and values.
Data:
- coins: list of {"type": str, "count": int, "blank_value": bool}
- note: str (optional instruction)
"""
from math_helpers import coin_svg
coins = data.get("coins", [])
note = data.get("note", "Identify each coin and write its value.")
# Coin values
values = {"penny": 1, "nickel": 5, "dime": 10, "quarter": 25}
names = {"penny": "Penny", "nickel": "Nickel", "dime": "Dime", "quarter": "Quarter"}
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c3">\n'
for coin in coins:
coin_type = coin.get("type", "penny")
count = coin.get("count", 1)
blank_value = coin.get("blank_value", False)
html += f'<div class="abox" style="text-align:center;">'
# Show the coin(s)
for _ in range(count):
html += f'{coin_svg(coin_type, size=24)}'
html += f'<p style="margin-top:4px;">{names.get(coin_type, coin_type)}</p>'
if blank_value:
html += f'<p>{b("3em")}ยข</p>'
else:
html += f'<p>{values.get(coin_type, 0)}ยข</p>'
html += f'</div>\n'
html += '</div>'
return html
def build_money_adding(data: dict) -> str:
"""
Adding money with coins.
Data:
- problems: list of {"coins": [{"type": str, "count": int}, ...], "blank": bool}
- note: str (optional instruction)
"""
from math_helpers import coin_svg
problems = data.get("problems", [])
note = data.get("note", "Count the coins and write the total.")
values = {"penny": 1, "nickel": 5, "dime": 10, "quarter": 25}
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c2">\n'
for problem in problems:
coins = problem.get("coins", [])
blank = problem.get("blank", True)
# Calculate total
total = sum(values.get(c["type"], 0) * c["count"] for c in coins)
html += f'<div class="abox" style="text-align:center;">'
# Show all coins
for coin in coins:
for _ in range(coin.get("count", 1)):
html += f'{coin_svg(coin["type"], size=20)}'
html += f'<p style="margin-top:6px;">$'
if blank:
html += f'{b("2em")}.{b("2em")}'
else:
html += f'{total // 100}.{total % 100:02d}'
html += f'</p></div>\n'
html += '</div>'
return html
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# PHASE 4: Geometry & Data Builders
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def build_picture_graph(data: dict) -> str:
"""
Picture graph reading/creating.
Data:
- categories: list of {"name": str, "count": int, "symbol": str, "blank": bool}
- key_value: int (what each symbol represents, default 1)
- note: str (optional instruction)
"""
categories = data.get("categories", [])
key_value = data.get("key_value", 1)
note = data.get("note", "Read the picture graph and answer the questions.")
symbol = data.get("symbol", "โ
")
html = f'<p class="note">{note}</p>'
html += '<div class="abox" style="margin-bottom:8px;">'
html += f'<p><b>Key:</b> {symbol} = {key_value}</p></div>\n'
html += '<div class="mgrid c1">\n'
for cat in categories:
name = cat.get("name", "")
count = cat.get("count", 0)
blank = cat.get("blank", False)
html += f'<div class="abox">'
html += f'<p><b>{name}:</b> '
if blank:
html += f'{b("6em")} items'
else:
# Show symbols
symbols = symbol * (count // key_value)
html += f'{symbols} ({count} items)'
html += f'</p></div>\n'
html += '</div>'
return html
def build_bar_graph(data: dict) -> str:
"""
Bar graph reading/creating.
Data:
- data: list of {"label": str, "value": int, "blank": bool}
- max_value: int (maximum value for scale)
- note: str (optional instruction)
"""
from math_helpers import bar_graph_svg
entries = data.get("entries", [])
max_value = data.get("max_value", 20)
note = data.get("note", "Read the bar graph.")
# Extract values and labels
values = [e["value"] for e in entries if not e.get("blank", False)]
labels = [e["label"] for e in entries]
colors = [BLUE, GREEN, RED, ORANGE, PURPLE, PINK, TEAL, GOLD][:len(entries)]
html = f'<p class="note">{note}</p>'
if values:
html += bar_graph_svg(values, colors, labels, width=250, height=120)
html += '<br><br>'
html += '<div class="mgrid c2">\n'
for entry in entries:
label = entry.get("label", "")
value = entry.get("value", 0)
blank = entry.get("blank", False)
html += f'<div class="abox"><p><b>{label}:</b> '
if blank:
html += f'{b("4em")}'
else:
html += f'{value}'
html += f'</p></div>\n'
html += '</div>'
return html
def build_shape_attributes(data: dict) -> str:
"""
Shape attribute identification (sides, vertices, angles).
Data:
- shapes: list of {"name": str, "sides": int, "vertices": int, "blanks": list}
- note: str (optional instruction)
"""
from math_helpers import shape_svg
shapes = data.get("shapes", [])
note = data.get("note", "Count the sides and vertices of each shape.")
html = f'<p class="note">{note}</p>'
html += '<table class="tbl">\n'
html += '<tr><th>Shape</th><th>Picture</th><th>Sides</th><th>Vertices</th></tr>\n'
for shape in shapes:
name = shape.get("name", "")
sides = shape.get("sides", 0)
vertices = shape.get("vertices", 0)
blanks = shape.get("blanks", []) # e.g., ["sides", "vertices"]
html += f'<tr>'
html += f'<td>{name}</td>'
html += f'<td style="text-align:center;">{shape_svg(name.lower(), size=30)}</td>'
if "sides" in blanks:
html += f'<td>{b("3em")}</td>'
else:
html += f'<td>{sides}</td>'
if "vertices" in blanks:
html += f'<td>{b("3em")}</td>'
else:
html += f'<td>{vertices}</td>'
html += f'</tr>\n'
html += '</table>'
return html
def build_symmetry(data: dict) -> str:
"""
Line of symmetry activities.
Data:
- shapes: list of {"name": str, "has_symmetry": bool, "lines": int}
- draw_activity: bool (include drawing lines of symmetry)
- note: str (optional instruction)
"""
shapes = data.get("shapes", [])
draw_activity = data.get("draw_activity", True)
note = data.get("note", "Draw the line(s) of symmetry.")
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c3">\n'
for shape in shapes:
name = shape.get("name", "")
lines = shape.get("lines", 0)
html += f'<div class="abox" style="text-align:center; padding:15px;">'
html += f'<p style="font-size:11px; margin-bottom:4px;"><b>{name}</b></p>'
html += f'<div style="border:2px solid #333; border-radius:4px; width:60px; height:60px; margin:0 auto; position:relative;"></div>'
html += f'<p style="font-size:10px; margin-top:4px;">{b("2em")} line(s) of symmetry</p>'
html += f'</div>\n'
html += '</div>'
return html
def build_area_perimeter(data: dict) -> str:
"""
Area and perimeter grid activities.
Data:
- shapes: list of {"rows": int, "cols": int, "blank_area": bool, "blank_perimeter": bool}
- note: str (optional instruction)
"""
from math_helpers import grid_svg
shapes = data.get("shapes", [])
note = data.get("note", "Count the squares for area. Count the units around for perimeter.")
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c2">\n'
for shape in shapes:
rows = shape.get("rows", 2)
cols = shape.get("cols", 3)
blank_area = shape.get("blank_area", True)
blank_perimeter = shape.get("blank_perimeter", True)
area = rows * cols
perimeter = 2 * (rows + cols)
html += f'<div class="abox">'
html += f'{grid_svg(rows, cols, cell=12, gap=1, color=BLUE)}'
html += f'<p style="margin-top:6px;">'
html += f'Area: '
if blank_area:
html += f'{b("4em")} squares'
else:
html += f'{area} squares'
html += f'</p>'
html += f'<p>Perimeter: '
if blank_perimeter:
html += f'{b("4em")} units'
else:
html += f'{perimeter} units'
html += f'</p></div>\n'
html += '</div>'
return html
def build_fraction_circle(data: dict) -> str:
"""
Fraction circle/pie chart activities.
Data:
- fractions: list of {"numerator": int, "denominator": int, "blank": bool}
- note: str (optional instruction)
"""
from math_helpers import fraction_svg
fractions = data.get("fractions", [])
note = data.get("note", "Write the fraction shown.")
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c4">\n'
for frac in fractions:
num = frac.get("numerator", 1)
denom = frac.get("denominator", 2)
blank = frac.get("blank", True)
html += f'<div class="abox" style="text-align:center;">'
html += f'{fraction_svg(num, denom, size=40)}'
html += f'<p style="margin-top:4px; font-size:14px;">'
if blank:
html += f'<span style="border-bottom:1.5px solid #333; min-width:2em; display:inline-block;"> </span>'
html += f'<span style="border-bottom:1.5px solid #333; min-width:2em; display:inline-block;"> </span>'
else:
html += f'{num}/{denom}'
html += f'</p></div>\n'
html += '</div>'
return html
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# PHASE 5: Multiplication & Division Builders
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def build_equal_groups(data: dict) -> str:
"""
Equal groups/multiplication introduction.
Data:
- groups: list of {"num_groups": int, "items_per_group": int, "blank_total": bool}
- note: str (optional instruction)
"""
from math_helpers import eq_group_svg, circle_svg
groups = data.get("groups", [])
note = data.get("note", "Count the equal groups and write the total.")
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c2">\n'
for group in groups:
num_groups = group.get("num_groups", 2)
items_per_group = group.get("items_per_group", 3)
blank_total = group.get("blank_total", True)
total = num_groups * items_per_group
html += f'<div class="abox">'
html += f'<p style="font-size:11px; margin-bottom:4px;">{num_groups} groups of {items_per_group}</p>'
# Show the groups
for _ in range(num_groups):
html += f'{eq_group_svg(items_per_group, BLUE)}'
html += f'<p style="margin-top:6px;">Total: '
if blank_total:
html += f'{b("4em")}'
else:
html += f'{total}'
html += f'</p></div>\n'
html += '</div>'
return html
def build_skip_counting(data: dict) -> str:
"""
Skip counting with blanks.
Data:
- sequences: list of {"start": int, "step": int, "count": int, "blank_indices": list}
- note: str (optional instruction)
"""
from math_helpers import skip_count_html
sequences = data.get("sequences", [])
note = data.get("note", "Skip count by the given number.")
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c2">\n'
for seq in sequences:
start = seq.get("start", 0)
step = seq.get("step", 5)
count = seq.get("count", 10)
blank_indices = seq.get("blank_indices", [])
# Generate sequence
numbers = [start + i * step for i in range(count)]
html += f'<div class="abox"><p style="font-size:11px; margin-bottom:4px;">Count by {step}:</p>'
html += f'<div class="skrow">{skip_count_html(numbers, blank_indices, BLUE)}</div></div>\n'
html += '</div>'
return html
def build_times_table(data: dict) -> str:
"""
Times table practice.
Data:
- factor: int (which times table, e.g., 5)
- blanks: list of indices to blank out (0-10)
- note: str (optional instruction)
"""
from math_helpers import times_table_html
factor = data.get("factor", 5)
blanks = data.get("blanks", [])
note = data.get("note", f"Practice the {factor}s times table.")
color = data.get("color", BLUE)
html = f'<p class="note">{note}</p>'
html += '<div class="abox">\n'
# Build custom times table with blanks
html += f'<h4 style="color:{color}; margin-bottom:8px;">The {factor}s</h4>'
for i in range(11):
result = factor * i
if i in blanks:
html += f'<div style="padding:2px 0; font-size:13px;"><span>{factor} ร {i} = </span>{b("3em")}</div>'
else:
html += f'<div style="padding:2px 0; font-size:13px;"><span>{factor} ร {i} = </span><span style="color:{color}; font-weight:700;">{result}</span></div>'
html += '</div>'
return html
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# CELEBRATION: Scavenger Hunt & Certificate
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def build_scavenger_hunt(data: dict) -> str:
"""
Math scavenger hunt cards.
Data:
- clues: list of {"question": str, "answer": str, "hint": str}
- note: str (optional instruction)
"""
clues = data.get("clues", [])
note = data.get("note", "Solve each clue and find the answer!")
html = f'<p class="note">{note}</p>'
html += '<div class="mgrid c2">\n'
for i, clue in enumerate(clues, 1):
question = clue.get("question", "")
hint = clue.get("hint", "")
html += f'<div class="scavenger-card">'
html += f'<p class="clue"><b>Clue {i}:</b> {question}</p>'
if hint:
html += f'<p style="font-size:10px; color:#999; margin-top:4px;">Hint: {hint}</p>'
html += f'<p style="margin-top:6px;">Answer: {b("6em")}</p>'
html += f'</div>\n'
html += '</div>'
return html
def build_certificate(data: dict) -> str:
"""
Completion certificate.
Data:
- title: str (certificate title)
- subtitle: str (subtitle)
- achievement: str (what was achieved)
- include_name: bool (include name line, default True)
"""
title = data.get("title", "Certificate of Achievement")
subtitle = data.get("subtitle", "This certifies that")
achievement = data.get("achievement", "has completed 2nd Grade Math")
include_name = data.get("include_name", True)
html = '<div class="cert-border">\n'
html += f'<div class="cert-title">{title}</div>\n'
html += f'<div class="cert-subtitle">{subtitle}</div>\n'
if include_name:
html += f'<div class="cert-name"></div>\n'
html += f'<div class="cert-body">{achievement}</div>\n'
html += f'<div style="margin-top:16px;">'
html += f'<div style="border-top:2px solid #333; width:120px; margin:0 auto;"></div>'
html += f'<p style="font-size:10px; margin-top:4px;">Teacher Signature</p>'
html += f'</div>'
html += f'<div style="margin-top:16px;">'
html += f'<div style="border-top:2px solid #333; width:100px; margin:0 auto;"></div>'
html += f'<p style="font-size:10px; margin-top:4px;">Date</p>'
html += f'</div>\n'
html += '</div>'
return html
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Utility: Get all available builders
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def build_addition_facts(data: dict) -> str:
"""
Simple addition fact practice.
Data:
- facts: list of strings like "8 + 5" or tuples (left, right)
- note: str (optional instruction)
- cols: int (number of columns, default 2)
- font_size: int (default 16)
"""
facts = data.get("facts", [])
note = data.get("note", "Solve each problem.")
cols = data.get("cols", 2)
font_size = data.get("font_size", 16)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for fact in facts:
if isinstance(fact, tuple):
fact_str = f"{fact[0]} + {fact[1]}"
else:
fact_str = fact
html += f'<div class="abox"><p style="font-size:{font_size}px;">{fact_str} = {b("4em")}</p></div>\n'
html += '</div>'
return html
def build_addition_word_problems(data: dict) -> str:
"""
Addition word problems.
Data:
- problems: list of dicts with "text" key
- note: str (optional instruction)
"""
problems = data.get("problems", [])
note = data.get("note", "Read carefully and solve.")
html = f'<div class="wp">\n'
for problem in problems:
text = problem.get("text", "")
html += f'<p>{text} {b("6em")}</p>\n'
html += '</div>\n'
return html
def build_missing_addend_facts(data: dict) -> str:
"""
Missing addend practice (e.g., 5 + ___ = 10).
Data:
- facts: list of strings like "5 + ___ = 10"
- note: str (optional instruction)
- cols: int (number of columns, default 2)
"""
facts = data.get("facts", [])
note = data.get("note", "What number is missing?")
cols = data.get("cols", 2)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for fact in facts:
html += f'<div class="abox" style="text-align:center;"><p>{fact}</p></div>\n'
html += '</div>'
return html
def build_subtraction_facts(data: dict) -> str:
"""
Simple subtraction fact practice.
Data:
- facts: list of tuples (minuend, subtrahend) or strings like "10 - 3"
- note: str (optional instruction)
- cols: int (number of columns, default 2)
- font_size: int (font size in px, default 16)
"""
facts = data.get("facts", [])
note = data.get("note", "Solve each subtraction problem.")
cols = data.get("cols", 2)
font_size = data.get("font_size", 16)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for fact in facts:
if isinstance(fact, tuple):
minuend, subtrahend = fact
fact_str = f"{minuend} โ {subtrahend} = {b('4em')}"
else:
# Parse string like "10 - 3"
parts = fact.replace("-", "").replace("โ", "").split()
minuend, subtrahend = int(parts[0]), int(parts[1])
fact_str = f"{minuend} โ {subtrahend} = {b('4em')}"
html += f'<div class="abox" style="text-align:center; font-size:{font_size}px;"><p>{fact_str}</p></div>\n'
html += '</div>'
return html
def build_subtraction_word_problems(data: dict) -> str:
"""
Subtraction word problems.
Data:
- problems: list of dicts with "text" key
- note: str (optional instruction)
"""
problems = data.get("problems", [])
note = data.get("note", "Read each problem carefully and solve it.")
html = f'<div class="wp">\n'
for problem in problems:
text = problem.get("text", "")
html += f'<p>{text}</p>\n'
html += '</div>\n'
return html
def build_fact_families_subtraction(data: dict) -> str:
"""
Fact family practice with subtraction focus.
Data:
- families: list of [addend1, addend2, sum] triples
- note: str (optional instruction)
- cols: int (number of columns, default 2)
"""
families = data.get("families", [])
note = data.get("note", "Write the related subtraction facts.")
cols = data.get("cols", 2)
html = f'<p class="note">{note}</p>'
html += f'<div class="mgrid c{cols}">\n'
for family in families:
addend1, addend2, total = family[0], family[1], family[2]
html += f'<div class="abox"><p>{addend1} + {addend2} = {total}</p>'
html += f'<p>{total} โ {addend1} = {b("4em")}</p>'
html += f'<p>{total} โ {addend2} = {b("4em")}</p></div>\n'
html += '</div>'
return html
def build_challenge_box(data: dict) -> str:
"""
Challenge box with star border.
Data:
- problems: list of dicts with "text" key (HTML allowed)
"""
problems = data.get("problems", [])
html = '<div class="starbox"><p><b>Challenge</b></p>'
for problem in problems:
html += f'<p>{problem.get("text", "")}</p>'
html += '</div>'
return html
def get_available_builders() -> list:
"""Return list of all available builder function names."""
return [
# Phase 1
"build_number_bonds",
"build_fact_families",
"build_make_10_strategy",
"build_pairs_to_make",
"build_missing_addend",
"build_missing_subtrahend",
"build_addition_facts",
"build_addition_word_problems",
"build_missing_addend_facts",
"build_challenge_box",
"build_subtraction_facts",
"build_subtraction_word_problems",
"build_fact_families_subtraction",
# Phase 2
"build_column_addition",
"build_column_subtraction",
"build_regrouping_visual",
"build_two_step_problems",
# Phase 3
"build_ruler_measurement",
"build_clock_telling",
"build_coin_identification",
"build_money_adding",
# Phase 4
"build_picture_graph",
"build_bar_graph",
"build_shape_attributes",
"build_symmetry",
"build_area_perimeter",
"build_fraction_circle",
# Phase 5
"build_equal_groups",
"build_skip_counting",
"build_times_table",
# Celebration
"build_scavenger_hunt",
"build_certificate",
]
if __name__ == "__main__":
print("Available activity builders:")
for name in get_available_builders():
print(f" โข {name}")
print(f"\nTotal: {len(get_available_builders())} builders")