Skip to content

Computer Science

The ideas under the software: algorithms, theory, and the comforting news that the fundamentals never really change.

Artificially Unintelligent

mini(wins), max(losses)

NOTE πŸ“œ This is a post about AI. All views are my own and do not represent my employer. Please review my Disclosures.

Most problems in AI come down to optimization. Neural networks train by minimizing a loss. Good Old-Fashioned AI plays games by maximizing a heuristic evaluation. The architectures look nothing alike, but the engine underneath is the same: pick the parameter (or move) that pushes some number in the right direction.

For a chess engine, that number comes from a "fitness function": a heuristic that takes the board state and produces a score for the position. The AI then prioritizes moves that maximize its fitness while trying to minimize the adversary's. This is the famous minimax algorithm, and it goes something like this:

def minimax(board, depth, maximizing):
    if depth == 0 or board.is_terminal():
        return evaluate(board)

    if maximizing:
        value = float('-inf')
        for move in board.legal_moves():
            child = board.apply(move)
            value = max(value, minimax(child, depth - 1, False))
        return value
    else:
        value = float('inf')
        for move in board.legal_moves():
            child = board.apply(move)
            value = min(value, minimax(child, depth - 1, True))
        return value

We can do something interesting here. We win by maximizing our score and minimizing the opponent's. But what happens if we negate the fitness \(f(x)\), say by returning \(-f(x)\)? We start minimizing our own score and maximizing the opponent's. We start, in effect, trying to lose.

def dumb_evaluate(board):
    return -evaluate(board)

One line. That's the whole thing. The algorithm doesn't change; it still faithfully maximizes whatever score it's given. We just give it a worse score, and it faithfully drives the game off a cliff.

With this, we get some pretty entertaining games. I present to you Artificial Unintelligence.

Smart vs Dumb

Eight hand-picked games where Smart starts at a material deficit, often facing a Black army arranged into a deliberate visual pattern, and still wins. The set is ordered from the most ordinary to the most theatrical. Every game ends in checkmate (1-0).

Standard Match

Standard Match

The control. Standard opening, no FEN setup, no visual pattern, just Smart vs Dumb. Smart wins on g6 with a quiet bishop sacrifice and a queen mate. Sets the baseline before things get strange.

Pawn Cross

Pawn Cross

Black's king sits dead center inside a small + of pawns. Smart has only K + R against this miniature cross and threads the rook around the arms, stripping pawns one at a time before delivering Ra1#.

Zigzag Fence

Zigzag Fence

Black's pawns form a perfect zigzag fence across ranks 4–7. Smart's queen alone walks the fence end to end, picking off pawns on the diagonal and converging on Qe2#.

Bishop Constellation

Bishop Constellation

Black's eight bishops sit on the long diagonals like a star map; Smart's pawn phalanx sits below them. 36 moves of slow promotion warfare yield four white queens and a final Qc1# from the corner of the board.

Four Knights

Four Knights

Smart starts with K + four knights symmetrically posted on a1/c1/f1/h1; Black has the entire opening army. Knights jig their way into a cooperative net and mate before Black can mobilize a single major piece.

Skull Mask

Skull Mask

Black's pieces draw a skull: rooks for eye sockets, queens for temples, knight and bishops for jaw and teeth. Smart's king starts in the corner and tip-toes out while a rook and queen deconstruct each feature, ending Qd5#.

Spiral Vault

Spiral Vault

Black's pieces spiral outward from the king in a vault formation, a chaos of bishops, knights, rooks and queens stacked on every square of the upper half. Smart's R+B+Q carves a path inward through 27 moves of attrition to Qg7#.

Mosaic Blitz

Mosaic Blitz

Black's bishops and knights tile the back two ranks in a perfect checkerboard mosaic. Smart's pawn wave cracks open the mosaic, promotes three queens in five moves, and ends in a queen-and-queen double-mate.

AI Foundations

Ball don't lie, and neither does linear algebra.

NOTE πŸ“œ This is a post about AI. All views are my own and do not represent my employer. Please review my Disclosures.

In the future, AI will be the last problem humans solve by themselves. This final frontier is called AGI: Artificial General Intelligence. It will do just about everything a human can, and if it can't do it it will learn it.

As humans we learn in the brain and evolve through evolution. Machines learn in the cloud and evolve in the cloud, on processors with 80 billion transistors apiece, fed by datacenters that draw the power of a small city, programmed to do nothing but learn and evolve.

