← Back to the AI section

402 Agency · AI research since 2017 · Bachelor's thesis project, Technical University of Sofia, February – December 2017

TU.Diplomna

A two-player pygame arena and a Deep Q-Network agent whose Q-function is a multilayer perceptron written in numpy by hand. “Diplomna” is Bulgarian for thesis; it is the repository's own name. Written up in 2026 from the code and the 35 commits.

Arena
960 × 512 px
30 × 16 tiles of 32 px
Observation
26 features
13 per unit, absolute x, y
Actions
4: up, down, left, right
no shoot action
Q-network
26 → 10 → 6 → 6 → 4
tanh on every layer
Training
300,000 steps, headless
batch 1, γ 0.2, lr 1e-4
Code
~1,500 lines Python, 15 modules
numpy, pygame, matplotlib
History
35 commits
17 Feb – 4 Dec 2017
Source
Bitbucket: donald_ovcharov / tu.diplomna
public repository, 672 MB
Video
Learned agent vs bot
as linked in the README, not re-verified
The thesis
English showcase edition
the 48-page document, with its figures and experiments

1An assessment

Impressive for a bachelor's thesis in 2017?
Yes. One student built every layer by hand in ten months, physics engine to backpropagation, at a time when the ingredients were online but nothing joined them.
A good implementation of the DeepMind paper?
Partly. The idea is there and it works well enough to produce a learned behaviour. The two mechanisms the paper introduced for stability are missing, and two core lines of the final commit are wrong.

The reasoning below comes from the code and the commit history, not from recollection. Terms are explained in section 2.

What was available in 2017

The DeepMind paper had been in Nature for two years, and DeepMind had published its reference code, in Lua for the Torch framework. OpenAI Gym had launched in 2016; OpenAI's own DQN baseline arrived in May 2017. Both were built on TensorFlow, whose 1.0 release came in February 2017; PyTorch was a months-old beta. Tutorials existed but were few and framework-based: a handful of blog posts implementing DQN in Keras or TensorFlow on Gym's CartPole and Atari environments. From-scratch reinforcement-learning walkthroughs were rare; the best known, Karpathy's 2016 Pong-from-pixels post, used a different method (policy gradients). For plain neural networks there were two well-known numpy walkthroughs, Matt Mazur's step-by-step backpropagation example and Andrew Trask's eleven-line network, and the first versions of this repository's network use their vocabulary: neth1, outh1, Etotal, a nonlin(x, deriv) helper, np.random.seed(1).

So the halves existed separately: a paper and framework code on one side, from-scratch tutorials for plain networks on the other. No code assistant, and a thin Stack Overflow on reinforcement learning. A student who wanted to understand the method rather than call a library had to join the halves from the paper, the calculus and trial and error. The 35 commits are a record of that.

What was built, and what stands out

Everything in the pipeline is hand-written: a 2-D physics engine with vector force summation, per-tile friction and momentum-based knockback; a game with terrain, swords, bullets and health; an environment in the shape of Gym's step and reset; a scripted opponent; a 26-number state; a four-layer neural network with its own backpropagation; a Q-learning loop with exploration, action repeat and a Bellman target. About 1,500 lines, one author, no learning library at any point.

  • The method was validated in stages. A Q-table on a five-state version of the problem first. The network fitted to known functions (a square root, a sine) before it was trusted with Q-values. Backpropagation derived twice, once in scalars and once in matrices, and the two compared; the comparison caught an index error.
  • The debugging is legible. Commits go from “not working” to “working model” through identifiable fixes: a flipped error sign, a deleted weight update, biases that were not summed over the batch. Thousands of cost-curve images were generated to watch training and then removed.
  • The choices show understanding of the mechanics, not just the recipe. Learning only from exploratory moves, a clean use of the fact that Q-learning is off-policy. Holding a random action for five frames so it has a physical effect at thirty steps per second. Feeding absolute coordinates so the network has to learn geometry. Diagnosing that one shared learning rate starved the output side of a tanh stack, and fixing it with per-layer step sizes. Collapsing the Q-table's state space until the table could fill.
  • Reproducibility habits unusual in student code. A seeded random generator “so random gets deterministic”, and numeric warnings promoted to exceptions.
  • A learned behaviour at the end. Dodging, in a two-player game with physics, against an opponent with perfect aim, as the README reports; a video of it is linked from the README.

