Code
Notes from the keyboard on the craft of building software, where the simplest idea is usually the one that survives.
The Thunderfury Incident
"Did someone say Thunderfury, Blessed Blade of the Windseeker?" — The IT Department, probably
I have a confession: early in college, I casually played World of Warcraft (WoW). With that off my chest, let me tell you about the time I almost got into trouble over a shell script. And how WoW was involved.
To set the scene: I was in campus housing, just getting started in Linux land, trying to master the shell. I mostly tinkered on my MacBook, but we had access to shared, virtual Linux workstations. Think of it as a free timeshare for college students. I realized something: I could broadcast messages to everyone else logged into the same machine via wall. I also had access to about 40 shared workstations across the network.
So I wrote a script and tested it for a couple of minutes in a computer lab. I got my confirmation from the confused looks on people's faces. Each person on a workstation would be spammed randomly with the simple message: “Did someone say Thunderfury, Blessed Blade of the Windseeker?”.

A day or so later, I was hanging out in the computer science lounge, talking to a friend who happened to grade for the introductory programming class. He was eager to tell me my script had hit a student’s assignment submission and muddled it. The student went to the teacher and accused me of hacking their computer. Amused, the teacher just said “oh, that’s just Illya” and graded over it.
But, how did they know it was me?
See, my script had a bit of a flaw. The -n flag in wall suppresses the banner that shows who sent the message; omitting it broadcasts your message with your username attached. So there was no hiding from it, which is why I only gave it a “light test run.”
I gave it a couple of weeks and didn’t hear anything about it. So I published a blog post, shell script attached, and called it a day. Coast was clear.
Until a few months later, when I got a text from a friend that worked in the IT department. They were upset about my shell script. And they weren’t upset that I ran it, they were upset I published it on my blog.
Now, I was worried. They were meeting later in the day to decide what action to take. I held my breath and took down the page. I got a follow-up that everything would be okay. I asked my friend to apologize on my behalf, and I wouldn’t do it again. And that’s the last I heard of it.
So, here’s that blog post.
If your school is anything like mine (engineering and science, mostly), you probably have some kind of virtual Linux machines you can SSH into. If you’ve done any digging, you might have realized that commands such as wall or write are not disabled. If you are anything like me, you probably thought about writing a shell script that will automatically log you in, spam something (i.e. the famous Thunderfury, Blessed Blade of the Windseeker) on a random machine, and leave. Well you’re in luck.
#!/bin/bash
PASSWORD="your-password-here"
USERNAME="your-username"
HOST_PREFIX="linux"
MIN_HOST=1
MAX_HOST=39
MESSAGE="Did someone say [Thunderfury, Blessed Blade of the Windseeker]?"
spam() {
local n=$(( RANDOM % (MAX_HOST - MIN_HOST + 1) + MIN_HOST ))
local host
printf -v host "%s%02d" "$HOST_PREFIX" "$n"
sshpass -p "$PASSWORD" ssh -t -l "$USERNAME" "$host" \
"printf '%s\n' '$MESSAGE' | wall"
}
while :; do
spam
sleep $(( RANDOM % 60 + 1 ))
doneSet PASSWORD, USERNAME, and HOST_PREFIX up top. MIN_HOST and MAX_HOST bound the random suffix, zero-padded to two digits (so linux01 through linux39). The loop fires on a random 1-60 second interval. Neat!
Academia Portfolio
4 years, 10 projects, 91k lines of code. Pain is temporary, GPA is forever.
Between 2014 and 2018 I wrote a lot of code for school. Below are my favorite projects from my academic years.
This repository isn't just source files; it's a collection of memories. Ten years later, I can (and will) tell you where I was when I wrote each of them. I remember how late into the night I stayed at it. I remember how I wrote them, from the elegant one-liners to the nastiest regexes and bit manipulation you will ever see. I remember why I wrote them, because pain is temporary, but GPA is forever.
If you'd rather read the notes and problem sets themselves, they're collected in Academia Notes. The full source is on GitHub.
Notes
| Document | Pages | Description |
|---|---|---|
| Curated | 284 | Curated selection of best work |
| Assignments | 498 | Homework with solutions |
| Notes | 473 | Lecture notes and study materials |
| Complete | 1,113 | Everything |
Projects
Senior Year:
Junior Year:
Sophomore Year:
1. Chess AI
A chess AI engine built from scratch using bitboards. Each position fits in a set of 64-bit integers, one bit per square, so move generation is pure bitwise operations: shifts for sliding pieces, masks to prevent wraparound at the edges of the board. On top of that, alpha-beta pruning, iterative deepening, and a custom evaluation function that considers piece position, king safety, and pawn structure.

This was my most memorable software project throughout all of college; with good reason, it was the most notorious within our computer science department. Part one of the project was generating a complete chess move engine with a simple search algorithm to explore the game space. This was one of the handful of assignments I ever turned in late. And I started it a week before it was assigned.
Bitboards were nasty; I never anticipated how complex they would make the engine. But it was a proper learning experience, and I came out much stronger at bit manipulation. While well structured, it was a large surface area for bugs to fester. I tested my move generator against a standard Python one to see if it was correct; after running through >50 test cases, I called it good. Except there's still one test case, on the desktop of a computer I no longer have, that I could never get to pass. And I never even knew why.
This and a few other engine bugs made me skip the class-wide chess AI competition, one of my big regrets of college. I contemplated submitting a build that inverted the fitness function, producing an agent that optimizes for losing, but chickened out, afraid it would error on an illegal move. The decision wasn't pride; with 18 credit hours and a part-time internship, my straight-As were hanging on for dear life.
Code
The moving function shifts bits to simulate piece movement, masking edge files to prevent wraparound. Each piece type builds on this primitive.
Bitboard MoveEngine::moving(const Bitboard& board, const Direction& direction) {
const static Bitboard aFileInverse = 0xfefefefefefefefe;
const static Bitboard hFileInverse = 0x7f7f7f7f7f7f7f7f;
switch (direction) {
case north: return board << 8;
case south: return board >> 8;
case east: return (board << 1) & aFileInverse;
case west: return (board >> 1) & hFileInverse;
case northeast: return (board << 9) & aFileInverse;
case northwest: return (board << 7) & hFileInverse;
case southeast: return (board >> 7) & aFileInverse;
case southwest: return (board >> 9) & hFileInverse;
default: return Bitboard();
}
}
Pawns are the worst: different moves per color, double-moves from the starting rank, diagonal captures only when an enemy is present.
Bitboard MoveEngine::pawnMoves(const Bitboard& pawn, Bitboard self,
Bitboard enemy, const Color& selfColor) {
const Bitboard enemyOriginal = enemy;
self = ~self;
enemy = ~enemy;
static Bitboard secondRank = 0xff00;
static Bitboard seventhRank = 0xff000000000000;
if (selfColor == white) {
return (pawnNorthMovesWithBlockers(pawn, self & enemy)
| pawnNorthNorthMovesWithBlockers(pawn & secondRank,
self & enemy)
| (moving(pawn, northeast) & enemyOriginal)
| (moving(pawn, northwest) & enemyOriginal))
^ pawn;
} else {
return (pawnSouthMovesWithBlockers(pawn, self & enemy)
| pawnSouthSouthMovesWithBlockers(pawn & seventhRank,
self & enemy)
| (moving(pawn, southeast) & enemyOriginal)
| (moving(pawn, southwest) & enemyOriginal))
^ pawn;
}
}
And FEN (Forsyth-Edwards Notation), the standard for serializing a chess position as a string. Parsing it requires a regex that looks like someone smashed their keyboard. The apology is warranted.
std::string FenParser::getToken(const FenToken& token) {
// lol sorry
const char* regexString =
R"((([pPnNbBrRqQkK0-8]{1,8}/?){8})\s*(w|b)\s*)"
R"(([KQkq-]{0,4})\s*([a-hA-H0-8\-]{1,2})\s*)"
R"((\d+)\s*(\d+)*)";
std::regex regexExpression(regexString);
std::smatch match;
if (std::regex_search(fenString, match, regexExpression)) {
switch (token) {
case board: return match[1];
// off by one, regex error; don't ask
case colorAtPlay: return match[3];
case castling: return match[4];
case enPassant: return match[5];
case halfTurns: return match[6];
case fullTurns: return match[7];
default:
throw std::logic_error("Fen String is fucking broke");
}
} else {
throw std::logic_error("Fen String is fucking broke");
}
}
2. Puzzle Solvers
Four puzzle solvers built around different search algorithms. The standout is an A* implementation with custom heuristics that chews through state-space problems in milliseconds. Each puzzle forced careful thought about state representation and admissible heuristics. Watching the solver walk thousands of states to find an optimal path, faster than you can blink, was deeply satisfying.