After they are born, nothing will be the same. Every problem we've been trying to solve, from medicine for every disease to a unified theory of physics, will be solved. Just not by us.

But that's AI of the future. Today, AI is seeping its tentacles into just about everything. It's taken over the voice assistants of the past, and turbocharged our code completion engines. What used to be ML is now AI, in part due to the generative wave: chatbots that hold a real conversation, image models that paint on demand, code completion that finishes your sentences before you do.

Today, I'll talk about what AI is, in concrete terms.

What Is AI

Three definitions, that you will certainly come across in most textbooks:

"The field of study that gives computers the ability to learn without being explicitly programmed."

β€” Arthur Samuel, 1959, coined while building a checkers program at IBM. The most-quoted definition in every textbook.

"A computer program is said to learn from experience E with respect to some class of tasks T and performance measure P, if its performance at tasks in T, as measured by P, improves with experience E."

β€” Tom Mitchell, 1997, from Machine Learning (McGraw-Hill). The formal definition.

"Artificial intelligence is the science and engineering of making intelligent machines, especially intelligent computer programs."

β€” John McCarthy, 1956, who coined the term "artificial intelligence" at the Dartmouth Workshop.

In short, AI is giving intelligence without explicit programming. It started with symbolic AI: logic, rules, search, puzzles. These developed into expert systems, using hard-coded rules for medicine, engineering, finance.

The early confident promises ran out, twice. The 1974-1980 AI winter. The 1987-1993 winter followed the collapse of the LISP-machine industry and expert systems' failure to scale into the enterprise. Funding evaporated both times. The current boom is the longest sustained period of progress AI has ever had, which is either a sign that the technology finally works or a sign that the third winter will be uglier than the first two when it comes.

Game-playing AI was the comeback story between the winters and the modern era. IBM's Deep Blue beat Garry Kasparov at chess in 1997 with brute-force search and a hand-crafted evaluation function. IBM's Watson took Jeopardy! in 2011 with statistical NLP layered over a curated knowledge base. DeepMind's AlphaGo beat Lee Sedol at Go in 2016, combining deep neural networks with Monte Carlo tree search in a game whose branching factor had been considered intractable. AlphaZero learned chess, shogi, and Go from self-play in a matter of hours the next year.

The modern era starts around 2012, when AlexNet won ImageNet with a deep convolutional network and a pair of GPUs, dropping the image-classification error rate by ten points and making deep learning the default. Word2vec (2013) showed that words could live as vectors in a learned space. The Transformer paper (2017) replaced recurrence with attention. GPT-3 (2020) showed that scale alone, billions of parameters and trillions of tokens, was a research direction. By late 2022 a public-facing chatbot crossed 100 million users in two months, and the industry has not stopped pouring concrete since.

Mathematically, an AI is nothing more than a function fitting by optimization. Simpler AIs can learn linear relationships and need just a few linear algebra equations at their disposal. More complex ones, such as language models, employ advanced techniques like neural networks, loosely modeled on the brain's layered neurons. As they are more sophisticated, they too need more sophisticated mathematics: calculus, statistics, and information theory. You don't need advanced maths to train or deploy AI models, but to make advancements in frontier AI you certainly will. You won't understand backpropagation if you don't understand derivatives and the chain rule; and thankfully, that's not our goal today.

The goal here is to use AI to solve day-to-day problems, not derive new theorems for the future. At least, not yet.

In Concrete Terms

The goal is to train an AI model, which is nothing more than the learned function mapping training data to predictions. Your training data contains the features you wish to train on, the input variable (i.e., square footage when predicting house price). The thing you're predicting is called the label or target (i.e., the actual house price).

Parameters are the internal values the model learns, such as the famous weights that correspond to how large language models store their knowledge. Along the way, you'll optimize hyperparameters, which are essentially settings you choose before training (i.e., learning rate, batch size, regularization strength). The learned function improves via a loss function, which measures how wrong the model is. For example, if you have a set of features you wish to train on and the labels that you know to be ground truth, you can simply minimize for the mean-squared error:

$$ \mathrm{MSE} = \frac{1}{n} \sum_{i=1}^{n} \left( y_i - \hat{y}_i \right)^2 $$

The dataset is usually broken into a:

  • Training Set Data the model learns from, usually 70–80% of your data.
  • Validation Set Held-out data for tuning hyperparameters.
  • Test Set Final unseen data for honest performance evaluation.