Against the paper

As an implementation of the DeepMind recipe it is partial. The core is present: a network standing in for the Q-table; exploration by sometimes acting at random; a target that copies the network's own prediction and overwrites one action's entry with the reward plus a discounted estimate of the future; gradient descent on the squared error. Two mechanisms are missing, and they are the two the paper introduced to make this stable: experience replay, learning from a random sample of remembered moments rather than the latest one, and a target network, a frozen copy used to estimate the future so that the goal does not move with every update. Without them the method is the online neural Q-learning that predates the paper, and which the paper describes as prone to instability.

The final commit also has faults in the two lines the algorithm hinges on: the discounted-future term uses the position of the best next action instead of its value, and the derivative used in backpropagation is taken one step off. The reward is computed from the opponent's side. What the network could learn from, in that commit, was the immediate reward; the README reports dodging, and the repository holds no evaluation data to say more. Section 10 lists each of these with the line numbers.

Verdict

Scope, for one student in ten monthsWide. Physics, game, environment, network, backpropagation and learner, all written by hand.
Understanding of the methodShown in the design choices and in how each failure was diagnosed and fixed.
Faithfulness to the DeepMind paperPartial. The idea, not the stabilising recipe; replay and target network absent; two core lines wrong.
Rigour of evaluationLow. No metrics, no saved weights, no baseline; one video and one README sentence.
Code qualityResearch code. Configuration by editing source, dead code kept as a record, a virtualenv committed.

For 2017, for one student working from a paper and scattered tutorials, this is substantial work: the parts most people skip, the calculus and the debugging, are exactly the parts done by hand. It is not a faithful DQN. Read it as evidence of understanding, not as a reference implementation.

2The terms, explained simply

Everything on this page can be followed with these. Each entry gives the plain idea first.

The learning problem

Reinforcement learning
Learning by trial and error, the way you would train a dog with treats. The learner tries things, gets points for good outcomes, and does more of what earned points. Nobody shows it the right answer; it has to find out.
Agent, environment
The agent is the learner (here, the unit the network controls). The environment is the world it acts in (the arena, the opponent, the physics).
State
A snapshot of the situation, as a list of numbers. Here: where both units are, their health, what tile they stand on, where the bullets are. 26 numbers in all.
Action
One of the moves the agent can make. Here: up, down, left, right. It cannot shoot.
Reward
The points for one step. Here: most of it for not standing on lava, the rest for not having been hit much. The agent's job is to collect as much reward as it can over time.
Episode
One game, from the start until a unit's health reaches zero.
Policy
The agent's rulebook: which move to make in which situation. The whole point of training is to end up with a good one.
Hyperparameters
Settings a person chooses before training rather than something the agent learns: how fast to learn, how much to care about the future, how big the network is. “Hand-tuned” means chosen by trying values.

The scores

Q-value
A score for “how good is it to make move A in situation S”, counting the reward that follows. Q is for quality. Once you have good Q-values, the policy is simple: in each situation, pick the move with the highest score.
Q-table
A spreadsheet of Q-values: one row per situation, one column per move. It works when situations are few. It cannot work when situations are described by 26 continuous numbers, because there are effectively infinitely many rows.
Q-learning
The rule for improving the scores from experience: after each move, nudge the score you had toward “the reward I just got, plus a share of the best score available from where I ended up”.
Bellman update, bootstrap
That nudge, named after the mathematician who wrote it down. “Bootstrapping” means updating a guess using another guess: the value of now is estimated from the value you expect next, which is itself an estimate.
Discount, γ (gamma)
How much future points count compared with points now. 0.9 is patient: rewards ten steps away still matter. 0.2, the value used here, is short-sighted: only the next step or two count.
Learning rate, α (alpha)
How far to move toward the new estimate each time. Too large and the scores swing wildly; too small and learning takes forever. The schedule of lowering it during training is the same idea as taking smaller steps as you get close.
Exploration, ε-greedy
Mostly make the move you currently think is best (“greedy”), but sometimes make a random one so you discover moves you had underrated. ε (epsilon) is the chance of the random move. The 2017 code used the letter for the opposite chance; the page says so where it matters.
Off-policy
Learning about the best possible rulebook while behaving by a different one, for instance mostly at random. Q-learning can do this because its update asks “what is the best move from here”, not “what move did I actually make next”.
Action repeat
Holding a chosen move for several frames instead of re-deciding thirty times a second, so that a move has a visible effect before it is judged.