The AI class had two programming projects: part two was the chess AI above. Part one, much simpler, was an AI engine that played a match-three clone. Reasonable project with sizeable scope, it was one of the projects that gave me real confidence as a programmer then. I remember comparing solutions with my friend Mark: his had a smaller code footprint, and he said mine was bloated. I remarked "your code is concise, but mine is poetic", then showed everyone my one-line move generator, like poetry.
Here's my desk setup at the time, editing this very code. I'm particularly proud of the new MacBook Pro; I bought it with my own internship money.

Code
Python generators let you build lazy sequences that compute on demand. Instead of materializing all moves upfront, the generator yields valid ones one at a time. Memory stays flat regardless of how many possible moves exist, because only the ones we actually touch get computed.
@staticmethod
def actions(state):
# This is ugly, but by abusing list comprehension, I get lazy evaluation.
# In turn, I actually do a linear search of the entire space, but only store
# the states that are legal. Thank you, generators.
row_max, column_max = MechanicalMatch.grid_size(state.grid)
return [] if state.swaps >= state.max_swaps else (
Action((row, column), direction)
for row in range(0, row_max)
for column in range(0, column_max)
for direction in [Direction.UP, Direction.LEFT]
if MechanicalMatch.swap_is_valid(state.grid, (row, column), direction)
)
3. Shape Packer
An evolutionary algorithm for 2D shape packing. Given irregular shapes and a rectangular board, find the placement that maximizes coverage. The genome encodes position and orientation per piece; mutation perturbs placements, recombination swaps configurations between parents. Fitness proportional selection, k-tournament (with and without replacement), truncation for survival.

This was my first real test of writing performant Python, and boy was it full of lessons. In short, my code was slooowww. Not hours but days slow. Every submission felt like a grueling experience (this is when I fell in love with tmux). But there's a certain fun to watching the convergence in such tight packings. Optimization problems are fun.
4. Linear Algebra Library
A templated C++ linear algebra library. Matrices, vectors, and decompositions (LU, QR, Cholesky). Heavy use of operator overloading so matrix math reads naturally. The final project ties it all together to solve linear systems with different numerical methods.
This assignment taught me that our library was sometimes open until 3am; I found a spot in the basement next to the vending machines. It wasn't a mental test, it was an endurance one. The course wasn't just about numerical modeling in code, but about writing good numerical modeling code: fully templated, high test coverage, with proper documentation. Two weeks to deliver 2k lines of code and 57 test cases, and a self-imposed single night to ship 1.6k lines of comments.
This problem called for solving steepest descent, using the various matrix and vector types we'd built:
- vector
- banded matrix
- diagonal matrix
- rectangular matrix
- symmetric matrix
Code
An iterative linear system solver that follows the gradient downhill until it converges. The initial guess is just the b vector because "why not."
template <typename T>
Vector<T> SteepestDescentSolver<T>::operator()(const SymmetricMatrix<T>& A,
const Vector<T> b) {
Vector<T> x = b; // initial guess is the b vector, cause why not
T alpha{};
unsigned i = 0;
Vector<T> residual = b - (A * x);
if (!isDiagonallyDominant(A)) {
throw NonDiagonallyDominantMatrixError();
}
while (norm(residual) > EPSILON && i++ < MAX_ITERATIONS) {
residual = b - (A * x);
alpha = (residual * residual) / ((A * residual) * residual);
x += alpha * residual;
}
return x;
}
5. CFG Tracer
Undergraduate research project that instruments C++ code to trace control flow at runtime. A control flow graph represents all possible paths through a program: nodes are basic blocks, edges are jumps. This tool parses source, identifies basic blocks, and generates execution traces. Boost handled the regex. The goal was to understand how programs actually execute versus how we think they execute.
Even by my senior year, most of the bigger projects were codebases I developed or co-developed; this was my first notable exception. With a fellow researcher, our job was to pick up an existing codebase from a graduate student and get it running. I thought it would be a walk in the park, but it needed some massaging. I particularly liked this assignment because it was a semester-long, tag-team effort to push someone else's work forward.
6. Splatoonio
Capstone project, a multiplayer mobile game in Flutter/Dart. Went from concept to deployed app with a team. Real-time synchronization, touch controls, cross-platform deployment. The kind of project where you learn that 80% of software engineering is communication.


I hope Nintendo doesn't read this. We had a team vote on the project name, and Splatoonio won. We can change it.
This was my most "complete" software project in college: server, client, docs, pitch, you name it. And it was hardly my doing; it was a team project, and our team was the dream team. No, literally: our team name was "Dream Team", after we realized we averaged two internships per person and all of us were in the same AI and numerical modeling classes (the most demanding combination at our college).
Our last presentation of the year was naturally a live demo, and we couldn't disappoint. We wanted to showcase the rendering across campus because our classroom definitely wasn't big enough, and we only had a production build with no demo wiring. So I showed up on game day in running gear, introduced our team, and proceeded to run across most of campus with the game running. I even timed my return to the last minute of the demo to make a statement. We were unanimously the top project of the class, affirmed by one of the most memorable rounds of applause I got as a student.
7. Space Invaders
Space Invaders running on an 8051 microcontroller. Assembly and C, pressed against tight memory constraints. Every byte mattered. Implementing smooth sprite movement and collision detection on hardware this limited teaches you what efficiency really means.

Cold November nights, coding with How I Met Your Mother playing in the background (see Code, below). This one assignment made me appreciate video game logic: writing a screen rendering engine with nothing but ncurses is a tall order. Keeping track of not just bounding boxes but changing state, animations, player input, drawing, all of it.
Despite the complexity and having never done anything like it, I got something working. It had several bugs centered around the aliens: they never progressed down the screen, they never shot, you could never hit the one in the last row. But it was satisfying nonetheless. One snag: this was supposed to run on hardware with 4k of memory, and my first compile for the target platform came in at 15k. Yikes. I stripped essentially every library and wrote my own. 8k.
This is the part where I'd love to say I found a clever hack to squeeze under the limit, but there's no perfect ending. I hit my wits' end, talked to the professor, and made up for the failure by implementing another feature.
I did get to present my game to the whole class. And my adventures made for some great memes, which I attached to my homework and presented to the class too.



Code
The game loop is a switch inside a do { } while (true), with the render living in the default: branch. Instead of the usual tick → input → update → draw, this loop reads a key and only redraws when the player didn't press anything. Hold a key and the screen stops updating. Space Invaders with a frame rate inversely proportional to how panicked you are.
do {
switch (getch()) {
case KEY_LEFT: /* ... move ... */ break;
case KEY_RIGHT: /* ... move ... */ break;
case ' ': /* ... shoot ... */ break;
case 'q': endwin(); exit(0); break;
default:
createHeader(&game, &header);
createShooter(game.gunner.center, &game, &footer);
createGameboard(&game, &gameboard,
stateOfAliens, stateOfShot);
draw(&game, &header, &gameboard, &footer);
break;
}
i++;
if (i % STATE_CHANGE_ALIENS == 0) {
stateOfAliens = stateOfAliens ? false : true;
}
stateOfShot = (i % 25 == 0);
} while (true);
The alien-selection logic is three branches of nested ternaries, picking which invader sprite to draw based on the row and animation frame.
if ((i / heightOfAverageAlien + 2) % 3 == 2) {
(*aliens)[i][j] = stateOne
? smallInvaderOne[i % heightOfAverageAlien][j % smallWidth]
: smallInvaderTwo[i % heightOfAverageAlien][j % smallWidth];
} else if ((i / heightOfAverageAlien + 2) % 3 == 0) {
(*aliens)[i][j] = stateOne ? mediumInvaderOne[...]
: mediumInvaderTwo[...];
} else {
(*aliens)[i][j] = stateOne ? largeInvaderOne[...]
: largeInvaderTwo[...];
}
The comment two lines above this block is the most honest sentence I ever wrote in a CS assignment:
// Then we mod by 3 because that's the number of aliens, and we
// compare to a number I put there because the returned numbers
// baffle me.
I had found an empirically-correct offset, and rather than figure out why, I shipped a comment saying so. Ten-years-later me is proud.
And then there's the HIMYM tax, paid in a split declaration so the comments land the punchline:
// It's gonna be legend..
void waitForIt(unsigned char seconds);
// ..ary! Legendary.
void waitForIt(unsigned char seconds) {
unsigned int retTime = (unsigned int)time(0) + (unsigned int)seconds;
while (time(0) < retTime);
}
8. Camelot
A team software engineering project with full documentation, UML diagrams, and Doxygen-generated API docs. Agile methodology, code reviews, collaborative development. The code itself is less interesting than the practice of building software with other people. An optional iOS chat client hooks into the server for real-time messaging. Swift, JSQMessagesViewController for the UI, SwiftSocket for TCP.