The split exists because of overfitting: the failure mode where a model memorizes training data instead of learning the pattern. A model that scores 99% on training and 60% on test isn't smart; it's a lookup table. Generalization is the opposite, where the model performs about as well on data it's never seen as on data it has. Holding out a test set is the only way to know which one you've got.

Underfitting is overfitting's mirror twin: the model is too simple, misses real patterns, and is wrong on both training and test. The bias-variance tradeoff is the search for the sweet spot: a model complex enough to capture the signal, simple enough not to memorize the noise. Most hyperparameter tuning is, in effect, walking that line.

After your model is trained, you make predictions via inference.

Metrics answer the question "did it work?" For classification, the headline is accuracy (fraction correct), but accuracy lies when classes are imbalanced. Precision is the fraction of positive predictions that were right; recall is the fraction of actual positives the model caught. F1 is their harmonic mean. For regression, MSE or RMSE is the default; MAE when outliers shouldn't dominate.

Basketball IQ

In most academic AI introductory texts, a problem would be built upon:

  • Iris dataset 150 flowers, four features, three species
  • MNIST 70,000 handwritten digit images, ten classes
  • California / Boston housing geographic and structural features, sale price

But, I find these to be too academic. My closest relation to Iris is that it's my grandmother's name. So I would like to step through a problem relevant to today. Literally today. Like, the NBA finals.

I like the fact that this problem is simple and easily relatable. But it's actually not an ideal AI problem. While it seems like a lot of data, an entire NBA season and playoffs, it really boils down to 82 regular season games per team and roughly 80 playoff games league-wide. Not ideal. But it'll be fun to watch our prediction play out in real time.

The full system, code, and whitepaper live at github.com/illyastarikov/artificial. Briefly: the model is a four-head ensemble (Elo + logistic regression + two LightGBMs), the features are 55 per-game columns, the label is whether the home team wins, and the dataset is 21,742 games split chronologically. The detailed model description, the metrics on the test set, and the live 2026 predictions are in the NBA Finals section below.

You don't have to figure out what regression or what sort of problem this is yet, we're going to do that in the next section.

Learning Paradigms

AI doesn't learn one way. There are four broad paradigms, ordered below from most-deployed in industry to most-research-flavored. Real systems mix and match, but every approach falls into one of these buckets.

Supervised learning

You have inputs and the correct outputs. The model learns the mapping. The dominant paradigm in industry.

Classification Predict a discrete category.

  • Algorithms: logistic regression, SVMs, random forests, gradient boosting, k-NN, naive Bayes, neural nets.
  • Famous uses: Gmail spam filtering, breast cancer detection from mammograms, ImageNet (AlexNet, 2012), credit card fraud detection.

Regression Predict a continuous number.

  • Algorithms: linear/ridge/lasso regression, regression trees, gradient boosting (XGBoost), neural nets.
  • Famous uses: Zillow's Zestimate (home prices), Uber surge pricing, demand forecasting at Walmart.

Unsupervised learning

No labels, no ground truth. The model has to find structure on its own: groups of similar things, hidden axes of variation, outliers that don't fit any pattern. Less common than supervised learning in deployed products, but it's how you make sense of data you've never seen before.

Clustering - Algorithms: k-means, DBSCAN, hierarchical, Gaussian mixtures. - Famous uses: customer segmentation, news topic grouping (Google News), gene expression analysis.

Dimensionality reduction - Algorithms: PCA, t-SNE, UMAP, autoencoders. - Famous uses: visualizing high-dim data, image compression, eigenfaces (face recognition, 1991).

Anomaly detection - Algorithms: isolation forest, one-class SVM, autoencoders. - Famous uses: fraud detection, network intrusion, manufacturing defect detection.

Association rules - Algorithms: Apriori, FP-growth. - Famous uses: Amazon "frequently bought together", market-basket analysis.

Semi-supervised & self-supervised learning

Mostly unlabeled data with a sprinkle of labels (semi-), or labels invented from the data itself like "predict the next word" (self-).

  • Famous uses: Google's early image search labeling, word2vec (2013), and the entire pre-training era of modern AI.

Reinforcement learning

An agent takes actions in an environment and learns from rewards. No labeled examples; feedback comes from outcomes.

  • Algorithms: Q-learning, SARSA, policy gradients, actor-critic, DQN, PPO.
  • Famous uses: AlphaGo beating Lee Sedol (2016), AlphaZero, OpenAI Five (Dota 2), DeepMind's Atari agents, robot locomotion, data-center cooling at Google.

Tip-Off

Enough theory. The NBA Finals problem is supervised at heart: every past game has a known winner, every feature has a known value, the model learns the mapping. The next section runs it.

