You will build a complete interactive game that learns! This is a terminal-based implementation of the classic "20 Questions" game with a twist: when the computer guesses wrong, it asks you to teach it about the new animal and adds the information to its decision tree.
Features:
- 🎯 Interactive guessing game
- 🧠 Learns from mistakes
- 💾 Saves/loads tree to binary file
- ↩️ Undo/redo edits
- 🌳 Visual tree explorer with scrolling
- 🔍 Tree integrity checking
- ⚡ No recursion in gameplay (uses explicit stacks!)
Learning AI: The game starts with minimal knowledge (just 3 animals) but grows smarter as you play. Each time it fails to guess your animal, it learns by asking you for a distinguishing question and adding both the question and animal to its decision tree.
Advanced Data Structures: You'll implement multiple data structures from scratch in C:
- Binary Decision Tree - Stores questions (internal nodes) and animals (leaves)
- Stacks - For iterative tree traversal and undo/redo operations
- Queue - For breadth-first traversal during save/load
- Hash Table - For indexing attributes and potential query optimization
No Recursion for Gameplay: Unlike typical tree problems, you'll use explicit stacks instead of recursion for the game loop. This teaches you how compilers convert recursive calls to iterative code.
Persistent Storage: Your tree saves to a compact binary format using BFS traversal, so the game "remembers" everything it learned across sessions.
Undo/Redo System: Made a mistake teaching the game? No problem! Full undo/redo support using dual stacks, just like a text editor.
Think of an animal, and I'll try to guess it!
Does it live in water? (y/n): n
Is it a Dog? (y/n): n
What animal were you thinking of? Cat
Give me a yes/no question to distinguish Cat from Dog: Does it meow?
For Cat, what is the answer? (y/n): y
Thanks! I'll remember that.
[Play again...]
Does it live in water? (y/n): n
Does it meow? (y/n): y
Is it a Cat? (y/n): y
Yay! I guessed it!
Initial tree:
"Does it live in water?"
/ \
"Fish" "Dog"
After learning about Cat:
"Does it live in water?"
/ \
"Fish" "Does it meow?"
/ \
"Cat" "Dog"
After learning about Bird:
"Does it live in water?"
/ \
"Fish" "Does it meow?"
/ \
"Cat" "Does it fly?"
/ \
"Bird" "Dog"
By completing this lab, you will:
- ✅ Improve your skills in manual memory management in C (malloc/free)
- ✅ Implement iterative algorithms using explicit stacks
- ✅ Build multiple data structures from scratch
- ✅ Work with binary file I/O and serialization
- ✅ Handle complex pointer relationships
- ✅ Debug with valgrind and gdb
- ✅ Design undo/redo systems
- ✅ Manage a multi-week project
Time Required: 33-49 hours total
- Week 1: Data structures (20-25 hours)
- Week 2: Game logic and persistence (8-12 hours)
- Week 3: Testing and polish (5-12 hours)
Lines of Code: You'll write 370-530 lines across 4 files TODOs: 33 total (32 required + 1 optional challenge)
Deliverables:
- Complete C implementation with all TODOs
- Write-up (1-2 pages) analyzing design and complexity
- All unit tests passing
- Zero memory leaks (verified with valgrind or memory sanitizer)
# 1. Build and run
cd src
make
make run
# Press 'p' to play - you'll see an error (expected!)
# Press 'q' to quit
# 2. Run tests
make test
# Tests will fail - that's normal, you haven't implemented anything yet!
# 3. Start implementing
# Open ds.c and find TODO 1Why the error? The UI works, but you need to implement node creation (TODOs 1-2) and uncomment code in main.c initialize_tree() before the tree works.
// In ds.c
create_question_node() // Malloc node, strdup text, isQuestion=1
create_animal_node() // Similar but isQuestion=0
free_tree() // Recursive: free children, then self
count_nodes() // Recursive: count all nodesmain.c initialize_tree() function!
Test: make test - node tests should pass
Stack for iterative tree traversal. Dynamic array that doubles when full.
fs_init() // Malloc array, capacity=16
fs_push() // Resize if needed, add frame
fs_pop() // Return frames[--size]
fs_empty() // Return size == 0
fs_free() // Free arrayTest: make test - stack tests should pass
Linked list for BFS traversal.
q_init() // front=NULL, rear=NULL
q_enqueue() // Malloc node, link at rear
q_dequeue() // Remove from front, update rear if empty!
q_empty() // Return front == NULL
q_free() // Dequeue allTest: make test - queue tests should pass
Separate chaining for attribute indexing.
canonicalize() // "Does it meow?" → "does_it_meow"
h_hash() // djb2: hash = ((hash << 5) + hash) + c
h_init() // Calloc buckets
h_put() // Search chain, add to list or create entry
h_contains() // Search for key-value pair
h_get_ids() // Return all IDs for key
h_free() // Free chains, keys, arraysTest: make test - hash tests should pass
Similar to Frame Stack but for Edit structs.
undo_last_edit() // Pop from g_undo, restore pointers, push to g_redo
redo_last_edit() // Pop from g_redo, reapply, push to g_undoIterative traversal using explicit stack. Learning phase creates new nodes and records edits.
Key steps:
- Push root frame
- While stack not empty:
- Pop frame
- If question: ask, push child based on answer
- If leaf: guess, or learn if wrong
- Learning: get animal/question, create nodes, update tree, record edit
Test: make run and play!
Use BFS to serialize tree with node IDs.
save_tree():
- BFS to assign IDs (0, 1, 2, ...)
- Write header (magic, version, count)
- Write each node (isQuestion, textLen, text, yesId, noId)
load_tree():
- Read header, validate
- Read all nodes into array
- Link using stored IDs
- Set g_root = nodes[0]
Test: make test - persistence tests should pass
BFS to verify: questions have 2 children, leaves have 0 children.
Extra credit challenge!
make test # See which tests pass# 1. Unit tests
make clean && make test
# 2. Memory leaks
make valgrind-test
# 3. Interactive
make run
# Test: play, learn, undo, redo, save, load, integrity
# 4. Final check
make valgrind # Run full program, should show no leaks| ❌ Wrong | ✅ Correct |
|---|---|
malloc(sizeof(Node*)) |
malloc(sizeof(Node)) |
free(node); free(node->text); |
free(node->text); free(node); |
node->text = question; |
node->text = strdup(question); |
malloc(textLen) |
malloc(textLen + 1) for strings |
| ❌ Wrong | ✅ Correct |
|---|---|
if (size > capacity) |
if (size >= capacity) |
q->front = NULL; when empty |
q->front = NULL; q->rear = NULL; |
| ❌ Wrong | ✅ Correct |
|---|---|
entry->key = key; |
entry->key = strdup(key); |
Node *n = malloc(sizeof(Node));
n->text = strdup(text); // Allocates memory!
n->yes = n->no = NULL;
n->isQuestion = 1; // or 0 for animals
return n;void free_tree(Node *node) {
if (!node) return;
free_tree(node->yes); // Children first!
free_tree(node->no);
free(node->text); // Then text
free(node); // Then node
}if (s->size >= s->capacity) {
s->capacity *= 2;
s->frames = realloc(s->frames, s->capacity * sizeof(Frame));
}
s->frames[s->size++] = (Frame){node, answeredYes};QueueNode *temp = q->front;
*node = temp->treeNode;
*id = temp->id;
q->front = q->front->next;
if (!q->front) q->rear = NULL; // Empty queue!
free(temp);// 1. Search chain for existing key
// 2. If found: check for duplicate ID, add if new
// 3. If not found: create entry with strdup(key), insert at headFor complete hints, see HINTS.md.
make # Build main program
make test # Build and run tests
make run # Run the game
make valgrind # Check for memory leaks
make clean # Remove build files
make help # Show all targetsgdb ./guess_animal
(gdb) run
(gdb) bt # Backtrace shows where it crashedvalgrind --leak-check=full --track-origins=yes ./run_tests
# Look for "definitely lost" - those are real leaks# Read the assertion that failed
# Check line number in tests.c
# Add printf to see what's happening- ds.c - All data structures (26 TODOs)
- game.c - Game logic and undo/redo (3 TODOs)
- persist.c - Save/load (2 TODOs)
- utils.c - Integrity checker (2 TODOs)
- lab5.h - All type definitions
- main.c - UI (only uncomment initialize_tree after TODOs 1-2!)
- tests.c - Unit tests
- Makefile - Build system
- README.md - This file (complete guide)
- HINTS.md - Detailed implementation hints
- BUILD.md - Full build and debugging reference
- Correctness (50%): All tests pass, game works, undo/redo, save/load
- Code Quality (20%): Clean code, good names, comments
- Memory Safety (20%): No leaks, proper malloc/free
- Write-up (10%): Design choices, complexity analysis
Submission Checklist:
- All 32 required TODOs implemented
-
make testpasses -
make valgrindshows no leaks - Game works (play, learn, undo, redo, save, load)
- Code compiles without warnings
- Write-up included
- Data structures: 15-20 hours
- Game logic: 6-10 hours
- Persistence: 4-6 hours
- Testing/debugging: 6-10 hours
- Write-up: 2-3 hours
- Total: 33-49 hours
Start early! This is not a weekend project.
Q: Can I use recursion?
A: Only for free_tree() and count_nodes(). Everything else must be iterative!
Q: How do I know if my implementation is correct?
A: Run make test after each TODO. Tests verify correctness.
Q: Should I implement TODO 30 (shortest path)?
A: It's optional. Focus on required TODOs first.
Q: What if valgrind shows "still reachable"?
A: Usually okay (ncurses cleanup). Focus on "definitely lost".
Q: Can I add helper functions?
A: Yes! Add static helpers in .c files.
Q: Tests pass but game crashes?
A: Game loop is complex. Use gdb to find where it crashes.
- Read TODO comments in the code
- Check HINTS.md for implementation details
- Look at tests.c to see expected behavior
- Try printf debugging or gdb
- Ask on Ed/Piazza or office hours
Include: TODO number, error message, what you tried, relevant code snippet.
malloc(size) // Allocate
free(ptr) // Free
realloc(ptr, size) // Resize
strdup(s) // Duplicate string (allocates!)strlen(s) // Length
strcmp(s1, s2) // Compare (0 = equal)
strcpy(dest, src) // Copyfopen(file, "rb") // Open for reading binary
fopen(file, "wb") // Open for writing binary
fread(buf, 1, n, fp) // Read n bytes
fwrite(buf, 1, n, fp) // Write n bytes
fclose(fp) // Closeisalnum(c) // Alphanumeric?
isspace(c) // Whitespace?
tolower(c) // To lowercase- Test after every TODO - Catch bugs early
- Use valgrind frequently - Don't wait for the end
- Draw diagrams - Visualize pointers and structures
- Start with TODOs 1-2 - Everything builds on nodes
- Don't forget to uncomment main.c after TODOs 1-2!
Ready to start? Open ds.c and implement TODO 1! 🚀
Questions? Check BUILD.md or HINTS.md for more details.