This class was pure joy. Not too difficult, not too easy. It was mostly just building useful software: an end-to-end chat interface. I got to make use of my iOS skills while the team built a fully-functioning message server. We put it together with flashy presentations.
Code
The server is exactly what you'd expect from a sophomore who just learned sockets: threaded TCP, a module-global SOCKET_LIST, and a broadcast loop that fans every message out to every connected client.
class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
my_socket = self.request
SOCKET_LIST.append(my_socket)
while True:
data = str(my_socket.recv(1024), 'ascii')
if not data:
if my_socket in SOCKET_LIST:
SOCKET_LIST.remove(my_socket)
return
try:
response = bytes(data, 'ascii')
except Exception:
response = bytes(json.dumps({
"error": "Something went wrong"
}), 'ascii')
for s in SOCKET_LIST:
s.sendall(response)
The iOS side is sophomore-level in a different way: no push, no WebSockets, no long polling. Just a Timer that reads the TCP socket every second, decodes the bytes into a JSQMessage, and hops back to the main queue to render. Real-time by brute force.
switch client.connect(timeout: 1) {
case .success:
self.timer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { _ in self.getNewMessage() }
// ...
}
func getNewMessage() {
DispatchQueue.global(qos: .background).async {
let data = self.client.read(1024 * 10)
guard data != nil else { return }
if let string = String(bytes: data!, encoding: .utf8) {
let message = JSQMessage(
senderId: User.reciever.rawValue,
displayName: getName(User.reciever),
text: string)
self.chatView.newMessage(message)
} else {
print("not a valid UTF-8 sequence")
}
DispatchQueue.main.async {
self.chatView.finishReceivingMessage()
}
}
}
9. CLC Tally
iOS app for tracking student headcounts at Missouri S&T's Computer Learning Center (CLC). Built to solve a real problem: tutors needed a quick way to log how many students they helped. Simple interface, local storage, export.

