Color Sort Puzzle
Python list · Fancy terminal UI · Swaps · Undo · Smart hint · Score · Class
Game Idea
+--------------------------------------------------------------------------------------+| 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 |+--------------------------------------------------------------------------------------+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
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
is_solved(["R", "R"], ["R", "R"]) -> Trueis_solved(["R", "G"], ["R", "R"]) -> Falsecorrect_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
board = ["R", "B", "G", "Y"]target = ["R", "G", "G", "Y"]correct_count(board, target) -> 3swap_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
board = ["B", "R", "G"]swap_blocks(board, 0, 1)board becomes ["R", "B", "G"]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
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.")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
board = ["B", "R", "G"]target = ["R", "B", "G"]find_best_swap(board, target) -> (0, 1)Template
template.py
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()Quick Check
Manual run
# 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 # quitExpected 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
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
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
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)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.