The network

Neural network
A large adjustable formula with many knobs, that turns a list of input numbers into a list of output numbers. Learning means turning the knobs, a little at a time, so the outputs get closer to what they should be. The network here has 406 knobs.
Weights, biases
The knobs. Weights scale each input on its way to the next layer; biases shift the result. “Initialised uniform in (−1, 1)” means every knob starts at a random setting in that range.
Layer, hidden layer
The network is built in stages. Inputs go into the first stage, its outputs into the next, and so on. The stages between input and output are “hidden”. This one has three: 10, 6 and 6 units wide.
Activation, tanh
After each stage, every number is passed through a squashing function so values stay in a sane range and the network can represent curves, not just straight lines. tanh squashes anything into −1 to 1. Using it on the output too means every Q-value is capped at 1.
Function approximation
Using a formula (the network) to estimate the scores instead of storing them in a table. The formula can give an answer for situations it has never seen exactly, which a table cannot.
Loss, cost
A single number for how wrong the network's output is compared with the target. Training pushes it down. “Squared error” means the wrongness is squared, so big mistakes count much more than small ones.
Gradient descent
The method for turning the knobs: work out, for each knob, which direction lowers the loss, then move every knob a small step that way. Repeat many thousands of times.
Backpropagation
The bookkeeping that finds those directions, working backwards from the output through each stage using the chain rule from calculus. Frameworks like TensorFlow and PyTorch do this automatically. Here it is written out by hand, which is the unusual part.
Vanishing gradient
In a deep stack of squashing stages, the “which way to turn” signal gets smaller at every stage it passes back through, so some layers barely learn. The per-layer step sizes in this network (×1, ×10, ×100, ×1000) are a hand-made fix for that.
Features, feature engineering
The numbers a person chooses to hand to the network. “Hand-picked features” means the author decided that position, health, tile type and bullet positions are what matters, and scaled each to a sensible range. The alternative, which DeepMind used, is to hand over raw pixels and let the network find the features.
Batch, minibatch
How many examples the network learns from in one knob-turn. One example at a time is “batch size 1”; several averaged together is a minibatch.

The DQN parts

Deep Q-Network (DQN)
Q-learning where the spreadsheet is replaced by a neural network. The DeepMind version, published in Nature in 2015, learned to play Atari games from the screen pixels alone.
Experience replay
Keep a diary of past moments (situation, move, reward, what came next) and study random pages from it instead of only the most recent moment. This stops lessons being distorted by whatever happened to occur in a row. Not used in this project.
Target network
When estimating “the best score from where I ended up”, use a frozen copy of the network that is refreshed only now and then, so the target does not shift every time the network learns. Not used in this project.
Headless
Running the game without drawing it, so training goes as fast as the computer allows instead of thirty frames a second.
numpy, pygame, Gym
numpy is Python's library for fast arithmetic on arrays of numbers, the tool the network is written with. pygame is a library for making 2-D games. OpenAI Gym is a standard way to package a game so learning code can plug into it with two calls, reset and step; this project copies that shape.

Live demo · tabular Q-learning · built in 2026 for this page, not the 2017 code

Q-values on a smaller arena

lava −1, episode ends grass 0 ice: slide to the next non-ice tile sword +1, episode ends mean Q > 0 mean Q < 0 ▲ greedy action
steps per second
episode
0
steps
0
last return
last 50 episodes
map seed
1

Arena focused: Space run / pause · S step · R reset