NBA Finals

NBA playoff bracket

This bracket is the model's call from the start of the 2026 playoffs, before a single game tipped. The sections below walk through the model behind it, how accurate it is on held-out games, the bracket it produced, and where it has already gone wrong. How It Played Out grades the call against the real postseason.

The Model

Four predictors look at the same game from different angles: a chess-style team rating, a straight-line classifier, a tree ensemble that picks the winner, and another tree ensemble that picks the scoring margin. Their four predictions get blended together and run through a calibration step so the stated confidence matches the real hit rate. The full math, the hyperparameters, and the code are in the whitepaper.

Results on the Test Set

Across 60 independent training seeds on 3,261 held-out games:

Metric Mean Std
Log-loss 0.634 0.016
Brier score 0.215 0.004
Accuracy 65.9% 0.8%
Expected calibration error 0.023 0.006

65.9% accuracy beats the all-time NBA home-win rate (~60%) and lands within about a point of the ~67% that strong public game-prediction models have historically hit calling winners straight up. An ECE of 2.3% means when the model says "65% chance of winning," teams in that bucket win about 65% of the time. The calibration is honest.

The Start-of-Playoff Bracket

Before the first game, with all sixteen teams alive, a vectorized Monte Carlo simulator runs 100,000 best-of-seven bracket trials respecting the 2-2-1-1-1 home schedule, each game drawn from the model's per-game probabilities. A full bracket sim takes 30 seconds. The top four teams by title odds:

Team P(Conf. Finals) P(Finals) P(Champion)
SAS 80.4% 48.2% 36.3%
OKC 86.8% 44.7% 30.3%
DET 69.7% 41.9% 14.6%
BOS 54.3% 29.9% 10.4%

By title odds, SAS leads at 36% β€” a 2-seed the model rates like a 1-seed, in a sim where 2-seeds collectively win it all more often than 1-seeds do (47% to 45%). OKC is a close second. The East trails badly: DET and BOS fill out the top four, while the two teams that would actually emerge from it β€” NYK and CLE β€” sit way down at 3.6% and 1.0%. Hold that thought.

Model predictions vs. actual outcomes

Where the Model Missed

3 of the 12 series completed in the first two rounds:

  • PHI over BOS, Round 1, 3-1 comeback. Boston led 3-1; Philadelphia won the next three to take the series 4-3, the 14th team in NBA history to erase a 3-1 deficit. The model gave BOS 91% to advance.
  • MIN over DEN, Round 1. A 6-seed beating a 3-seed. The model gave DEN 85%.
  • CLE over DET, Round 2, Game 7. Detroit had the higher seed and the pre-series Elo edge; the model gave DET 79%. Cleveland won 4-3.

All three were calls the books also missed. No model nails every series; the question is whether it's biased. ECE of 2.3% says no.

Scenarios

Each of the 100,000 Monte Carlo trials is itself a complete bracket realization. Aggregate them and you get the title odds above. Look at individual trials and you get a distribution of brackets, some of which are pure chalk, a few of which are total chaos.

Modal bracket: the single most-frequent realization across the 100k trials

The most-frequent realization across the 100k trials is SAS over DET β€” the same SAS that tops the title odds. The model's single best bracket and its overall favorite agree on the champion. (Pure chalk, every top seed advancing, would crown OKC; the model likes SAS enough to override the seed line.)

Cinderella champion: ORL (8-seed) over OKC

The deepest Cinderella the simulator produced is ORL β€” an 8-seed β€” over OKC. The model crowns an 8-seed about once every 500 runs (0.2%). It exists in the distribution, but it's not where you put your money.

The full whitepaper, code, and per-trial scenarios live at github.com/illyastarikov/artificial.

How It Played Out

The model's favorite was SAS β€” 36% to win it all, and the single bracket it would have printed. SAS is in the Finals, so the headline call is alive.

