homework.wenqian.dev
< Back to index
Tutorial 12026-06-25

Color Sort Puzzle

Python list · Fancy terminal UI · Swaps · Undo · Smart hint · Score · Class

Game Idea

terminal
+--------------------------------------------------------------------------------------+|                                 COLOR SORT PUZZLE                                   ||                     Sort one Python list by swapping two indexes.                   ||                                                                                      || Level: Candy Lane   Par: 6   Moves: 0   Hints: 0   Undos: 0                         ||                                                                                      || Target  R  R  G  G  B  B  Y  Y                                                      || Index   0  1  2  3  4  5  6  7                                                      || Board   B  R  Y  G  R  B  G  Y                                                      || Status  .. OK .. OK .. OK .. OK                                                     ||                                                                                      || Progress [#########---------------] 3/8                                              ||                                                                                      || Commands: 0 4 = swap | h = hint | u = undo | r = restart | m = menu | q = quit      |+--------------------------------------------------------------------------------------+
Rule: The game is still simple: the player swaps two indexes until one 1D list matches the target list. The fun comes from presentation, feedback, and small game features.

Teaching Flow

Stage 1: make the terminal feel like a game

Draw a title panel, target row, board row, index row, and command bar before teaching the full loop.

Stage 2: keep the core data as one list

The board is a 1D Python list. Fancy display does not change the data model.

Stage 3: test commands one by one

The game loop is provided. Students test swaps, hint, undo, restart, and quit to see which helper function each command uses.

Stage 4: make feedback immediate

After every swap, show whether the board improved, stayed neutral, or became worse.

Stage 5: turn list logic into game features

correct_count becomes progress. history becomes undo. find_best_swap becomes a smart hint.

TODO Reference

TODO 1

is_solved(board, target)

Task

Check whether the game is finished.

Input

board: current list. target: target list.

Output

A boolean. True means the two lists are exactly the same. False means not solved yet.

Example

python
is_solved(["R", "R"], ["R", "R"]) -> Trueis_solved(["R", "G"], ["R", "R"]) -> False
TODO 2

correct_count(board, target)

Task

Count how many positions already match the target.

Input

board: current list. target: target list. They should have the same length.

Output

An integer count.

Example

python
board  = ["R", "B", "G", "Y"]target = ["R", "G", "G", "Y"]correct_count(board, target) -> 3
TODO 3

swap_blocks(board, first, second)

Task

Swap two values inside the board list.

Input

board: current list. first: first index. second: second index.

Output

No return value. The board list is changed directly.

Example

python
board = ["B", "R", "G"]swap_blocks(board, 0, 1)board becomes ["R", "B", "G"]
TODO 4

parse_swap(command, board)

Task

Turn the player's text command into two valid indexes.

Input

command: a string such as "0 3". board: current list, used to know the valid index range.

Output

Valid input returns ((first, second), ""). Invalid input returns (None, "error message").

Example

python
parse_swap("0 3", ["R", "G", "B", "Y"]) -> ((0, 3), "")parse_swap("0 9", ["R", "G", "B", "Y"]) -> (None, "Indexes must be between 0 and 3.")
TODO 5

find_best_swap(board, target)

Task

Find a helpful hint by trying possible swaps.

Input

board: current list. target: target list.

Output

A pair of indexes such as (0, 3), or None if the board is already solved.

Example

python
board  = ["B", "R", "G"]target = ["R", "B", "G"]find_best_swap(board, target) -> (0, 1)

Template

template.py

python
import reimport sysfrom random import shuffleESC = chr(27) + "["RESET = f"{ESC}0m"USE_COLOR = sys.stdout.isatty()ANSI_RE = re.compile(re.escape(ESC) + "[0-9;]*m")LEVELS = [    {        "name": "Candy Lane",        "target": ["R", "R", "G", "G", "B", "B", "Y", "Y"],        "par": 6,    },    {        "name": "Neon Shelf",        "target": ["R", "R", "G", "G", "B", "B", "Y", "Y", "P", "P"],        "par": 8,    },]COLOR_STYLE = {    "R": "41;97",    "G": "42;30",    "B": "44;97",    "Y": "43;30",    "P": "45;97",}def paint(text, style):    if not USE_COLOR:        return text    return f"{ESC}{style}m{text}{RESET}"def bold(text):    return paint(text, "1")def dim(text):    return paint(text, "2")def block(symbol):    return paint(f" {symbol} ", COLOR_STYLE.get(symbol, "47;30"))def visible_len(text):    return len(ANSI_RE.sub("", text))def pad_visible(text, width):    return text + " " * max(0, width - visible_len(text))def print_panel(lines, width=88):    border = "+" + "-" * (width - 2) + "+"    print(border)    for line in lines:        print("| " + pad_visible(line, width - 4) + " |")    print(border)def make_board(target):    board = target.copy()    shuffle(board)    while board == target:        shuffle(board)    return boarddef draw_indexes(length):    return "Index   " + " ".join(f"{i:^3}" for i in range(length))def draw_blocks(label, values):    return f"{label:<7} " + " ".join(block(value) for value in values)def render_screen(level, board, moves, hints, undos, message):    target = level["target"]    correct = correct_count(board, target)    lines = [        bold("COLOR SORT PUZZLE"),        f"Level: {level['name']}   Par: {level['par']}   Moves: {moves}   Hints: {hints}   Undos: {undos}",        "",        draw_blocks("Target", target),        draw_indexes(len(board)),        draw_blocks("Board", board),        "",        f"Progress: {correct}/{len(target)} correct positions",        dim("Commands: 0 4 = swap | h = hint | u = undo | r = restart | q = quit"),    ]    if message:        lines.append("")        lines.append(message)    print_panel(lines)def is_solved(board, target):    # TODO 1: Check whether the puzzle is solved.    # Input:    #   board  - the current list, for example ["R", "G", "R"]    #   target - the target list, for example ["R", "R", "G"]    # Output:    #   True if board and target are exactly the same list.    #   False otherwise.    # Example:    #   is_solved(["R", "R"], ["R", "R"]) -> True    #   is_solved(["R", "G"], ["R", "R"]) -> False    return Falsedef correct_count(board, target):    # TODO 2: Count how many blocks are already in the correct position.    # Input:    #   board  - the current list    #   target - the target list    # Output:    #   An integer count.    # Example:    #   board  = ["R", "B", "G", "Y"]    #   target = ["R", "G", "G", "Y"]    #   correct_count(board, target) -> 3    # Reason:    #   index 0 is correct, index 1 is wrong, index 2 is correct, index 3 is correct.    return 0def swap_blocks(board, first, second):    # TODO 3: Swap two blocks inside board.    # Input:    #   board  - the current list    #   first  - the first index    #   second - the second index    # Output:    #   No return value is needed. This function changes board directly.    # Example:    #   board = ["B", "R", "G"]    #   swap_blocks(board, 0, 1)    #   board becomes ["R", "B", "G"]    passdef parse_swap(command, board):    # TODO 4: Read and validate a swap command.    # Input:    #   command - a string typed by the player, for example "0 3"    #   board   - the current list, used to know the valid index range    # Output:    #   If valid:    #     return ((first, second), "")    #   If invalid:    #     return (None, "a helpful error message")    # Must reject:    #   - not exactly two values, for example "0"    #   - non-number values, for example "a 3"    #   - same index twice, for example "2 2"    #   - indexes outside the board, for example "0 99"    # Example:    #   parse_swap("0 3", ["R", "G", "B", "Y"]) -> ((0, 3), "")    #   parse_swap("0 9", ["R", "G", "B", "Y"]) -> (None, "Indexes must be between 0 and 3.")    return None, "TODO: parse two indexes."def find_best_swap(board, target):    # TODO 5: Find a useful hint.    # Input:    #   board  - the current list    #   target - the target list    # Output:    #   A pair of indexes like (0, 3), or None if no hint is needed.    # Task:    #   Try every possible pair of indexes.    #   For each pair:    #     1. copy board    #     2. swap that pair in the copy    #     3. use correct_count to score the copy    #   Return the pair that gives the highest score improvement.    # Example:    #   board  = ["B", "R", "G"]    #   target = ["R", "B", "G"]    #   find_best_swap(board, target) -> (0, 1)    return Nonedef play_game():    level = LEVELS[0]    target = level["target"]    board = make_board(level["target"])    history = []    moves = 0    hints = 0    undos = 0    message = "Game loop is ready. Fill TODO 1-5 to make the game work."    while True:        render_screen(level, board, moves, hints, undos, message)        if is_solved(board, target):            print("You win! The board now matches the target list.")            return        try:            command = input("Command: ").strip().lower()        except EOFError:            command = "q"        if command in ("q", "quit", "exit"):            print("Thanks for playing Color Sort Puzzle.")            return        if command in ("r", "restart"):            board = make_board(target)            history = []            moves = 0            hints = 0            undos = 0            message = "Restarted. Try a new sequence of swaps."            continue        if command in ("u", "undo"):            if not history:                message = "Nothing to undo yet."            else:                board = history.pop()                undos += 1                message = "Undo complete."            continue        if command in ("h", "hint"):            hint = find_best_swap(board, target)            hints += 1            if hint is None:                message = "No hint yet. Finish TODO 5 to make hints smart."            else:                message = f"Hint: try swapping index {hint[0]} and index {hint[1]}."            continue        pair, error = parse_swap(command, board)        if pair is None:            message = error            continue        old_correct = correct_count(board, target)        history.append(board.copy())        swap_blocks(board, pair[0], pair[1])        moves += 1        new_correct = correct_count(board, target)        if new_correct > old_correct:            message = "Nice swap. More blocks are in the correct position."        elif new_correct == old_correct:            message = "That swap was neutral. Keep looking."        else:            message = "That swap moved some blocks away. You can undo it."if __name__ == "__main__":    play_game()
Teacher note: The template already has the terminal artwork and the game loop. Students only fill five list-focused helper functions, so every TODO visibly upgrades the game without requiring them to design the whole control flow.

Quick Check

Manual run

bash
# Save the solution as solution.py, then try:python3 solution.py# Fast smoke test:printf "q\n" | python3 solution.py# Useful commands while playing:1        # choose level 1h        # show a smart hint0 3      # swap index 0 and index 3u        # undo the last swapr        # restart current levelm        # return to level menuq        # quit

Expected behavior

  • The terminal shows a framed game panel.
  • h prints a smart hint that improves the board when possible.
  • u restores the previous board from history.
  • Valid swaps update progress and feedback immediately.

Discussion points

  • ANSI color is display only; the data is still plain strings in a list.
  • Undo is just a list of old board copies.
  • Smart hint is a nested loop over all possible swaps.
  • Score is arithmetic using moves, hints, and undos.

Next Knowledge: Class

Main idea: A class lets one thing keep its own data and its own actions together. For this game, that means stats can remember moves, hints, and undos instead of leaving three loose variables in the game loop.

Vocabulary

  • class: the blueprint, like GameStats.
  • object: one real thing made from the class, like stats.
  • attribute: data stored on the object, like stats.moves.
  • method: a function attached to the object, like stats.record_swap().

What to emphasize

  • __init__ runs when the object is created.
  • self means this object.
  • self.moves is different from a normal local variable named moves.
  • A method can change the object's attributes.

Step 1: Put Game Stats Into A Class

python
class GameStats:    def __init__(self, par):        self.par = par        self.moves = 0        self.hints = 0        self.undos = 0    def record_swap(self):        self.moves += 1    def record_hint(self):        self.hints += 1    def record_undo(self):        self.undos += 1    def summary(self):        return f"Par: {self.par} | Moves: {self.moves} | Hints: {self.hints} | Undos: {self.undos}"stats = GameStats(par=6)print(stats.summary())stats.record_swap()stats.record_swap()stats.record_hint()print(stats.summary())

Lab 1: create an object

Run stats = GameStats(6). Ask what values live inside stats before any method is called.

Lab 2: call methods

Call record_swap, record_hint, and record_undo. Print summary after each line so students can see the object changing.

Lab 3: add one method

Add is_over_par(self), returning True when moves is greater than par.

Lab 4: connect it back to the game

Replace moves, hints, and undos with one stats object in the game loop.

Step 2 Preview: One Object Can Manage A Puzzle

python
class ColorPuzzle:    def __init__(self, target, board):        self.target = target        self.board = board.copy()        self.history = []    def is_solved(self):        return self.board == self.target    def swap(self, first, second):        self.history.append(self.board.copy())        self.board[first], self.board[second] = self.board[second], self.board[first]    def undo(self):        if len(self.history) == 0:            return False        self.board = self.history.pop()        return Truepuzzle = ColorPuzzle(    target=["R", "R", "G", "G"],    board=["G", "R", "R", "G"],)print("Target:", puzzle.target)print("Board: ", puzzle.board)puzzle.swap(0, 1)print("After swap:", puzzle.board)puzzle.undo()print("After undo:", puzzle.board)
Teacher note: Do not ask students to rewrite the whole game as classes today. The goal is narrower: object = data + methods. If they understand GameStats, the class concept has landed.

Where to Slow Down

Slow down when students see that each fancy feature maps back to a list idea: board.copy for history, nested loops for hints, indexes for swaps, and equality checks for winning.