The 2017 agent used a neural network over 26 continuous features. This table over 120 cells makes every update visible: one state per tile, four actions, and the readout shows the Bellman update with the actual numbers. The heat-map is the 2017 visualiser's mapping, mean Q per tile, green positive and red negative; arrows show the greedy action. The ε here is the probability of exploring, the usual convention. The 2017 variable eps was the probability of acting greedily. The terms are explained in section 2 above.

3The game

Two units on a 960 × 512 px arena divided into 30 × 16 tiles of 32 px. The outer three columns and two rows are lava; the interior is grass, with a one-in-ten chance of ice per tile. One sword lies on the map at a time. Walking over it adds 5 to the mass of your loaded bullet and spawns a new sword elsewhere. Bullets fly at 300 px/s with a 0.6 s cooldown. A hit applies a knockback force of bullet mass × 4000 × the victim's hit factor, then raises that hit factor by 0.2, so every hit makes the next one push harder. Lava drains 15 health per second. You win by pushing the opponent onto the lava.

30 tiles × 32 px = 960 px 16 × 32 px = 512 px sword · +5 bullet mass UNIT_1 · bot bullet · 300 px/s UNIT_2 · learner lava · 3 columns, 2 rows · 15 hp/s grass · friction 40 ice · 1 in 10 tiles · friction 0.5 on hit: force = bullet mass × 4000 × victim hit_factor, then hit_factor += 0.2 sword picked up: loaded bullet mass += 5 · bullet 300 px/s · cooldown 0.6 s
Figure 1. The arena as the code defines it: grid, lava bands, terrain, units, sword and a bullet in flight. Ice positions are random per game; these are illustrative.

Physics

  • Units: mass 10, health 100, hit factor 1, control speed 200 px/s.
  • Forces are composed by vector addition; kinetic friction per tile: grass 40, lava 40, ice 0.5, so ice really slides.
  • Fixed timestep of 1/30 s. Training runs the same physics without a frame clock.
  • Bullets are units with isFlying set: no friction, no lava damage, removed when they leave the screen.

The environment interface

The environment is written in the shape of OpenAI Gym, which was a year old at the time. env.step(events) returns (state, reward), env.restart() resets, env.checkWin() ends an episode when either unit's health reaches zero. Rendering lives in a separate DrawEngine, so training runs headless at whatever speed the CPU allows and the game is drawn only afterwards. The Gym loop is pasted into game_engine.py as a TODO comment, which is an honest description of how far the resemblance goes: there is no done flag and no info.

The opponent

bot.py is 25 lines. It walks along whichever axis is farther from the sword and fires at the learner's centre on every frame, limited only by the cooldown. It never dodges. Because it monopolises the swords, its bullets get heavier over a game. The learner is UNIT_2, has no shoot action, and can only move.

4What the learner sees

The state is 13 hand-picked features per unit, for both units, in a fixed order: 26 numbers. Positions are fed as absolute x and y rather than as distances. The README states why:

“I am feeding the NN with (X,Y) values instead of distances so that the NN has to understand the concept of map positions.”

positionbodytilenearest own bulletsecond bullet unit 1bot unit 2learner xyhphitmassdpsμb1xb1yb1θb2xb2yb2θ xyhphitmassdpsμb1xb1yb1θb2xb2yb2θ ÷1000÷1000÷100÷30÷100÷15÷40pxpx÷360pxpx÷360 ÷1000÷1000÷100÷30÷100÷15÷40pxpx÷360pxpx÷360 hatched: always 0 · the learner has no shoot action, so it owns no bullets
Figure 2. The 26 inputs in order (unit_state.py). μ is the friction coefficient of the tile under the unit; b1 and b2 are the unit's own two nearest bullets, sorted by distance, with their heading. Bullet x and y are appended in raw pixels; everything else is scaled to about 0 – 1.

The learner never fires, so its own six bullet features stay at zero. The bot's bullets enter through the bot's block, sorted by distance to the bot, which is where they were just launched from; that is the signal a dodging policy has to work with.

Four actions, encoded as a bitfield (up 8, down 4, left 2, right 1). No diagonals are used and there is no shoot action. The reward for one unit is