It nailed the West. SAS and OKC were its top two by a wide margin, and they are exactly the teams that met in the West Finals; the favorite, SAS, came through. Across the first two rounds it went 9 of 12 β€” a 75% hit rate, the three misses (Philadelphia's comeback over Boston, Minnesota over Denver, Cleveland over Detroit) all upsets the books missed too.

It missed the East. The model loved DET and BOS β€” 42% and 30% to reach the Finals β€” and pegged the teams that actually got there, NYK and CLE, at 14% and 7%. The upsets that did it, Boston and Detroit falling, are the same ones on its miss list. So of the four conference finalists, the model got the West pair and missed the East pair: two of four, not a sweep.

A 36% favorite is a favorite, not a promise, and a model can read the field well and still be wrong about which top seeds fall. But the one bracket it would have bet has SAS cutting down the nets β€” and SAS is one series away. Game 1 is June 3; the title is still open.

The Last Problem

AI is function fitting by optimization. The function got enormous, but the engine is the same one Arthur Samuel described in 1959. What's changed is scale, and what scale buys is generalization. A model that fits 21,742 NBA games well enough predicts the Finals to within a few percent of where the books are. A model that fits the internet well enough holds a conversation in any language about any topic.

Thankfully, the function that solves the last problem hasn't been fit yet.

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
Loading document...
1 / 1 (preview)

Projects

Senior Year:

  1. Chess AI
  2. Puzzle Solvers
  3. Shape Packer
  4. Linear Algebra Library
  5. CFG Tracer
  6. Splatoonio

Junior Year:

  1. Space Invaders
  2. Camelot

Sophomore Year:

  1. CLC Tally
  2. Grading Suite

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.

Chess AI Demo

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.

Mechanical Match Demo

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.

My desk setup at Missouri S&T, circa senior year

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.

Shape Packer Demo

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.

Space Invaders Demo

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.

Socket Chat Client

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.

CLC Tally Screenshot

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
  1. TeX: 31,043 lines
  2. C/C++ Header: 28,235 lines
  3. C++: 17,246 lines
  4. SQL: 11,184 lines
  5. Python: 7,758 lines
  6. C: 2,667 lines
  7. Shell: 1,513 lines
  8. Assembly: 656 lines
  9. 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.

Academia Notes

4 years, 29 courses, 1,113 pages. Because who doesn't love compiler errors from their notes?

From 2014 to 2018 I kept nearly every homework, lecture note, and problem set in LaTeX. Mathematical notation, diagrams, code listings, all rendered properly. Compiled, they come out to 1,113 pages.

The source is on GitHub. Four compilations below, covering different subsets of the material.

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
Loading document...
1 / 1 (preview)

Courses Covered

Computer Science

Computer Engineering

Mathematics

Physics

Statistics, Philosophy, Psychology

How to Code on iPad

iPad is the best coding computer, which is why I always code on Mac.

Have you ever...

  • Had an idea at 11pm and wished you could just pick up where you left offβ€”without walking to your desk?
  • Wanted to code on the couch without the "I'm opening my laptop to work" signal that kills the vibe?
  • Tried to fit MacBook on an airplane tray table next to your drink?
  • Wished you could code at the park on nothing but cellular and a keyboard?

I have. So I started coding on my iPad; and I'm not going back.

Coding on iPad setup

A Slab of Glass (and a Keyboard)

iPad Pro with Magic Keyboard is the most comfortable computer I've ever used. Not the most powerful, not the most versatileβ€”the most comfortable. It weighs roughly a kilogram, runs silent, lasts all day, and disappears into a backpack without complaint. Every location that used to be "not a computer place" became one.

There's another side to portability: durability. Spill water on MacBook keyboard and you're buying new MacBook. Spill lava on my iPad's Magic Keyboard and I'm buying a new keyboard; an expensive one, sure, but a keyboard nonetheless.

The hardware is perfect for coding; the software is the problem.

Coding ON iPad

iPad Pros ship with the same M-series chips as Macs. The silicon is identical. In theory, this device could run macOS, Xcode, a full terminal, the works. But Apple won't let it. There's no native shell, no package manager, no pip install. Everything is an app, sandboxed and isolated.

I tried. Textastic has syntax highlighting and FTP. Pythonista runs Python directly on the device. These apps are impressive for what they are, but "what they are" is not a development environment.

I wanted to install my Vim configuration: my dotfiles, my plugins, the muscle memory I've spent years building. On Mac, it's a single git clone and a setup script. On iPad, it's an odyssey: file system sandboxing, half my plugins broken, no way to pipe commands together. I just wanted a terminal. A real one.

The editing was fine. Everything around itβ€”file management, version control, builds, testingβ€”felt like surgical precision with oven mitts. Long press to rename. Share sheet to move. Switch apps to run. iPad stopped feeling like a tool and started feeling like a puzzle.

So I gave up programming on iPad, and I started programming via iPad.

Coding VIA iPad

My MacBook already has everything I need: Z-Shell, Vim, tmux, Git, Python, C++ toolchains. All I needed was a way to reach it from my iPad. This problem was solved in 1995 via SSH.

An SSH Client

Blink is my SSH client. It's a native iOS terminal emulator that supports SSH and Mosh, renders fonts well, and integrates with iPad keyboard shortcuts so ⌘+Tab works like Mac. It doesn't try to be an IDE or a file manager. It's a terminal. That's it.

Other options worth considering:

  • Termius cross-platform with SFTP and port forwarding; has a free tier
  • Prompt polished and minimal, from the team behind Nova and Transmit
  • Secure ShellFish SSH client with native Files app integration for iOS and Mac

Reaching Your Mac

On the same network, macOS has SSH built in. Go to System Settings β†’ General β†’ Sharing β†’ Remote Login and toggle it on. Your Mac gets a local hostname, and from Blink:

ssh [email protected]

That's it. You're in.

On a different network, Tailscale creates a free personal VPN connecting your devices as if they were on the same network. Install it on both, sign in, and your Mac gets a stable address you can SSH into from anywhere.

Developing a website and hosting it locally? SSH port forwarding lets you access it on your iPad:

ssh -L 4000:localhost:4000 [email protected]

Now localhost:4000 in Safari on your iPad shows your local dev server running on your Mac.

Mosh: SSH's Resilient Sibling

Mosh is SSH for mobile connections. Where SSH dies on network hiccups, Mosh picks up where you left off and predicts keystrokes locally so typing feels instant on high-latency connections.

I use it sparingly. It occasionally mangles my terminal rendering, the kind of artifacts that make Vim feel like a funhouse mirror. For day-to-day coding, plain SSH is cleaner.

But for long-running connections, Mosh is unbeatable. If I'm port forwarding a dev server to Safari, I don't want that tunnel dying every time I switch apps or my WiFi wobbles. I'll start a Mosh session for the port forward, open Safari in Split View, and code in a separate SSH session. The Mosh connection just sits there, indestructible, keeping my preview running.

tmux: The Session That Never Dies

tmux is what makes this setup practical. It's a terminal multiplexer: persistent sessions with multiple panes that survive disconnections.

I start a tmux session on my Mac, open Vim in one pane, run a build in another. Then I close my iPad, go to sleep, wake up, open Blink, reconnect, and type tmux attach. Everything is exactly where I left it. The Vim buffer with my half-written function. The build output. The git log I was reviewing.

Without tmux, every disconnection kills your processes and closes your files. With it, your Mac becomes a persistent development server that your iPad dials into whenever you want.

A Typical Session

I open Blink, type ssh mac, and I'm at my Z-Shell prompt. If a tmux session is already runningβ€”it usually isβ€”I reattach:

tmux attach

From here, it's indistinguishable from sitting at my Mac. Vim, Python, C++, Git, grep. It is my Mac's terminal, just displayed on my iPad. Some common workflows:

  • Writing this blog in Vim and Markdown
  • Vibe coding with Claude Code, Vim, and whatever else the project needs
  • Web development running Ghost locally and forwarding port 2368 to Safari in Split View
  • Git commits, branches, and PRs without leaving the terminal
  • Server ops SSHing into production from the couch
Coding on iPad session

Where It Falls Apart

A few months in, here's what doesn't work.

Images are a pain. Anything visual, the files live on your Mac and a terminal image viewer doesn't cut it. For image-heavy work, I still reach for MacBook.

File transfer is clunky. AirDrop and iCloud Drive work but neither integrates into a terminal workflow. Too much context-switching.

Network quality matters. Fast WiFi feels local. Weak signal or cellular, every keystroke has a delay. There's a floor below which this isn't enjoyable.

Your Mac needs to stay on. The display can sleep, but the system needs to keep running. Keep it plugged in and toggle System Settings β†’ Battery β†’ Options β†’ "Prevent your Mac from sleeping automatically when the display is off" (or pmset -a sleep 0 in Terminal). One-time setup.

One screen is still one screen. Split View gives you two apps, but there's no second monitor for documentation.

I'm still learning. A few months in, I'm still hitting walls. That's part of the appeal, honestly. There's something satisfying about optimizing a setup nobody designed but works anyway.

The Best Computers

My iPad isn't replacing my Mac. It's extending it to places Mac can't comfortably go. Mac does the computing; iPad does the being-there.

If you have Mac and iPad, you already have everything you need. SSH has been around since 1995. tmux since 2007. The tools are decades old. The form factor is what's new.

The best coding computer is the one that lets you code when you otherwise wouldn't.