The irony is that I mostly wrote this in the CLC. I'd never found a tally app with this particular data format, and it was much easier to have my phone always-on taking count than periodically marking a notebook. I ended up being the only user, because I didn't have an App Store account. But this was an "enjoy the journey, not the destination" project: the beauty of developing and improving something weekly that you actually use.
Code
The entire data model is 20 lines. Each tap appends a Date to an array in UserDefaults. The counter is userLog.count. "Users this hour" is a filter. No database, no Core Data, no schema migrations. Sometimes the best software is the software that just works.
class Counter: CustomStringConvertible {
public var count: Int { return userLog.count }
private var userLog: [Date] {
get {
UserDefaults.standard
.object(forKey: "log") as? [Date] ?? []
}
set {
UserDefaults.standard.set(newValue, forKey: "log")
UserDefaults.standard.synchronize()
}
}
public func increment() { userLog.append(Date()) }
public func decrement() {
if !userLog.isEmpty { userLog.removeLast() }
}
public func usersThisHour() -> Int {
let hourOf = { (d: Date) in
Calendar.current.component(.hour, from: d)
}
let now = hourOf(Date())
return userLog.filter { hourOf($0) == now }.count
}
var description: String { return "\(count)" }
}
10. Grading Suite
Automated grading tools for CS 1570, the intro programming course. A style checker that enforces coding standards, a roster checker that validates submissions, a grader script that runs test cases, and a plagiarism checker.
Built out of necessity: grading hundreds of submissions by hand became unsustainable. I automated as much as possible so I could focus on the core concepts: algorithms, data structures, and programming paradigms.
The plagiarism checker never flagged anyone until assignment 8 out of 10; unironically, the hardest assignment of the year. Assignment 7 was a pair-programming project, assignment 8 was strict solo; and the same pair from assignment 7 decided to tackle assignment 8 together. I brought it to the instructor, and they asked my opinion on what we should do. Wanting to be fair, I proposed:
They split the work evenly, they should split the grade evenly: take each score and divide by two.
Output
Output is emitted as markdown so it drops straight into whatever report format the course coordinator wanted:
## student_submission.cpp
**80 Column Rule**
- Line 42: ` if (studentName == "John" && assignment.isComplete()`
**Tabs**
- Line 17: ` int counter = 0;`
**Non-Uppercase Constants**
- Line 8: `const int maxStudents = 60;`
- Line 9: `const double passingGrade = 70.0;`
**Header Guards Don't Match**
- Line 3: `#ifndef STUDENT_H`
**Missing Documentation (12 Functions, 4 Lines of Comments)**
Code
Every rule is a regex; most of them look like someone leaned on the keyboard. A sampler:
# 80-column rule: match anything, then demand a non-space in column 81.
# Trailing whitespace counts as a violation, which was the point.
column = r".{80}\S"
# Tabs: anchor to start of line, look for one tab character.
tabs = r"\A\t"
# Non-uppercase constants: "const <type> <name>;" where <name>
# has any lowercase letter. The nested char classes and optional
# assignment tail are what make it ugly.
constants = r"const\s+([a-zA-Z]|_)([a-zA-Z]|[0-9]|_)*\s+" \
r"(([a-zA-Z]|_)([a-zA-Z]|[0-9]|_)*|\s*,\s*)*" \
r"([a-zA-Z]|_)([A-Z]|[0-9]|_)*[a-z]+([A-Z]|[0-9]|_)*(\s*=\s*.+)*;"
# Switch without default: grab a whole switch block, then
# re-search inside for `default:`.
switch_block = r"switch\s*\(.*\)\s*\{[^\{;]+\}"
# Header-guard matcher: capture the #ifndef name and #define name, compare them.
header_guard = r"#ifndef\s*(.*)\n#define\s*(.*)"
# Header-comment detector: // or * or whitespace, then
# "File" / ".cpp" / ".hpp" / ".h". Paired with a separate
# /(Author|author)/ check for the author line.
header = r"(\/\/|\*|\s)+.*(File|file|.hpp|.cpp|.h)"
By The Numbers
| Metric | Value |
|---|---|
| Courses | 25 |
| Total Commits | 545 |
| Total Files | 1,826 |
| Lines of Code | 91,512 |
| Languages | 9 |
- TeX: 31,043 lines
- C/C++ Header: 28,235 lines
- C++: 17,246 lines
- SQL: 11,184 lines
- Python: 7,758 lines
- C: 2,667 lines
- Shell: 1,513 lines
- Assembly: 656 lines
- MATLAB: 337 lines
Commit Activity by Hour
+-------------------------------------------------------+
| Commit Activity by Hour |
+-------------------------------------------------------+
| Hour | Commits | Distribution |
+-------------------------------------------------------+
| 00:00 | 14 | ███████ |
| 01:00 | 4 | ██ |
| 02:00 | 11 | ██████ |
| 03:00 | 5 | ██ |
| 04:00 | 0 | |
| 05:00 | 0 | |
| 06:00 | 0 | |
| 07:00 | 4 | ██ |
| 08:00 | 7 | ███ |
| 09:00 | 32 | █████████████████ |
| 10:00 | 34 | ██████████████████ |
| 11:00 | 30 | ████████████████ |
| 12:00 | 28 | ███████████████ |
| 13:00 | 21 | ███████████ |
| 14:00 | 31 | ████████████████ |
| 15:00 | 24 | █████████████ |
| 16:00 | 36 | ███████████████████ |
| 17:00 | 20 | ██████████ |
| 18:00 | 25 | █████████████ |
| 19:00 | 40 | █████████████████████ |
| 20:00 | 41 | ██████████████████████ |
| 21:00 | 64 | █████████████████████████████████ |
| 22:00 | 44 | ████████████████████████ |
| 23:00 | 16 | ████████ |
+-------------------------------------------------------+
| Total commits: 545 |
+-------------------------------------------------------+
Peak activity: 9 PM with 64 commits.
Activity Heatmap
ACTIVITY HEATMAP
──────────────────────────────────────────────────────────────────────
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
2014 │ ░ │ │ │ │ │ │ │ │ │ │ │ │
├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
2015 │ │ │ ░ │ │ │ │ │ │ │ │ ░ │ ░ │
├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
2016 │ ░ │ ░ │ ██ │ ██ │ ██ │ │ ░ │ │ │ ░ │ ▒ │ ░ │
├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
2017 │ ░ │ ▒▒ │ ▒▒ │ ██ │ ▒ │ │ │ ▒ │ ▒▒ │ ▒ │ ░ │ ░ │
├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
2018 │ ██ │ ██ │ ██ │ ▒▒ │ │ │ │ │ │ │ │ │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
──────────────────────────────────────────────────────────────────────
░ 1-10 ▒ 11-30 █ 31-50 ██ 51+
Spring semesters: Jan-May
Fall semesters: Aug-Dec
Lines of Code by Year
LINES OF CODE BY YEAR
──────────────────────────────────────────────────────────
2014 2015 2016 2017 2018
│ │ │ │ │
│ │ │ │ ████████
│ │ │ │ ████████
│ │ │ ████████ ████████
│ │ │ ████████ ████████
│ │ ████████ ████████ ████████
│ │ ████████ ████████ ████████
│ ████████ ████████ ████████ ████████
████████ ████████ ████████ ████████ ████████
────────────────────────────────────────────────────────
~2k ~10k ~20k ~25k ~35k
For the notes and problem sets themselves, see Academia Notes.
To my peers and professors at Missouri S&T: thank you. You made those four years special, and I remember them fondly. The late nights coding, the early morning classes where I'd take my first sips of coffee, the many fruitful discussions in between. All of it.
Sorting Algorithms
sort<Algorithm>(from: bubble, to: Tim)
When I was a Teacher Assistant (TA) in Intro To Computer Science lab, fellow TA
Ian and I were showing off our programming prowess. I thought I had it in the bag: I had solved a competitive programming problem in compile-time (C++ templates are Turing-complete!) and a Space Invaders clone for a class.
But Ian was more clever than I, and showed me something that fundamentally changed how I saw a core-component of programming: a terminal-based (ncurses) sorting algorithm visualizers.
It was the first time I had ever seen these algorithms graphed like this — ever! And, yes, I blame my Algorithm instructor. I finally could see all the hypothetical sorting in a real-life application.
With the power of LLMs in hand, and a website as my canvas, I wanted to see if I could recreate this. Kudos to you, Ian.
Sorting algorithms form the backbone of computer science, serving as fundamental building blocks for countless applications from database management to search engines. This comprehensive guide examines the 25 most important sorting algorithms, organized by type, with detailed analysis of their performance, implementation, and practical applications.
1. Basic Comparison-Based Algorithms
These fundamental algorithms serve as the foundation for understanding sorting concepts, though they generally have O(n²) time complexity.
1.1 Bubble Sort
Complexity Analysis:
- Best Case: O(n) / Ω(n) - when array is already sorted
- Average Case: O(n²) / Θ(n²)
- Worst Case: O(n²)
- Space: O(1)
Properties: Stable, In-place, Adaptive
def bubble_sort(arr):
"""
Bubble Sort with optimization
Time: O(n²) average/worst, O(n) best
Space: O(1)
"""
n = len(arr)
for i in range(n):
swapped = False
# Last i elements are already sorted
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
# If no swapping occurred, array is sorted
if not swapped:
break
return arr
When to Use:
- Small datasets (< 50 elements)
- Educational purposes - excellent for teaching
- Nearly sorted data
- Memory-constrained environments
Step-by-Step Example:
Array: [64, 34, 25, 12, 22, 11, 90]
Pass 1: [34, 25, 12, 22, 11, 64, 90] - Largest element "bubbles" to end
Pass 2: [25, 12, 22, 11, 34, 64, 90]
... continues until sorted
History: First described by Edward Harry Friend in 1956. The name "bubble sort" was coined by Kenneth E. Iverson due to how smaller elements "bubble" to the top.
Notable Trivia: Donald Knuth famously stated "bubble sort seems to have nothing to recommend it, except a catchy name." Despite criticism, it remains the most taught sorting algorithm due to its simplicity.
1.2 Selection Sort
Complexity Analysis:
- Best/Average/Worst Case: O(n²) - always makes same comparisons
- Space: O(1)
Properties: Unstable, In-place, Not adaptive
def selection_sort(arr):
"""
Selection Sort implementation
Time: O(n²) for all cases
Space: O(1)
"""
n = len(arr)
for i in range(n):
# Find minimum element in remaining unsorted array
min_idx = i
for j in range(i + 1, n):
if arr[j] < arr[min_idx]:
min_idx = j
# Swap the found minimum element
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
When to Use:
- When memory write operations are expensive (e.g., flash memory)
- Small datasets where simplicity matters
- When the number of swaps needs to be minimized
Key Advantage: Performs only O(n) swaps compared to O(n²) for bubble sort.
History: Has ancient origins in manual sorting processes. Formalized in the 1950s as one of the fundamental sorting methods.
1.3 Insertion Sort
Complexity Analysis:
- Best Case: O(n) - already sorted
- Average/Worst Case: O(n²)
- Space: O(1)
Properties: Stable, In-place, Adaptive, Online
def insertion_sort(arr):
"""
Insertion Sort implementation
Time: O(n²) average/worst, O(n) best
Space: O(1)
"""
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
When to Use:
- Small datasets (typically < 50 elements)
- Nearly sorted data - performs in O(n) time
- Online algorithms - when data arrives sequentially
- As a subroutine in quicksort and mergesort for small subarrays
Notable Use: Used in Timsort (Python's built-in sort) for small runs. Often faster than O(n log n) algorithms for arrays with fewer than 10-20 elements.
1.4 Shell Sort
Complexity Analysis:
- Best Case: O(n log n)
- Average Case: O(n^1.25) to O(n^1.5) depending on gap sequence
- Worst Case: O(n²) for Shell's original sequence
- Space: O(1)
Properties: Unstable, In-place, Adaptive
def shell_sort(arr):
"""
Shell Sort using Shell's original sequence
Time: O(n²) worst case, O(n log n) average
Space: O(1)
"""
n = len(arr)
gap = n // 2
while gap > 0:
# Perform gapped insertion sort
for i in range(gap, n):
temp = arr[i]
j = i
while j >= gap and arr[j - gap] > temp:
arr[j] = arr[j - gap]
j -= gap
arr[j] = temp
gap //= 2
return arr
When to Use:
- Medium-sized datasets (100-5000 elements)
- When recursion should be avoided
- Embedded systems - simple and efficient
History: Invented by Donald L. Shell in 1959, it was one of the first algorithms to break the O(n²) barrier.
1.5 Cocktail Shaker Sort (Bidirectional Bubble Sort)
Complexity Analysis:
- Best Case: O(n)
- Average/Worst Case: O(n²)
- Space: O(1)
Properties: Stable, In-place, Adaptive, Bidirectional
def cocktail_shaker_sort(arr):
"""
Cocktail Shaker Sort (Bidirectional Bubble Sort)
Time: O(n²) average/worst, O(n) best
Space: O(1)
"""
n = len(arr)
start = 0
end = n - 1
while start < end:
swapped = False
# Forward pass
for i in range(start, end):
if arr[i] > arr[i + 1]:
arr[i], arr[i + 1] = arr[i + 1], arr[i]
swapped = True
if not swapped:
break
end -= 1
swapped = False
# Backward pass
for i in range(end, start, -1):
if arr[i] < arr[i - 1]:
arr[i], arr[i - 1] = arr[i - 1], arr[i]
swapped = True
if not swapped:
break
start += 1
return arr
Advantage: Better than bubble sort at moving small elements (turtles) to the beginning.
2. Efficient Comparison-Based Algorithms
These algorithms achieve O(n log n) average performance and form the backbone of many practical sorting implementations.
2.1 Quick Sort
Complexity Analysis:
- Best/Average Case: O(n log n)
- Worst Case: O(n²) - when pivot is always minimum/maximum
- Space: O(log n) - recursion stack
Properties: Unstable, In-place, Not adaptive
def quicksort(arr, low=0, high=None):
"""
Quicksort with Hoare partition scheme
Time: O(n log n) average, O(n²) worst
Space: O(log n)
"""
if high is None:
high = len(arr) - 1
if low < high:
pivot_idx = partition(arr, low, high)
quicksort(arr, low, pivot_idx)
quicksort(arr, pivot_idx + 1, high)
return arr
def partition(arr, low, high):
"""Hoare partition scheme"""
pivot = arr[low]
i = low - 1
j = high + 1
while True:
i += 1
while arr[i] < pivot:
i += 1
j -= 1
while arr[j] > pivot:
j -= 1
if i >= j:
return j
arr[i], arr[j] = arr[j], arr[i]
Why It's Preferred Despite O(n²) Worst Case:
- Excellent average-case performance with good constant factors
- Cache-friendly sequential access patterns
- In-place sorting
- Modern implementations use introsort to guarantee O(n log n)
History: Invented by Tony Hoare in 1959 while working on machine translation at Moscow State University.
2.2 Merge Sort
Complexity Analysis:
- All Cases: O(n log n) - guaranteed performance
- Space: O(n) - requires additional space for merging
Properties: Stable, Not in-place, Not adaptive
def merge_sort(arr):
"""
Merge Sort implementation
Time: O(n log n) guaranteed
Space: O(n)
"""
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
"""Merge two sorted arrays"""
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
When to Use:
- When stability is required
- External sorting (large datasets that don't fit in memory)
- Linked lists (efficient with O(1) extra space)
- Parallel processing
History: Invented by John von Neumann in 1945, with detailed analysis published in 1948.
2.3 Heap Sort
Complexity Analysis:
- All Cases: O(n log n) - guaranteed performance
- Space: O(1) - true in-place sorting
Properties: Unstable, In-place, Not adaptive
def heap_sort(arr):
"""
Heap Sort implementation
Time: O(n log n) guaranteed
Space: O(1)
"""
n = len(arr)
# Build max heap
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
# Extract elements from heap
for i in range(n - 1, 0, -1):
arr[0], arr[i] = arr[i], arr[0]
heapify(arr, i, 0)
return arr
def heapify(arr, n, i):
"""Maintain heap property"""
largest = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and arr[left] > arr[largest]:
largest = left
if right < n and arr[right] > arr[largest]:
largest = right
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)
When to Use:
- Memory-constrained environments
- Real-time systems (guaranteed performance)
- Systems concerned with malicious input
History: Invented by J. W. J. Williams in 1964, with in-place version by Robert Floyd.
2.4 Binary Tree Sort
Complexity Analysis:
- Best/Average Case: O(n log n) - with balanced tree
- Worst Case: O(n²) - with unbalanced tree
- Space: O(n) - for tree structure
Properties: Can be stable, Not in-place
When to Use:
- Educational purposes
- When tree structure is needed for other operations
- Online sorting
Note: Self-balancing trees (AVL, Red-Black) guarantee O(n log n) performance.
2.5 Smooth Sort
Complexity Analysis:
- Best Case: O(n) - for sorted data
- Average/Worst Case: O(n log n)
- Space: O(1)
Properties: Unstable, In-place, Adaptive
History: Invented by Edsger W. Dijkstra in 1981 as an improvement over heapsort for partially sorted data.
Notable Use: Used in musl C library's qsort() implementation.
3. Non-Comparison Based Algorithms
These algorithms achieve linear O(n) time complexity by exploiting specific properties of the data rather than comparing elements.
3.1 Counting Sort
Complexity Analysis:
- All Cases: O(n + k) where k is the range of values
- Space: O(n + k)
Properties: Stable, Not in-place
def counting_sort(arr):
"""
Counting Sort for non-negative integers
Time: O(n + k)
Space: O(n + k)
"""
if not arr:
return arr
max_val = max(arr)
count = [0] * (max_val + 1)
# Count occurrences
for num in arr:
count[num] += 1
# Calculate cumulative count
for i in range(1, len(count)):
count[i] += count[i - 1]
# Build output array
output = [0] * len(arr)
for i in range(len(arr) - 1, -1, -1):
output[count[arr[i]] - 1] = arr[i]
count[arr[i]] -= 1
return output
When to Use:
- Sorting integers in a small range
- As a subroutine in radix sort
- When k is O(n) or smaller
History: Invented by Harold H. Seward in 1954 at MIT.
3.2 Radix Sort
Complexity Analysis:
- All Cases: O(d × (n + k)) where d is number of digits
- Space: O(n + k)
Properties:
- LSD (Least Significant Digit): Stable
- MSD (Most Significant Digit): Can be stable
def radix_sort_lsd(arr):
"""
LSD Radix Sort implementation
Time: O(d × (n + k))
Space: O(n + k)
"""
if not arr:
return arr
max_val = max(arr)
exp = 1
while max_val // exp > 0:
counting_sort_for_radix(arr, exp)
exp *= 10
return arr
def counting_sort_for_radix(arr, exp):
n = len(arr)
output = [0] * n
count = [0] * 10
for i in range(n):
index = arr[i] // exp
count[index % 10] += 1
for i in range(1, 10):
count[i] += count[i - 1]
i = n - 1
while i >= 0:
index = arr[i] // exp
output[count[index % 10] - 1] = arr[i]
count[index % 10] -= 1
i -= 1
for i in range(n):
arr[i] = output[i]
When to Use:
- Sorting integers with many digits
- String sorting (MSD variant)
- When d is small compared to log n
History: Dates back to 1887 with Herman Hollerith's tabulating machines.
3.3 Bucket Sort
Complexity Analysis:
- Best/Average Case: O(n + k) for uniform distribution
- Worst Case: O(n²) when all elements fall into one bucket
- Space: O(n + k)
Properties: Stable (if sub-sorting is stable), Not in-place
def bucket_sort(arr):
"""
Bucket Sort for floating-point number
Time: O(n + k) average
Space: O(n + k)
"""
if not arr:
return arr
min_val, max_val = min(arr), max(arr)
bucket_count = len(arr)
buckets = [[] for _ in range(bucket_count)]
# Distribute elements into buckets
for num in arr:
if max_val == min_val:
index = 0
else:
index = int((num - min_val) / (max_val - min_val) * (bucket_count - 1))
buckets[index].append(num)
# Sort individual buckets
result = []
for bucket in buckets:
if bucket:
bucket.sort() # Can use insertion sort
result.extend(bucket)
return result
When to Use:
- Uniformly distributed floating-point numbers
- Large datasets with known range
- When memory is not a constraint
3.4 Pigeonhole Sort
Complexity Analysis:
- All Cases: O(n + range) where range = max - min + 1
- Space: O(range)
Properties: Stable, Not in-place
When to Use:
- Small range of integer values
- When range is comparable to n
- Simple counting applications
History: Based on the pigeonhole principle, formally described by A.J. Lotka (1926).
3.5 Flash Sort
Complexity Analysis:
- Best/Average Case: O(n) for uniform distribution
- Worst Case: O(n²)
- Space: O(m) where m is number of classes
Properties: Unstable, In-place (major advantage)
When to Use:
- Large uniformly distributed datasets
- When memory is limited
- When O(n) average performance is critical
History: Invented by Karl-Dietrich Neubert in 1998 as an efficient in-place implementation of bucket sort.
4. Modern Hybrid Algorithms
These algorithms represent the state-of-the-art in practical sorting, combining multiple techniques for superior performance.
4.1 Timsort
Complexity Analysis:
- Best Case: O(n) - already sorted
- Average/Worst Case: O(n log n)
- Space: O(n)
Properties: Stable, Not in-place
Key Innovations:
- Run Detection: Identifies naturally occurring sorted subsequences
- Minimum Run Size: Calculates optimal minrun (32-64 elements)
- Galloping Mode: Switches to exponential search when one run consistently "wins"
Where It's Used:
- Python's default sort since version 2.3
- Java for sorting objects (Java 7+)
- Android, V8, Swift, Rust
History: Created in 2002 by Tim Peters for Python. A critical bug was discovered and fixed in 2015 through formal verification.
4.2 Introsort (Introspective Sort)
Complexity Analysis:
- All Cases: O(n log n) - guaranteed by heapsort fallback
- Space: O(log n)
Properties: Unstable, In-place
Techniques Combined:
- Quicksort for main sorting
- Heapsort when recursion depth exceeds 2×log₂(n)
- Insertion sort for small subarrays (< 16 elements)
Where It's Used:
- C++ STL's std::sort() in GCC and LLVM
- Microsoft .NET Framework 4.5+
History: Created by David Musser in 1997 to provide guaranteed O(n log n) performance while maintaining quicksort's average-case speed.
4.3 Block Sort (WikiSort)
Complexity Analysis:
- Best Case: O(n)
- Average/Worst Case: O(n log n)
- Space: O(1) - constant space!
Properties: Stable, In-place
Key Innovation: Achieves stable merge sort performance with O(1) space by using internal buffering.
When to Use: When O(1) space complexity and stability are both required.
4.4 Pattern-defeating Quicksort (pdqsort)
Complexity Analysis:
- Best Case: O(n) for specific patterns
- Average/Worst Case: O(n log n)
- Space: O(log n)
Properties: Unstable, In-place
Key Innovations:
- Pattern detection and optimization
- Branchless partitioning
- Adaptive strategy based on input characteristics
Where It's Used:
- Rust's default unstable sort
- C++ Boost libraries
History: Created by Orson Peters in 2016 to improve upon introsort with better pattern handling.
4.5 Dual-Pivot Quicksort
Complexity Analysis:
- Best Case: O(n) when all elements equal
- Average Case: O(n log n) - 5% fewer comparisons than single-pivot
- Worst Case: O(n²) - still possible but less likely
- Space: O(log n)
Properties: Unstable, In-place
Key Innovation: Uses two pivots to partition array into three parts, reducing comparisons.
Where It's Used: Java's default algorithm for primitive arrays since Java 7.
History: Created by Vladimir Yaroslavskiy in 2009, adopted by Java in 2011.
5. Specialized and Educational Algorithms
These algorithms serve specific purposes or demonstrate important concepts in computer science education.
5.1 Comb Sort
Complexity Analysis:
- Best Case: O(n log n)
- Average Case: O(n²/2^p) where p is number of increments
- Worst Case: O(n²)
- Space: O(1)
Properties: Unstable, In-place
Key Feature: Improves upon bubble sort using variable gap with shrink factor of 1.3.
History: Developed by Włodzimierz Dobosiewicz in 1980 to address bubble sort's inefficiency.
5.2 Gnome Sort (Stupid Sort)
Complexity Analysis:
- Best Case: O(n)
- Average/Worst Case: O(n²)
- Space: O(1)
Properties: Stable, In-place, Adaptive
Unique Feature: Uses only a single while loop - inspired by garden gnomes sorting flower pots.
5.3 Cycle Sort
Complexity Analysis:
- All Cases: O(n²)
- Space: O(1)
Properties: Unstable, In-place
Key Feature: Minimizes memory writes - each element is written at most once to its correct position.
When to Use: When memory write operations are expensive (EEPROM, Flash memory).
5.4 Pancake Sort
Complexity Analysis:
- Best Case: O(n)
- Average/Worst Case: O(n²)
- Space: O(1)
Properties: Unstable, In-place
Unique Constraint: Only allowed operation is "flip" (reverse prefix).
Historical Note: Bill Gates' only published academic paper was on this problem (1979), providing a (5n+5)/3 upper bound algorithm.
5.5 Bogo Sort
Complexity Analysis:
- Best Case: O(n) - already sorted
- Average Case: O(n·n!) - expected permutations
- Worst Case: O(∞) - theoretically unbounded
- Space: O(1)
Properties: Unstable, In-place
Educational Value:
- Demonstrates worst-case analysis
- Teaches randomized algorithms
- Shows importance of algorithm selection
import random
def bogo_sort(arr):
"""The worst sorting algorithm ever conceived"""
def is_sorted(arr):
return all(arr[i] <= arr[i+1] for i in range(len(arr)-1))
while not is_sorted(arr):
random.shuffle(arr)
return arr
Trivia: "Quantum Bogo Sort" hypothetically destroys universes where array isn't sorted, leaving only sorted universes.
Summary and Recommendations
Performance Comparison Table
| Algorithm | Best Case | Average Case | Worst Case | Space | Stable | In-Place |
|---|---|---|---|---|---|---|
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) | Yes | Yes |
| Selection Sort | O(n²) | O(n²) | O(n²) | O(1) | No | Yes |
| Insertion Sort | O(n) | O(n²) | O(n²) | O(1) | Yes | Yes |
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) | No | Yes |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes | No |
| Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) | No | Yes |
| Counting Sort | O(n+k) | O(n+k) | O(n+k) | O(n+k) | Yes | No |
| Radix Sort | O(d(n+k)) | O(d(n+k)) | O(d(n+k)) | O(n+k) | Yes | No |
| Timsort | O(n) | O(n log n) | O(n log n) | O(n) | Yes | No |
When to Use Which Algorithm
For Small Datasets (< 50 elements):
- Insertion Sort - simple and efficient
- Selection Sort - when minimizing swaps matters
For General Purpose:
- Timsort (Python) or Introsort (C++) - best overall performance
- Quick Sort with good pivot selection - excellent average case
For Guaranteed Performance:
- Merge Sort - stable and predictable
- Heap Sort - when O(1) space is required
For Special Data Types:
- Counting Sort - small integer ranges
- Radix Sort - large integers or strings
- Bucket Sort - uniformly distributed floats
For Educational Purposes:
- Start with Bubble Sort for simplicity
- Progress to Quick Sort and Merge Sort
- Use Bogo Sort to demonstrate algorithm analysis
Key Takeaways
- No single best algorithm - choice depends on data characteristics, constraints, and requirements
- Modern algorithms are hybrids - combining techniques yields superior performance
- Stability matters for sorting complex objects where maintaining relative order is important
- Space-time tradeoffs are crucial - some algorithms trade memory for speed
- Real-world performance often differs from theoretical complexity due to cache effects, data patterns, and implementation details
Understanding these 25 algorithms provides a comprehensive foundation for tackling sorting problems in any context, from embedded systems to large-scale data processing.
Wordleconomics
When in doubt, SLATE it out.
Over the past year, I've become quite invested in Wordle. Actually, Taylor and her family started playing first, and I quickly joined in. Soon enough, I even got my mom hooked on it!
If you're not familiar, Wordle is an engaging daily puzzle where you have six tries to guess a secret five-letter word, guided by helpful color-coded hints.
Throughout the year, I consistently chose the same starter: SLATE.
It felt safe and reliable—I always knew my next steps based on it. However, Taylor experimented with different starters and consistently crushed it.
This made me curious: what are actually the best starting words? After some digging, familiar contenders: CRANE, SLATE, TRACE, CRATE, CARET. I've compiled a comprehensive list below for (both of our) future reference.
S L A T E
T R A C E
C R A T E
C A R E T
Identifying the Optimal Starters
Rather than relying solely on intuition or AI, I wanted to see if a straightforward, "old-school" program could crack this puzzle. First, we needed a suitable dictionary of words. Unix and macOS provide the standard words library, but this includes obscure entries that Wordle may not recognize.
Next, we required an effective scoring method. A straightforward approach is to calculate letter frequency across all words—the more frequently a letter appears, the higher its score.