reward = 0.7 · (1 − dps/15)
       + 0.3 · (1 − clip(hit_factor, 0, 30)/30)
# health and sword-mass terms exist, weighted 0
# unit_state.py:61-68

which is 0.7 for not standing on lava plus a share that decays as the unit accumulates hits. The environment reward is reward(unit 1) − reward(unit 2). There is no terminal bonus and no done flag.

5The Q-function

No framework. nn.py is a numpy class with four weight matrices, four bias rows and a hand-written backward pass. Every layer, including the output, is tanh, so each Q-value is bounded to (−1, 1). Weights start uniform in (−1, 1) under np.random.seed(1), with the comment “Important for debug, random gets deterministic!”; np.seterr(all='raise') turns any numeric warning into an exception. Loss is half the squared error; the optimiser is plain gradient descent.

26 features10 tanh6 tanh6 tanh4 tanh = Q(s,·) unit 1 · 13 unit 2 · 13 updownleftright 26×10 weights · lr ×110×6 · lr ×106×6 · lr ×1006×4 · lr ×1000
Figure 3. The final network (nn.py, commit ffa4186f). Weights and biases start uniform in (−1, 1) with seed 1; loss ½ Σ (target − output)²; plain gradient descent with base learning rate 1e-4 and a per-layer multiplier on the step.

The per-layer multipliers are the last change made to the network:

# nn.py, lines 97-105
self.wx1 += deltaW0*self.lr
self.w12 += deltaW1*self.lr*10
self.w23 += deltaW2*self.lr*100
self.w3y += deltaW3*self.lr*1000

With one shared learning rate, the backpropagated error shrinks through each tanh layer and the output side barely moves. Scaling the step by 1, 10, 100 and 1000 from input to output compensates by hand. The base rate was cut to 1e-4 and the net narrowed to 26 → 10 → 6 → 6 → 4 in the same commit (11 Sep 2017). The commit message calls it removing weight decay; there was never an explicit decay term in the code, so what was removed is the implicit decay of the step size with depth.

Before the class was trusted with Q-values it was validated as a plain regressor on √(x² + y²), on a sine over [−20, 20] and on x/2; the harnesses survive as comments at the bottom of nn.py. Earlier still, network.py derives backprop for a 2-4-1 net by hand, one partial derivative per line with the chain rule written out in comments, then repeats the computation in matrix form directly beside it and compares the two. That comparison caught an index error in the scalar version.

6The training loop

state s26 features NN.predict(s)Q(s,·), 4 values choose agreedy with p = epselse random, held 5 frames env.stepbot walks to sword, firesphysics tick 1/30 s (s′, r)r = reward(u1) − reward(u2) target = Q(s,·)target[a] = r + γ·bootstrap NN.fit_singleone sample, batch 1backprop, gradient step only on exploratory frames s ← s′ · next step · env.restart() when a unit's health reaches 0 300,000 steps · eps 0.2 → 0.3 at 75k → 1.0 at 300k · lr 1e-4, ×0.5 at 150k, ×0.5 at 225k, ×0.1 at 262.5k cost printed every 10,000 steps · matplotlib curve at the end · then the game is drawn, AI vs bot, at 30 FPS
Figure 4. One step of main_nn.py. The network is updated only on the frames where a random action was chosen, from that single transition.

The variable eps is the probability of taking the greedy action, the opposite of the usual convention: 20 % greedy for the first quarter of training, 30 % after, 100 % once training ends. A random action is held for five frames so that it has a visible physical effect at 1/30 s per step; the network is updated only on those frames. So the behaviour policy is mostly random, the update is toward the greedy value, and the net learns only from exploratory transitions: off-policy in the strict sense.

A 20-sample minibatch was tried on 17 August and dropped two days later. The commit message, in Bulgarian, records the reason: on batches the updates average out, without them the network “corrects on every move”. With only the taken action's entry carrying new information, summing twenty targets does smear the signal across all four outputs.

