Pong RL
Pong from Pixels with Seven Deep RL Methods
Seven deep RL methods learn Pong from nothing but pixels, side by side on the same frames and the same score.
- DQN +4.60, 100% wins
- 3.6 min on one CPU thread
- 34 tests
Built withPyTorch
NumPyGymnasiumMatplotlib
pytest

Overview
Pong from pixels is the problem that made deep reinforcement learning famous, and Karpathy's 'Pong from Pixels' was my starting point: a coursework notebook with his numpy policy gradient and a PyTorch rewrite. Turning it into a package meant fixing eleven problems in that notebook, the main one a loss that gave zero gradient to every DOWN action.
I then built every main method on the same preprocessed frames: Karpathy's policy gradient (an MLP on frame differences), REINFORCE with a learned baseline, A2C with 16 parallel games, PPO with GAE and a clipped ratio, and DQN with double and dueling heads, prioritized replay and n-step returns. Each learning rule is one file of 60 to 120 lines; the game, the CNN, the evaluation and the logging are shared.
Because a pixel agent needs millions of Atari frames to win, I wrote a 40x40 numpy Pong with the same three actions, frame skip and a scripted opponent, preprocessed to 20x20 binary frames. Every method learns it in 2-6 minutes on one CPU thread, which makes the differences visible: the DQN family beats the opponent after 75k-100k steps, PPO after 125k, and the Monte Carlo methods need 175k to 400k. The same code trains on ALE/Pong-v5 in the Kaggle / Colab notebook.
The question
When seven deep RL methods see the same preprocessed frames and are scored by the same evaluation, how much do a learned baseline, bootstrapping and experience replay change the number of samples needed to win at Pong?
Key features
- Seven methods behind one interface: pg (Karpathy), reinforce, a2c, ppo, dqn, double-dqn and dueling-dqn, with --per and --n-step as flags for the DQN family.
- Two games, one interface: the built-in small Pong (20x20 binary frames, first to 5) and ALE/Pong-v5 (80x80 Karpathy preprocessing, frame skip 4, sticky actions, ROMs bundled, headless).
- Two ways to see motion: --obs diff (frame difference, one channel) or --obs stack (4 frames), accepted by every method.
- Two CNN encoders chosen automatically: a small one that keeps a 4x4 spatial map on 20x20 frames and the Nature CNN for 80x80, with orthogonal initialisation.
- A vectorised environment that steps n games in one process, auto-resets and returns the final frame so DQN can bootstrap through a time-limit cut.
- One evaluation for everyone: mean score over 30 games on fixed serves, win rate, and a --greedy switch; curves against steps and against wall-clock time.
- Four CLIs (pong-train, -eval, -play, -compare), six numbered examples, 34 tests, and a legacy folder that documents what the original notebook did and how each bug was fixed.
How it works
Preprocess the frame
ALE frames are cropped, downsampled by 2 and binarised exactly as in Karpathy's prepro (80x80). The small game is downsampled 40x40 to 20x20 with a max over 2x2 blocks so the 2x2 ball can never vanish.
Encode motion
One frame shows where the ball is, not where it goes. diff subtracts the previous frame so a moving object becomes a +1 blob next to a -1 blob; stack keeps the last 4 frames and lets the network compare them.
Learn, three ways
pg and reinforce wait for whole games and use Monte Carlo returns (reset at each point), with or without a learned V(s). a2c and ppo bootstrap after 5 or 64 steps with a critic, PPO reusing each batch for 4 epochs under a 10% ratio clip. The DQN family regresses Q to a target network from a replay buffer with a Huber loss and epsilon-greedy acting.
Evaluate on fixed serves
Copy i of the game is reset with seed 10000 + i, so every method faces the same serves and opponent noise; the policy-gradient agents sample their actions, the DQN family plays argmax Q.
Compare
scripts/compare_all.py trains the seven methods one at a time and builds the results table, the curves against steps and against minutes, and the one-panel-per-method GIF.
Examples


Input
pong-train --algo dqn && pong-eval runs/dqn/model.pt --episodes 30
Output
mean score +4.60 ± 0.55 over 30 games, win rate 100% (180,000 steps, 3.6 min on one CPU thread)
Tech stack
Karpathy MLP, CNN actor-critic and Q-networks, and the seven learning rules
The built-in 40x40 Pong, preprocessing, discounting and the sum-tree replay
ALE/Pong-v5 through ale-py, with the same three-action interface as the small game
Learning curves against steps and wall-clock time, preprocessing figures
34 tests: gradient identities, PPO clip, double DQN target, a pixel bandit each method must learn, the CLIs
What I learned
- Initialisation decided whether anything learned: with PyTorch's default init on sparse binary frames the PPO policy head got almost no gradient for 150k steps; orthogonal init fixed it
- A shorter horizon (gamma 0.99 to 0.97) gave lower-variance returns on the small game, where a point is decided 10-20 decisions after the move that mattered
- The original coursework notebook's loss gave zero gradient to every DOWN action; the full Bernoulli log-likelihood fixes it and a test checks it equals Karpathy's A (y - p) gradient
Limitations
- All results come from the built-in small Pong, one seed per method; no ALE Pong number in the repo comes from a trained agent (the real ALE runs live in the notebook with estimated budgets only)
- Double and dueling DQN with prioritized replay and 3-step returns land within noise of plain DQN here: the small game shows too little overestimation for the upgrades to matter
Keywords
reinforcement learning · Pong from pixels · Atari · policy gradient · REINFORCE · A2C · PPO · DQN · dueling DQN · prioritized experience replay · n-step returns · frame stacking · Arcade Learning Environment · Gymnasium · PyTorch