Quickly, some letters emerged as particularly common:
- Most frequent:
S,E,A - Moderately frequent:
O,R,I
Interestingly, four of these letters are vowels: E, A, O, I.
Positional frequency matters too—letters are scored higher if they're common in specific positions within words.

Notably:
- The
{S, 4}combination (a trailing "S") dominates, suggesting many plurals.{S, 4}: ____S
- Other frequent positional letters:
{A, 1}: _A___{E, 1}: _E___{E, 3}: ___E_{I, 1}: _I___{O, 1}: _O___{S, 0}: S____{T, 0}: T____{U, 1}: _U___{Y, 4}: ____Y
Finally, by combining aggregate letter frequency and positional data, a hybrid scoring system emerged. This method offers a more balanced and nuanced approach, producing unique top words: AEROS, SOARE, REAIS, AROSE, and RAISE.
S O A R E
R E A I S
A R O S E
R A I S E
Even when you feed my script the same 2,309‑word official Wordle answer list that WordleBot uses, our rankings still diverge because of how we each value information: my hybrid metric simply adds up how frequently each letter—and, to a lesser degree (10 % blend), each letter‑in‑position—appears across all answers, then zeroes out any word with duplicate letters on turn one, so high‑coverage vowel‑heavy options like AEROS and SOARE dominate; WordleBot, by contrast, runs a full entropy simulation for every guess and keeps duplicate letters if they shrink the remaining solution space, which is why consonant‑balanced staples like CRANE and SLATE top its chart. In short, we share the same dictionary; the gulf comes from aggregate‑frequency math versus entropy‑driven feedback simulation, plus my harsh repeat‑letter penalty and modest positional weight.
Parting Thoughts
Choosing the ideal Wordle starter is about balancing letter frequency and positional insights. Popular starters like CRANE and SLATE remain consistently strong choices due to their strategic letter placement and high-frequency letters. Meanwhile, hybrid scoring systems, which blend multiple metrics, offer compelling alternatives like SOARE and AEROS, maximizing initial guess effectiveness.
Whether sticking with tried-and-true favorites or exploring data-driven options, the real fun of Wordle lies in its daily puzzle-solving and the friends and family your spend doing it with.
My Words
The words generated by my program rank first by a hybrid metric (10% blend), then positional, then aggregate letter frequencies. The metrics are calculated by a sum of the letter’s value, with the value equaling the number of letter occurrences / total words. Positional does the same over the individual positions.
| Word | Hybrid # | Hybrid | Position # | Position | Aggregate # | Aggregate |
|---|---|---|---|---|---|---|
| AEROS | 1 | 0.98841 | 1 | 1.89498 | ||
| SOARE | 2 | 0.98324 | 3 | 1.89498 | ||
| REAIS | 3 | 0.97544 | 7 | 1.86772 | ||
| AROSE | 4 | 0.97213 | 2 | 1.89498 | ||
| RAISE | 5 | 0.96532 | 6 | 1.86772 | ||
| SERIA | 6 | 0.96462 | 9 | 1.86772 | ||
| SERAI | 7 | 0.96366 | 8 | 1.86772 | ||
| LARES | 8 | 0.96129 | 19 | 0.77624 | 17 | 1.82195 |
| RALES | 9 | 0.96041 | 20 | 1.82195 | ||
| TARES | 10 | 0.95962 | 6 | 0.79354 | ||
| ARISE | 11 | 0.95871 | 5 | 1.86772 | ||
| ALOES | 12 | 0.95772 | 10 | 1.83063 | ||
| AESIR | 13 | 0.95761 | 4 | 1.86772 | ||
| RATES | 14 | 0.95669 | ||||
| TOEAS | 15 | 0.95635 | 13 | 1.82518 | ||
| ARLES | 16 | 0.95380 | 14 | 1.82195 | ||
| RANES | 17 | 0.95379 | 23 | 0.77186 | 41 | 1.80754 |
| NARES | 18 | 0.95341 | 29 | 0.76567 | 40 | 1.80754 |
| EARLS | 19 | 0.95316 | 15 | 1.82195 | ||
| LAERS | 20 | 0.95265 | 16 | 1.82195 | ||
| REALS | 21 | 0.95174 | 21 | 1.82195 | ||
| TERAS | 22 | 0.95116 | 37 | 1.81649 | ||
| LEARS | 23 | 0.95079 | 19 | 1.82195 | ||
| TEARS | 24 | 0.94912 | 35 | 1.81649 | ||
| AEONS | 25 | 0.94816 | ||||
| PARES | 1 | 0.81023 | ||||
| BARES | 2 | 0.80168 | ||||
| CARES | 3 | 0.79946 | ||||
| MARES | 4 | 0.79818 | 90 | 1.74042 | ||
| PANES | 5 | 0.79441 | ||||
| PORES | 7 | 0.79219 | ||||
| BANES | 8 | 0.78586 | ||||
| PALES | 9 | 0.78458 | ||||
| BORES | 10 | 0.78364 | ||||
| CANES | 11 | 0.78364 | ||||
| DARES | 12 | 0.78364 | 60 | 1.75988 | ||
| MANES | 13 | 0.78236 | ||||
| CORES | 14 | 0.78142 | ||||
| GARES | 15 | 0.78028 | ||||
| MORES | 16 | 0.78014 | ||||
| FARES | 17 | 0.77765 | ||||
| PONES | 18 | 0.77637 | ||||
| BALES | 20 | 0.77604 | ||||
| TORES | 21 | 0.77550 | ||||
| MALES | 22 | 0.77253 | ||||
| HARES | 24 | 0.76998 | ||||
| PATES | 25 | 0.76856 | ||||
| ALOSE | 11 | 1.83063 | ||||
| STOAE | 12 | 1.82518 | ||||
| LASER | 18 | 1.82195 | ||||
| SERAL | 22 | 1.82195 | ||||
| ARETS | 23 | 1.81649 | ||||
| ASTER | 24 | 1.81649 | ||||
| EARST | 25 | 1.81649 |
Top Words
Top recommended words based on expert analysis. Check mark ✓ applies to words that have been Wordle words before.
| # | Word | Why it ranks |
|---|---|---|
| Tier A | ||
| 1 | CRANE (✓) | Highest WordleBot skill 99/99 |
| 2 | SLATE (✓) | ditto 99/99 – classic S‑start, E‑end |
| 3 | TRACE (✓) | 99 — covers C/R/T trio |
| 4 | CRATE (✓) | anagram of TRACE |
| 5 | CARET | 99, never an answer yet |
| 6 | CARTE | same 99 rating |
| 7 | SLANT | WordleBot 99, "hard‑mode friendly" |
| 8 | PLATE (✓) | newest 98/99 pick after CRANE |
| 9 | STARE (✓) | long‑time player favorite, 97 |
| 10 | SAINT (✓) | 97, nice S‑start / NT ending |
| 11 | LEAST | WordleBot 97, duplicate‑safe |
| 12 | STALE (✓) | 97, frequent solution ending |
| 13 | TASER | 97, yet unused answer |
| 14 | PARSE | 97, R/S/E trio |
| 15 | SNARE (✓) | 96, hits S/A/R/E combo |
| 16 | TRADE (✓) | 96, D tests mid‑freq cons |
| 17 | PLANE | 96, vowel‑balanced |
| 18 | SANER | 96, "anser" pattern |
| 19 | PLACE (✓) | 96, common C/E ending |
| 20 | SLICE (✓) | 96, tests C/I vowel |
| Tier B | ||
| 21 | TRICE (✓) | 98 WordleBot |
| 22 | DEALT | top hard‑mode 99 |
| 23 | LANCE | 98 alt to SLANT |
| 24 | TRIPE | 95 (hard‑mode) |
| 25 | SHALT | 94 skill; avoids ‑S plural issue |
| 26 | TAILS | 94; S‑ending test |
| 27 | PETAL | 93; alternate to PLATE |
| 28 | ROAST | high 97 in WordsRated pair study |
| 29 | RAISE | Tyler Glaiel's top "answer‑valid" pick |
| 30 | SAUCY | Hi‑score 'future‑answer' word, Feb 2024 |
| 31 | SAUCE | runner‑up to SAUCY |
| 32 | SOAPY | high vowel‑con repeat test |
| 33 | SEIZE | Z‑check without Q/J |
| 34 | CEASE | double‑E confirmation |
| 35 | BRINY | tests Y‑ending |
| 36 | CRIER | common bigram ‑ER |
| 37 | SALLY | WordleBot 92 but strong Y test |
| 38 | SADLY | similar Y test, avoids E |
| 39 | SOOTY | vowel+Y, covers double‑O |
| 40 | BRINE | #4 on WordsRated score list |
| Tier C | ||
| 41 | SALET | MIT "optimal" (avg 3.42 guesses) |
| 42 | SOARE | Glaiel/Fan #1 eliminator |
| 43 | SAINE | Hackernoon highest exact‑green probability |
| 44 | SLANE | MIT list #6 |
| 45 | SAREE | Bertrand Fan entropy #2 |
| 46 | SEARE | entropy #3 |
| 47 | SAICE | WordPlay top‑10 |
| 48 | REAST | MIT #2 overall |
| 49 | TRAPE | MIT #5 |
| 50 | PRATE | MIT #7 |
| 51 | TEALS | MIT tied #9 |
| 52 | TRAIN | MIT tied #9 – introduces N |
| 53 | RANCE | 3Blue1Brown "max‑4‑guess coverage" |
| 54 | RATED | same study – strong D check |
| 55 | RANTS | alt w/ S‑end |
| 56 | RONTE | high entropy variant |
| 57 | RAILE | WordPlay top‑10 (rare but allowed) |
| 58 | TRICE (✓) | already in Tier A — demonstrates overlap |
| 59 | LATER | Top TikTok/Reddit frequency‑ranked list pick |
| 60 | AROSE | Excel/YouTube statistical pick |
| Tier D | ||
| 61 | IRATE | linguist‑approved vowel+RT |
| 62 | ALTER | common ALT‑ pattern |
| 63 | ADIEU | 4‑vowel classic |
| 64 | AUDIO | 4‑vowel alt, tests U |
| 65 | ARISE | vowel/R/S spread |
| 66 | ROATE | best pure eliminator, not an answer |
| 67 | SAUTE | five high‑freq letters+U |
| 68 | POISE | balances mid vowels/cons |
| 69 | TEASE | vowel‑dense w/ common T/S/E |
| 70 | CAUSE | WordRated score #7 |
| 71 | SHINE | fills H/N combo hole |
| 72 | NOTES | Wired letter‑freq starter |
| 73 | RESIN | ≈ NOTES but R swap |
| 74 | TARES | Wired / Real‑Stats top 5 |
| 75 | SENOR | same Wired set |
| 76 | ROAST | already Tier B — popular SmartLocal |
| 77 | TALES | Prof. Smyth simulator #1 |
| 78 | CONES | simulator #2 |
| 79 | HATES | 97 % success in 3‑word strat |
| 80 | POUTY | vowel‑light follow‑up favorite |
| Tier E | ||
| 81 | CLINT | best second word for SOARE combo |
| 82 | ROUND | part of 3‑word meta |
| 83 | CLIMB | third word in same set |
| 84 | SALLY | WordRated list (tests double L/Y) |
| 85 | SADLY | Y‑ending + D check |
| 86 | SOOTY | digs into double‑O / Y |
| 87 | BRINY | rare B/Y test |
| 88 | SEIZE | Z‑probe after vowels |
| 89 | DEALT | already Tier B — hard‑mode default |
| 90 | LANCE | already Tier B |
| 91 | OUIJA | meme‑ish 4‑vowel+J probe |
| 92 | ABOUT | vowel‑heavy common pick |
| 93 | CANOE | community vowel test |
| 94 | STORE | SmartLocal "other good word" |
| 95 | COALS | best two‑word pair (COALS+NITER) |
| 96 | NITER | complement to COALS |
| 97 | SUITE | Tom's Guide demo of today's solve |
| 98 | PIQUE | tests rare Q/I pair |
| 99 | TARSE | Reddit pick beats SALET in 2024 tweaks |
| 100 | TILER | frequency‑based R‑ending probe (Real‑Stats) |
Source Code
Want the source code? Find it here.
Evolutionary Algorithms Endgame Dynamics
Adaptive Restarts and Termination Conditions
Update: Previously, a system was introduced for detecting if an individual was stuck at a local optimum. After extensive testing, this system was shown to be fragile. This post has been updated to showcase a more robust system.
Previously our Evolutionary Algorithms had it pretty easy: there would be either one local optimum (like our Secret Message problem instance) or multiple valid local optima (like the 3-SAT problem instance). In the real world, we might not be so lucky.
Often, an Evolutionary Algorithm might encounter a local optimum within the search space, and it will not be so easy to escape — offspring generated will be in close proximity of the optimum, and the mutation will not be enough to start exploring other parts of the search space.
To add to the frustration, there might not enough time or patience to wait for the Evolutionary Algorithm to finish. We might have different criteria we are looking for, outside of just a fitness target.
We are going to tackle both of these issues.
Applying Termination Conditions
First, we will examine what criteria we want met before our Evolutionary Algorithm terminates. In general, there are six that are universal:
- Date and Time. After a specified date and time, terminate.
- Fitness Target. This is what we had before; terminate when any individual attains a certain fitness.
- Number of Fitness Evaluations. Every generation, every individual's fitness is evaluated (in our case, every generation \(\mu + \lambda\) fitnesses are evaluated). Terminate after a specified number of fitness evaluations.
- Number of Generations. Just like the number of fitness evaluations, terminate after a specified generations.
- No Change In Average Fitness. This is a bit tricky. After specify \(N\) generations, we check every \(N\) generations back to determine if the average fitness of a population has improved. We have to be careful in our programming; by preserving diversity, we almost always lose fitness.
- No Change In Best Fitness. Just like No Change In Average Fitness, but instead of taking the average fitness, we take the best.
Later, we will see how Conditions 5 & 6 will come in handy to determining if we are stuck in a local optimum.
To make sure we are always given valid termination conditions, we will have a super class that all termination conditions will inherit from. From there, we will have a separate condition for each of the listed conditions above.
class _TerminationCondition:
pass
class FitnessTarget(_TerminationCondition):
"""Terminate after an individual reaches a particular fitness."""
class DateTarget(_TerminationCondition):
"""Terminate after a particular date and time."""
class NoChangeInAverageFitness(_TerminationCondition):
"""Terminate after there has been no change in the average
fitness for a period of time."""
class NoChangeInBestFitness(_TerminationCondition):
"""Terminate after there has been no change in the best fitness
for a period of time."""
class NumberOfFitnessEvaluations(_TerminationCondition):
"""Terminate after a particular number of fitness evaluations."""
class NumberOfGenerations(_TerminationCondition):
"""Terminate after a particular number of generations."""
Now, we need something that will keep track of all these conditions, and tells us when we should terminate. And here's where we need to be careful.
First, we need to know when to terminate. We want to mix and match different conditions, depending on the use case. This begs the questions:
Should the Evolutionary Algorithm terminate when one condition has been met, or all of them?
Generally, it makes more sense to terminate when any of the conditions have been met, as opposed to all of them. Suppose the two termination conditions are date and target fitness. It does not make sense to keep going after the target fitness is reached, and (if in a time crunch) it does not make sense to keep going after a specified date.
Second, how should we define no change in average/best fitness? These values can be quite sinusoidal, so we want to be more conservative in our definition. One plausible solution is to take the average of the first quartile (the first 25% to ever enter the queue), and see if the there is a single individual with a better fitness in the second, third, or fourth quartile (the last 75% percent to enter the queue). This way, even if there were very dominant individuals in the beginning, a single, more dominant individual will continue the Evolutionary Algorithm.
From this, we have everything we might need to keep track of our terminating conditions.
class TerminationManager:
def __init__(self, termination_conditions, fitness_selector):
assert isinstance(termination_conditions, list)
assert all(issubclass(type(condition), _TerminationCondition)
for condition in termination_conditions), \
"Termination condition is not valid"
self.termination_conditions = termination_conditions
self.__fitness_selector = fitness_selector
self.__best_fitnesses = []
self.__average_fitnesses = []
self.__number_of_fitness_evaluations = 0
self.__number_of_generations = 0
def should_terminate(self):
for condition in self.termination_conditions:
if (isinstance(condition, FitnessTarget) and
self.__fitness_should_terminate()):
return True
elif (isinstance(condition, DateTarget) and
self.__date_should_terminate()):
return True
elif (isinstance(condition, NoChangeInAverageFitness) and
self.__average_fitness_should_terminate()):
return True
elif (isinstance(condition, NoChangeInBestFitness) and
self.__best_fitness_should_terminate()):
return True
elif (isinstance(condition, NumberOfFitnessEvaluations) and
self.__fitness_evaluations_should_terminate()):
return True
elif (isinstance(condition, NumberOfGenerations) and
self.__generations_should_terminate()):
return True
return False
def reset(self):
"""Reset the best fitnesses, average fitnesses, number of
generations, and number of fitness evaluations."""
self.__best_fitnesses = []
self.__average_fitnesses = []
self.__number_of_fitness_evaluations = 0
self.__number_of_generations = 0
def __fitness_should_terminate(self):
"""Determine if should terminate based on the max fitness."""
def __date_should_terminate(self):
"""Determine if should terminate based on the date."""
def __average_fitness_should_terminate(self):
"""Determine if should terminate based on the average fitness
for the last N generations."""
def __best_fitness_should_terminate(self):
"""Determine if should terminate based on the average fitness
for the last N generations."""
def __fitness_evaluations_should_terminate(self):
"""Determine if should terminate based on the number of
fitness evaluations."""
def __generations_should_terminate(self):
"""Determine if should terminate based on the number of generations."""
And the changes to our Evolutionary Algorithm are minimal, too.
class EA:
...
def search(self, termination_conditions):
generation = 1
self.population = Population(self.μ, self.λ)
fitness_getter = lambda: [individual.fitness
for individual
in self.population.individuals]
termination_manager = TerminationManager(termination_conditions,
fitness_getter)
while not termination_manager.should_terminate():
offspring = Population.generate_offspring(self.population)
self.population.individuals += offspring.individuals
self.population = Population.survival_selection(
self.population)
print("Generation #{}: {}".format(
generation, self.population.fittest.fitness))
generation += 1
print("Result: {}".format(self.population.fittest.genotype))
return self.population.fittest
However, we can still do better.
Generations Into Epochs
Before, the Evolutionary Algorithm framework we put in place was strictly a generational model. One generation lead to the next, and there were no discontinuities. Now, let's make our generational model into an epochal one.
We define an epoch as anytime our Evolutionary Algorithm encounters a local optimum. Once the end of an epoch is reached, the EA is reset, and the previous epoch is saved. Upon approaching the end of the next epoch, reintroduce the last epoch into the population; by this, more of the search space is covered.
How can we determine if we are at a local optimum?
We can't.
That does not mean we cannot have a heuristic for it. When there is little to no change in average/best fitness for a prolonged period of time, that typically means a local optimum has been reached. How long is a prolonged period of time? That's undetermined; it is another parameter we have to account for.
Note, if the Evolutionary Algorithm keeps producing more fit individuals, but the average fitness remains the same, the algorithm will terminate. Likewise, if the best fitness remains the same, but the average fitness closely approaches the best, the EA will terminate. Therefore, we should determine if the best fitness and the average fitness has not changed; only then should we start a new epoch.
Luckily, we already have something that will manage the average/best fitness for us.
class EA:
...
def search(self, termination_conditions):
epochs, generation, total_generations = 1, 1, 1
self.population = Population(self.μ, self.λ)
previous_epoch = []
fitness_getter = lambda: [individual.fitness
for individual
in self.population.individuals]
termination_manager = TerminationManager(termination_conditions,
fitness_getter)
epoch_manager_best_fitness = TerminationManager(
[NoChangeInBestFitness(250)], fitness_getter)
epoch_manager_average_fitness = TerminationManager(
[NoChangeInAverageFitness(250)], fitness_getter)
while not termination_manager.should_terminate():
if (epoch_manager_best_fitness.should_terminate() and
epoch_manager_average_fitness.should_terminate()):
if len(previous_epoch) > 0:
epoch_manager_best_fitness.reset()
epoch_manager_average_fitness.reset()
self.population.individuals += previous_epoch
previous_epoch = []
else:
epoch_manager_best_fitness.reset()
epoch_manager_average_fitness.reset()
previous_epoch = self.population.individuals
self.population = Population(self.μ, self.λ)
generation = 0
epochs += 1
self.population = Population.survival_selection(self.population)
offspring = Population.generate_offspring(self.population)
self.population.individuals += offspring.individuals
self.__log(total_generations, epochs, generation)
total_generations += 1
generation += 1
print("Result: {}".format(self.population.fittest.genotype))
return self.population.fittest
def __log(self, total_generations, epochs, generation):
"""Log the process of the Evolutionary Algorithm."""
...
Although considerably more complicated, this new Evolutionary Algorithm framework allows us to explore much more of a search space (without getting stuck).
Let's put it to the test.
A New 3-SAT Problem
We're going to take on a substantially harder 3-SAT instance: 1,000 clauses, 250 variables. To make it worse, the number of valid solutions is also lower. We will also include the following terminating conditions:
- Time of eight hours.
- Fitness of all clauses satisfied (100).
- A million generations.
So, how does our Evolutionary Algorithm fare?
Not well. After twenty epochs, and thousands of generations — we do not find a solution. Fear not; in subsequent posts, we will work on optimizing our Genetic Algorithm to handle much larger cases, more effectively.