steps300,000, headless (the README says 200k; the code says 300k)
greedy probability (eps)0.2 → 0.3 at step 75,000 → 1.0 after training
action repeatrandom action held 5 frames
updateonly on exploratory frames; batch 1
targetpredicted Q(s,·) with target[a] = r + γ · bootstrap
γ0.2
learning rate1e-4 base; ×0.5 at 150k, ×0.5 at 225k, ×0.1 at 262.5k; per-layer ×1 / ×10 / ×100 / ×1000
replay buffer, target networknone
loggingstep and last cost every 10,000 steps; cost curve shown at the end; nothing saved to disk

Cost is printed every 10,000 steps and plotted with matplotlib when training ends; then the loop keeps going with rendering on, AI against bot at 30 FPS. Nothing is saved: the weights live only in that process. No cost curve is reproduced here. 5,898 curve images from runs past 10.8 million steps were committed and deleted the same day (commit 810a5427), and no numbers survive in the repository.

7Before the network: the Q-table

The first learner, in April 2017, was a Q-table. 120 position states aliased and failed. 480 states, one per tile, with a terrain reward and count-based exploration, failed too. What worked on 3 April was collapsing the state to five values: which lava band the unit is in, or the interior (lr 0.1, γ 0.5, ε 0.5). A visualiser coloured every tile by the mean Q of its state, green positive and red negative, which is the display the live demo above re-creates.

01234
Figure 5. The state space that made tabular Q-learning work (commit 1edadedb): five states, five actions including stand still, 25 table entries.

Five states cannot express a bullet, an opponent or a sword, which is what pushed the project to function approximation. The README, later:

“Initially I have implemented it with Q-table but wasn't sufficient due to big state space, so I have decided to go for NN to replace the Q-Table.”

A scikit-learn MLPRegressor was tried first, in July, and replaced by the hand-written network in August.

8Result

From the README, December 2017:

“My success with the AI is that it learns to avoid bullets. Can't shoot yet.”

A video of the learned agent is linked from the README on Google Drive; the link is reproduced as recorded and was not re-verified for this page. There is a mode to play against the trained agent, switched on by editing main_nn.py.

What the repository does not contain: saved weights, tests, a command-line interface, or a thesis document. Of its 672 MB, almost all is a committed virtualenv and the Dungeon Crawl Stone Soup tileset (3,054 files) used for the sprites; the Python is about 1,500 lines, comments included.

9The commit history, as a lab notebook

Thirty-five commits between 17 February and 4 December 2017. Messages are paraphrased; dates and hashes are as recorded. The two marked commits are the ones that made the network learn.

Game engine

17 Feb – 31 Mar · 10 commits
  1. 02f379fcRepository created: a pygame event loop and a blank window
  2. 26cc6c72Physics: forces, mass, acceleration, a fixed 1/30 s timestep
  3. 2a8b6169Forces composed by vector addition; movement in all directions
  4. cdb44fc7Impulse model for forces; input handling split into UnitControl
  5. f2617c9eBullets fly; a mouse click fires toward the cursor
  6. d8d46c58Bullet collision detection; off-screen bullets removed
  7. d6bd914bMap grid of tiles; a hit becomes knockback
  8. 6524fc64Dungeon Crawl Stone Soup tileset added (3,054 files); arena set to 30 × 16 tiles
  9. 6a046754Health; friction and damage per tile type: lava, grass, ice
  10. 2d0289edRefactor; swords on the map; collected mass enters the knockback equation

Q-table

1 – 3 Apr · 4 commits
  1. bb71b6dbGym-shaped split into step and render; first Q-learning loop, not yet runnable
  2. a68c57b6Loop runs; 120 position states alias and learning fails
  3. 444992c4480 tile states, terrain reward, count-based exploration; still fails
  4. 1edadedbWorks: state collapsed to five lava bands; lr 0.1, γ 0.5, ε 0.5

A network from scratch

26 May – 17 Jun · 2 commits
  1. 636b2cdcBackprop derived by hand for a 2-4-1 net, one partial derivative per line; Q heat-map visualiser
  2. eee8e2beThe same network in matrix form, computed beside the scalar version to verify it

DQN

8 Jul – 19 Aug · 14 commits
  1. 58053333Rendering moved to DrawEngine so training runs headless; UnitState defines observation and reward
  2. d6827cd9scikit-learn MLPRegressor replaces the Q-table
  3. 9a33e10aLarger sklearn net, replay-style buffer, continuous distance features; not working
  4. ff7417e8Custom numpy network started alongside the sklearn one
  5. 077f9e86Custom network replaces sklearn; not working (gradient sign, ReLU derivative)
  6. 164f9d7dRow-major rewrite; cost plotting
  7. 32dac213Backprop correct: error sign, second-layer update restored, biases summed over the batch
  8. 7844ea13tanh activations, 100 hidden units; passes a sine regression test
  9. 1801654726-feature two-player state, differential reward, scripted opponent, episodes, 20-sample minibatch, learning-rate annealing
  10. 591d6117Configuration for a long run: 3,000,000 steps, hidden layers of 14 and 8
  11. 25bb5a16Breakthrough: batch size 1; four-layer net 26 → 40 → 40 → 40 → 4; the two nearest bullets as features
  12. 810a54275,898 cost-curve images from runs past 10.8 million steps removed
  13. 731942f0“Good config”: 26 → 20 → 15 → 10 → 4

Final tuning

10 – 11 Sep · 2 commits
  1. b497db29Sprites for the sword, the bullet and player 2
  2. ffa4186fPer-layer learning-rate multipliers 1 / 10 / 100 / 1000; net 26 → 10 → 6 → 6 → 4; lr 1e-4; Bellman bootstrap term added, γ 0.2

Write-up

4 Dec · 2 commits
  1. 328b6a31README written in the Bitbucket editor; a formatting edit follows as 57f20020

10Reading it back in 2026

A read-through of the final commit, nine years on. The first table is what the code does differently from the 2015 DeepMind paper it cites; the second is what a code review would flag.

Against the Nature DQN

TechniqueIn the paperHere
Experience replayuniform sampling from a buffer of past transitionsnone: the buffer is flushed after every sample (main_nn.py:120)
Target networka separate, periodically copied network for the bootstrapnone: the live network predicts s′ (main_nn.py:114)
Minibatch321; a batch of 20 was tried and dropped
Inputfour stacked frames into a convolutional net26 hand-picked features into a fully connected net
Losssquared error with the error clipped to ±1; rewards clipped to ±1squared error; rewards already in [−1, 1]
Explorationε from 1.0 to 0.1 over the first million framesgreedy probability 0.2 → 0.3 → 1.0; random actions held 5 frames; updates only on those frames

What a code review would flag

What the code doesEffectWhere
max_future = np.argmax(a1_predicted), then r + y*max_futurethe bootstrap term is the index of the best next action, 0 to 3, not its value; the target carries no value information beyond rmain_nn.py:115-116
tanh_deriv(a) called on activations, where a = tanh(z)the slope is computed as 1 − tanh(tanh z)² instead of 1 − tanh(z)²; the sign is right, the magnitude is notnn.py:67-70, 124-125
reward = reward(unit 1) − reward(unit 2); the learner is unit 2the learner is rewarded for the opponent's advantage; nothing in the loop flips the signgame_engine.py, unit_state.py:61-68
bullet x and y appended in raw pixels, 0 to 960, beside inputs scaled to about 1whenever a bullet is in flight the first tanh layer saturates on those inputsunit_state.py:45, 47
env.restart() on a win; no done flag in the targetthe transition that spans a restart is fitted like any other, and no terminal value is ever assignedmain_nn.py:60-62, 116

None of this changes what the repository shows: a physics engine, an environment, a tabular learner, a network derived by hand and a Q-learning loop around it, built and debugged in one year with the failures left in the history.

Source: bitbucket.org/donald_ovcharov/tu.diplomna · Bachelor's thesis, Computer and Software Engineering, Technical University of Sofia, 2017. The thesis document itself is presented in English at Visualising Game AI.

Written in 2026 from the repository's code and commit history. The live demo is a tabular re-illustration built for this page, not the 2017 program.

Donald Ovcharov, founder of 402. AI research and implementation since 2017; today it is Claude Code sessions, LangGraph pipelines and agents in production. Talk to us about AI →