flyplays 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
1
+ # Adding a new game
2
+
3
+ This guide takes you from nothing to a fly brain playing your game, and filmed. There are three ways in,
4
+ from least work to most control:
5
+
6
+ | way | you write | the fly sees | good for |
7
+ |---|---|---|---|
8
+ | **1. Any web game by URL** | nothing (one command) | moving pixels, plus an optional target colour | trying a game in a minute |
9
+ | **2. Your own Python game** | one class, 6 methods | exactly what you tell it | new games, experiments |
10
+ | **3. A web game, properly** | a class plus a few lines of JavaScript | real positions read from the page | the best play on a web game |
11
+
12
+ Whatever the way, the fly is the same. Things in the game drive its visual neurons, and its descending neurons
13
+ press the keys. Nothing is trained.
14
+
15
+ ---
16
+
17
+ ## The 3 ideas
18
+
19
+ **1. Time only moves when flyplays says so.** Every brain step (20 ms of fly time by default), flyplays calls
20
+ `tick(dt)` and the game moves on by exactly `dt`. Between calls the game must stand still. The fly's thinking
21
+ takes as long as it takes, the game still plays at its own speed, and **a seed always replays the same game**.
22
+
23
+ **2. The fly sees `Thing`s, not pixels.** Each step, `sense()` returns what's around the player:
24
+
25
+ ```python
26
+ Thing(bearing=-0.3, size=0.12, kind="target", key="sugar")
27
+ ```
28
+
29
+ - `bearing`: radians from straight ahead, positive to the right and negative to the left. The fly sees about
30
+ ±165°; only straight behind is blind.
31
+ - `size`: how big it looks, in radians. For something of radius `r` at distance `d`, that's `2*atan(r/d)`.
32
+ The helper `self.thing_at(dx, ahead, radius, kind, key)` computes both from screen coordinates.
33
+ - `kind`: `"target"` is something to go to. `"creature"` is something that can come at you. `"shot"` is a
34
+ projectile.
35
+ - `key`: any stable id for the same object across steps. The fly needs it to notice something **growing**
36
+ (looming).
37
+
38
+ **3. The fly answers with a `Decision`.** It has three yes/no actions, from its own neurons:
39
+
40
+ - `d.left` and `d.right` come from the steering neurons (left minus right over `threshold` Hz).
41
+ - `d.escape` comes from the escape neurons (over `escape` Hz).
42
+
43
+ You also get `d.turn_hz` (right minus left, for analog turning) and `d.escape_side` (`"L"` or `"R"`, whichever
44
+ side scared it). You decide what each action does in your game.
45
+
46
+ ### What does "ahead" mean in my game?
47
+
48
+ | game view | the fly faces | "left" means | bearing of an object at screen (x, y) |
49
+ |---|---|---|---|
50
+ | top-down (the player moves around) | the player's heading | turn left | rotate (x−px, py−y) into the heading |
51
+ | top-down, fixed (it moves left/right) | up the screen | move left | `thing_at(x - px, py - y, r)` |
52
+ | side-scroller | right, into the level | **up** (its left eye looks at the top of the screen) | `thing_at(y - py, x - px, r)` (below the player = its right) |
53
+ | first person | into the screen | turn left | the angle off the view centre |
54
+
55
+ For a side-scroller, "turn left" equals "up". That's how the fly plays Flappy: a gap above drives its left eye,
56
+ and its left steering neurons flap.
57
+
58
+ ---
59
+
60
+ ## Way 1: any web game by URL
61
+
62
+ ```bash
63
+ pip install "flyplays[web]"
64
+ flyplays install-browser
65
+ flyplays web https://example.com/game \
66
+ --keys left=ArrowLeft,right=ArrowRight,escape=Space \
67
+ --player 0.5,0.85 --facing up --seconds 30 --clip try.mp4
68
+ ```
69
+
70
+ | option | meaning |
71
+ |---|---|
72
+ | `--keys` | which key each action presses. `escape` taps by default and `left`/`right` are held; add `:tap` or `:hold` to change that (`left=Space:tap`) |
73
+ | `--player x,y` | where the player is, as fractions of the screen (0,0 is the top left) |
74
+ | `--facing` | `up` for top-down games, `right` for side-scrollers |
75
+ | `--target-color` | the colour of things to go to (coins, food), e.g. `#ffd700` |
76
+ | `--start-keys` | keys pressed once to start the game (default `Space`) |
77
+ | `--size w,h` | the browser window |
78
+
79
+ Anything that moves on screen is a `creature`, and things in the target colour are `target`s. The page can't tell
80
+ flyplays when you lose, so the score is the seconds played. It's slower than the other ways because it takes a
81
+ screenshot every step; the clip still plays at real speed.
82
+
83
+ Some games don't fit: ones that keep time with Web Audio, workers or a server won't freeze between steps.
84
+
85
+ ## Way 2: your own Python game
86
+
87
+ The complete example is [`examples/03_my_first_game.py`](examples/03_my_first_game.py). Here it is step by step.
88
+
89
+ ```python
90
+ import math
91
+ import numpy as np
92
+ from flyplays import Decision, Game, State, play
93
+
94
+ class SugarRush(Game):
95
+ name = "sugar" # shown in results
96
+ title = "A REAL FLY BRAIN HUNTS FOR SUGAR" # the banner on clips
97
+ score_name = "sugar"
98
+
99
+ def start(self, seed): # 1. set up a fresh game; use the seed for all randomness
100
+ self.rng = np.random.default_rng(seed)
101
+ self.x, self.y, self.heading = 240, 240, 0.0
102
+ self.score, self.time_left = 0, 30.0
103
+ self.sugar = (100.0, 100.0)
104
+
105
+ def tick(self, dt): # 2. move the game on by exactly dt seconds
106
+ self.time_left -= dt
107
+ self.x += math.sin(self.heading) * 60 * dt
108
+ self.y -= math.cos(self.heading) * 60 * dt
109
+ if math.hypot(self.sugar[0] - self.x, self.sugar[1] - self.y) < 20:
110
+ self.score += 1
111
+ self.sugar = (float(self.rng.uniform(40, 440)), float(self.rng.uniform(40, 440)))
112
+ return State(alive=self.time_left > 0, score=self.score)
113
+
114
+ def sense(self): # 3. what the fly sees, from where it stands
115
+ dx, dy = self.sugar[0] - self.x, self.y - self.sugar[1]
116
+ right = dx * math.cos(self.heading) - dy * math.sin(self.heading)
117
+ ahead = dx * math.sin(self.heading) + dy * math.cos(self.heading)
118
+ return [self.thing_at(right, ahead, 10, kind="target", key="sugar")]
119
+
120
+ def act(self, d: Decision): # 4. the fly's decision -> your controls
121
+ self.heading += math.radians(2) * (d.right - d.left)
122
+
123
+ def frame(self): # 5. (for clips) the screen, an RGB numpy array
124
+ ...
125
+
126
+ print(play(SugarRush(), seconds=30))
127
+ ```
128
+
129
+ Run it from the command line by saving it as `sugar.py` and running `flyplays play sugar:SugarRush --clip sugar.mp4`
130
+ from the same folder.
131
+
132
+ **Tips**
133
+
134
+ - Put **all** randomness on `self.rng` from the seed. Then a good game can be replayed and filmed.
135
+ - Keep `act()` dumb. It maps actions to controls and nothing else. Don't add "if the sugar is left, go left":
136
+ that's your rule, not the fly's, and it makes the result meaningless.
137
+ - `Tap(refractory)` from `flyplays` gives you one press, then a rest (a flap, a jump, a shot).
138
+ - `frame()` can return any size. Clips scale it to fit.
139
+
140
+ ## Way 3: a web game, properly
141
+
142
+ Subclass `WebGame` and read real positions from the page with a little JavaScript. The fly then sees the exact
143
+ objects instead of guessing from pixels. That's how the built-in Flappy works; see
144
+ [`src/flyplays/games/flappy.py`](src/flyplays/games/flappy.py).
145
+
146
+ ```python
147
+ from flyplays.games.web import WebGame, Keys
148
+
149
+ class MyWebGame(WebGame):
150
+ url = "https://example.com/game/"
151
+ viewport = {"width": 800, "height": 600}
152
+
153
+ def ready(self, seed): # the page has loaded: start the game
154
+ self.page.keyboard.press("Space")
155
+ self.keys = Keys(self.page, {"left": "ArrowLeft", "right": "ArrowRight", "escape": "Space"})
156
+ return self.read()
157
+
158
+ def read(self): # called every step: pull what you need out of the page
159
+ return self.page.evaluate("""() => ({
160
+ alive: !window.gameOver, score: window.score,
161
+ px: player.x, py: player.y,
162
+ enemies: enemies.map(e => ({x: e.x, y: e.y, r: e.r, id: e.id}))
163
+ })""")
164
+
165
+ def sense(self):
166
+ s = self.state # what read() returned this step
167
+ return [self.thing_at(e["x"] - s["px"], s["py"] - e["y"], e["r"], "creature", e["id"])
168
+ for e in s["enemies"]]
169
+
170
+ def act(self, d):
171
+ self.keys(d, 0.02)
172
+ ```
173
+
174
+ flyplays freezes the page's time for you. Date, timers, `requestAnimationFrame` and CSS animations only move
175
+ when the game ticks. `Math.random` is seeded, and requests to other sites (analytics, ads) are blocked.
176
+
177
+ ---
178
+
179
+ ## Tuning (honestly)
180
+
181
+ The interface has a few knobs. The brain itself has none. These live in `flyplays.fly.DEFAULT`; set them per
182
+ game with `settings = {...}` on your class, or per run with `--set threshold=8`.
183
+
184
+ | setting | what it does |
185
+ |---|---|
186
+ | `target` | how hard a `target` drives LC10a |
187
+ | `chase`, `chase_gain`, `loom_gain`, `threat`, `threat_max` | how a `creature` drives LC10a, LPLC2 and LC4 |
188
+ | `shot_gain` | how a `shot` drives LPLC1 |
189
+ | `spread` | how far off-centre one eye gets all of a thing |
190
+ | `threshold` | steering Hz (left minus right) that counts as a press |
191
+ | `escape` | escape-neuron Hz that counts as `escape` |
192
+ | `rate_tau` | smoothing of the output rates |
193
+
194
+ Also try `--flies 8`, where 8 flies with their own noise vote, and `--dt 0.01` for faster brain steps.
195
+
196
+ **Check it's the brain.** Run your game three ways, like
197
+ [`examples/06_is_it_really_the_brain.py`](examples/06_is_it_really_the_brain.py):
198
+
199
+ - as is;
200
+ - blind: `sense()` returns `[]`;
201
+ - swapped: left and right crossed in `act()`.
202
+
203
+ If the fly doesn't beat blind, your mapping isn't reaching it. Check the bearing signs first.
204
+
205
+ ## Sharing your game
206
+
207
+ Add it to `src/flyplays/games/`, register it in `games/__init__.py`, and open a pull request with:
208
+
209
+ - a clip;
210
+ - the brain vs blind vs swapped numbers.
flyplays-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 alextitonis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include GUIDE.md
2
+ recursive-include examples *.py
3
+ recursive-include tests *.py
4
+ prune media
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: flyplays
3
+ Version: 0.1.0
4
+ Summary: A real fruit fly brain (the complete 166,700-neuron connectome) plays games: Flappy Bird, Doom, any web game, or your own. Nothing trained.
5
+ Author: alextitonis
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://flyaiworld.com
8
+ Project-URL: Source, https://github.com/alextitonis/fly.ai/tree/main/flyplays
9
+ Keywords: connectome,drosophila,fruit fly,games,spiking neural network,flappy bird,doom,neuroscience
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Games/Entertainment
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: flybrain>=0.1.0
20
+ Requires-Dist: numpy>=2.0
21
+ Requires-Dist: scipy>=1.13
22
+ Requires-Dist: pillow>=10.1
23
+ Requires-Dist: imageio-ffmpeg>=0.5
24
+ Provides-Extra: web
25
+ Requires-Dist: playwright>=1.45; extra == "web"
26
+ Provides-Extra: doom
27
+ Requires-Dist: vizdoom>=1.2; extra == "doom"
28
+ Provides-Extra: gpu
29
+ Requires-Dist: flybrain[gpu]; extra == "gpu"
30
+ Provides-Extra: all
31
+ Requires-Dist: playwright>=1.45; extra == "all"
32
+ Requires-Dist: vizdoom>=1.2; extra == "all"
33
+ Dynamic: license-file
34
+
35
+ # flyplays: a real fruit fly brain plays games
36
+
37
+ <p align="center">
38
+ <img src="https://raw.githubusercontent.com/alextitonis/fly.ai/main/flyplays/media/fruit.gif" width="260" alt="A fly brain plays Fruit Fall">
39
+ <img src="https://raw.githubusercontent.com/alextitonis/fly.ai/main/flyplays/media/doom.gif" width="260" alt="A fly brain plays Doom">
40
+ </p>
41
+
42
+ Scientists mapped every neuron and every connection in a fruit fly's brain: **166,700 neurons, 25.6 million
43
+ connections**. `flyplays` runs that map as a spiking network and lets it play games. It plays Flappy Bird, Doom,
44
+ a little game made for flies, any web game by its URL, or a game you write yourself.
45
+
46
+ **Nothing is trained and nothing is scripted.** The game shows up on the fly's own visual neurons. The fly's own
47
+ steering and escape neurons press the keys. Everything in between is the real wiring.
48
+
49
+ ```bash
50
+ pip install flyplays
51
+ flyplays play fruit --clip fruit.mp4
52
+ ```
53
+
54
+ The first run downloads the brain (about 260 MB, once). It runs on a normal laptop CPU, faster with a CUDA GPU
55
+ (`pip install "flyplays[gpu]"`).
56
+
57
+ ## Is it really the brain?
58
+
59
+ Fruit Fall, 10 games of 40 seconds each (seeds 101–110):
60
+
61
+ | the fly | fruit caught per game | survived the swatter |
62
+ |---|---|---|
63
+ | **as it is** | **6.4** | 6 / 10 |
64
+ | blind (it sees nothing) | 1.6 | 1 / 10 |
65
+ | left and right outputs crossed | 0.0 | 0 / 10 |
66
+
67
+ When it sees, it catches four times as much fruit as when it can't. Cross its left and right wires and it catches
68
+ nothing at all. The wiring itself is doing the work. Check it yourself: `python examples/06_is_it_really_the_brain.py`.
69
+
70
+ It's not a good gamer, and that's part of the fun. It usually gets a few pipes in Flappy Bird and a few kills in
71
+ Doom, then panics.
72
+
73
+ ## Games
74
+
75
+ ```bash
76
+ flyplays list
77
+ flyplays play fruit # Fruit Fall: catch fruit, dodge the swatter (pure Python)
78
+ flyplays play flappy --clip flappy.mp4 # Flappy Bird, on the open-source Floppy Bird site [web]
79
+ flyplays play doom --opt level=e1m1 # Freedoom through ViZDoom [doom]
80
+ flyplays play my_game:MyGame # your own game (see GUIDE.md)
81
+ flyplays web <url> --keys left=ArrowLeft,right=ArrowRight,escape=Space --player 0.5,0.85 # any web game [web]
82
+ ```
83
+
84
+ Extras: `pip install "flyplays[web]"`, then `flyplays install-browser` once, for web games. Use
85
+ `pip install "flyplays[doom]"` for Doom, or `"flyplays[all]"` for everything.
86
+
87
+ Useful options:
88
+
89
+ - `--seed 1-20` plays 20 different games. A seed always replays the same game.
90
+ - `--clip out.mp4` films it.
91
+ - `--flies 8` makes 8 flies vote. This is best on a GPU.
92
+ - `--set threshold=8` changes an interface setting.
93
+
94
+ ## Clips
95
+
96
+ `--clip` (or `play(..., clip="x.mp4")`) films a 9:16 video at 1080×1920 and 50 fps, ready for TikTok, Shorts
97
+ or Reels. The game is on top and the fly's brain is underneath:
98
+
99
+ - what each eye sees;
100
+ - the decision neurons;
101
+ - a live strip of spikes from 166,700 neurons.
102
+
103
+ The clip always plays at the game's real speed, even when the brain thinks slower than real time.
104
+
105
+ ## How the fly plays
106
+
107
+ ```
108
+ the game ──► Things (where, how big) ──► the fly's visual neurons ──► 166,700-neuron connectome ──► descending neurons ──► keys
109
+ ```
110
+
111
+ **What goes in.** The game describes what's around the player as `Thing`s, each with a bearing and a size. Each
112
+ one drives the fly's identified visual neurons on the side it's on. A fly sees almost all the way round.
113
+
114
+ | kind | neurons | in a real fly |
115
+ |---|---|---|
116
+ | `target` | LC10a | what a male fly chases |
117
+ | `creature` | LC10a, LPLC2, LC4 | chase it while it's in view, looming as it comes closer, fast looming when it's close |
118
+ | `shot` | LPLC1 | small objects coming at you |
119
+
120
+ **What comes out.** Two groups of descending neurons (brain → body) are read as firing rates:
121
+
122
+ - **steering**: DNa02, DNg111 and DNae002. Left minus right gives `left` or `right`.
123
+ - **escape**: DNp01 (the giant fiber), DNp02, DNp04 and DNp11. They give `escape`, which in a real fly is the
124
+ escape jump.
125
+
126
+ The game turns those three actions into its keys. In Flappy, "turn left" means up, which means flap. In Doom,
127
+ an escape burst pulls the trigger.
128
+
129
+ The only knobs are the interface: how strongly things drive the eye neurons, and how many spikes count as a press
130
+ (`flyplays.fly.DEFAULT`). The brain itself is never changed.
131
+
132
+ ## Your own game
133
+
134
+ A game is a class with six methods: `start`, `tick`, `sense`, `act`, `frame` and `close`. See **[GUIDE.md](GUIDE.md)**
135
+ for a step-by-step walk-through, and [`examples/`](examples) for working code:
136
+
137
+ | example | what it shows |
138
+ |---|---|
139
+ | `01_quickstart.py` | play a game in 3 lines |
140
+ | `02_film_a_clip.py` | play several seeds, film the best |
141
+ | `03_my_first_game.py` | a complete new game in about 50 lines |
142
+ | `04_any_web_game.py` | any web game, seen through its pixels |
143
+ | `05_doom.py` | Doom |
144
+ | `06_is_it_really_the_brain.py` | the brain vs blind vs swapped controls |
145
+
146
+ ```python
147
+ from flyplays import Game, State, play
148
+
149
+ class MyGame(Game):
150
+ def start(self, seed): ...
151
+ def tick(self, dt): ... # move the game on by dt seconds -> State(alive, score)
152
+ def sense(self): ... # what the fly sees -> [Thing(bearing, size, kind)]
153
+ def act(self, d): ... # d.left / d.right / d.escape -> your controls
154
+ def frame(self): ... # the screen as an RGB array (for clips)
155
+
156
+ print(play(MyGame(), seconds=30, clip="mine.mp4"))
157
+ ```
158
+
159
+ ## Where the brain comes from
160
+
161
+ The connectome is MaleCNS v1.0, the complete central nervous system of a male *Drosophila melanogaster*. The
162
+ spiking network comes from [`flybrain`](https://pypi.org/project/flybrain/). It's made by
163
+ [fly.ai](https://flyaiworld.com), where the same brain also trades from its own wallet.
164
+
165
+ Web games: only automate games you're allowed to automate, and never games that post to public leaderboards.
166
+ The built-in Flappy game plays Nebez Briefkani's open-source [Floppy Bird](https://github.com/nebez/floppybird).
167
+ Doom is [Freedoom](https://freedoom.github.io/) through [ViZDoom](https://github.com/Farama-Foundation/ViZDoom).
168
+
169
+ MIT license.
@@ -0,0 +1,135 @@
1
+ # flyplays: a real fruit fly brain plays games
2
+
3
+ <p align="center">
4
+ <img src="https://raw.githubusercontent.com/alextitonis/fly.ai/main/flyplays/media/fruit.gif" width="260" alt="A fly brain plays Fruit Fall">
5
+ <img src="https://raw.githubusercontent.com/alextitonis/fly.ai/main/flyplays/media/doom.gif" width="260" alt="A fly brain plays Doom">
6
+ </p>
7
+
8
+ Scientists mapped every neuron and every connection in a fruit fly's brain: **166,700 neurons, 25.6 million
9
+ connections**. `flyplays` runs that map as a spiking network and lets it play games. It plays Flappy Bird, Doom,
10
+ a little game made for flies, any web game by its URL, or a game you write yourself.
11
+
12
+ **Nothing is trained and nothing is scripted.** The game shows up on the fly's own visual neurons. The fly's own
13
+ steering and escape neurons press the keys. Everything in between is the real wiring.
14
+
15
+ ```bash
16
+ pip install flyplays
17
+ flyplays play fruit --clip fruit.mp4
18
+ ```
19
+
20
+ The first run downloads the brain (about 260 MB, once). It runs on a normal laptop CPU, faster with a CUDA GPU
21
+ (`pip install "flyplays[gpu]"`).
22
+
23
+ ## Is it really the brain?
24
+
25
+ Fruit Fall, 10 games of 40 seconds each (seeds 101–110):
26
+
27
+ | the fly | fruit caught per game | survived the swatter |
28
+ |---|---|---|
29
+ | **as it is** | **6.4** | 6 / 10 |
30
+ | blind (it sees nothing) | 1.6 | 1 / 10 |
31
+ | left and right outputs crossed | 0.0 | 0 / 10 |
32
+
33
+ When it sees, it catches four times as much fruit as when it can't. Cross its left and right wires and it catches
34
+ nothing at all. The wiring itself is doing the work. Check it yourself: `python examples/06_is_it_really_the_brain.py`.
35
+
36
+ It's not a good gamer, and that's part of the fun. It usually gets a few pipes in Flappy Bird and a few kills in
37
+ Doom, then panics.
38
+
39
+ ## Games
40
+
41
+ ```bash
42
+ flyplays list
43
+ flyplays play fruit # Fruit Fall: catch fruit, dodge the swatter (pure Python)
44
+ flyplays play flappy --clip flappy.mp4 # Flappy Bird, on the open-source Floppy Bird site [web]
45
+ flyplays play doom --opt level=e1m1 # Freedoom through ViZDoom [doom]
46
+ flyplays play my_game:MyGame # your own game (see GUIDE.md)
47
+ flyplays web <url> --keys left=ArrowLeft,right=ArrowRight,escape=Space --player 0.5,0.85 # any web game [web]
48
+ ```
49
+
50
+ Extras: `pip install "flyplays[web]"`, then `flyplays install-browser` once, for web games. Use
51
+ `pip install "flyplays[doom]"` for Doom, or `"flyplays[all]"` for everything.
52
+
53
+ Useful options:
54
+
55
+ - `--seed 1-20` plays 20 different games. A seed always replays the same game.
56
+ - `--clip out.mp4` films it.
57
+ - `--flies 8` makes 8 flies vote. This is best on a GPU.
58
+ - `--set threshold=8` changes an interface setting.
59
+
60
+ ## Clips
61
+
62
+ `--clip` (or `play(..., clip="x.mp4")`) films a 9:16 video at 1080×1920 and 50 fps, ready for TikTok, Shorts
63
+ or Reels. The game is on top and the fly's brain is underneath:
64
+
65
+ - what each eye sees;
66
+ - the decision neurons;
67
+ - a live strip of spikes from 166,700 neurons.
68
+
69
+ The clip always plays at the game's real speed, even when the brain thinks slower than real time.
70
+
71
+ ## How the fly plays
72
+
73
+ ```
74
+ the game ──► Things (where, how big) ──► the fly's visual neurons ──► 166,700-neuron connectome ──► descending neurons ──► keys
75
+ ```
76
+
77
+ **What goes in.** The game describes what's around the player as `Thing`s, each with a bearing and a size. Each
78
+ one drives the fly's identified visual neurons on the side it's on. A fly sees almost all the way round.
79
+
80
+ | kind | neurons | in a real fly |
81
+ |---|---|---|
82
+ | `target` | LC10a | what a male fly chases |
83
+ | `creature` | LC10a, LPLC2, LC4 | chase it while it's in view, looming as it comes closer, fast looming when it's close |
84
+ | `shot` | LPLC1 | small objects coming at you |
85
+
86
+ **What comes out.** Two groups of descending neurons (brain → body) are read as firing rates:
87
+
88
+ - **steering**: DNa02, DNg111 and DNae002. Left minus right gives `left` or `right`.
89
+ - **escape**: DNp01 (the giant fiber), DNp02, DNp04 and DNp11. They give `escape`, which in a real fly is the
90
+ escape jump.
91
+
92
+ The game turns those three actions into its keys. In Flappy, "turn left" means up, which means flap. In Doom,
93
+ an escape burst pulls the trigger.
94
+
95
+ The only knobs are the interface: how strongly things drive the eye neurons, and how many spikes count as a press
96
+ (`flyplays.fly.DEFAULT`). The brain itself is never changed.
97
+
98
+ ## Your own game
99
+
100
+ A game is a class with six methods: `start`, `tick`, `sense`, `act`, `frame` and `close`. See **[GUIDE.md](GUIDE.md)**
101
+ for a step-by-step walk-through, and [`examples/`](examples) for working code:
102
+
103
+ | example | what it shows |
104
+ |---|---|
105
+ | `01_quickstart.py` | play a game in 3 lines |
106
+ | `02_film_a_clip.py` | play several seeds, film the best |
107
+ | `03_my_first_game.py` | a complete new game in about 50 lines |
108
+ | `04_any_web_game.py` | any web game, seen through its pixels |
109
+ | `05_doom.py` | Doom |
110
+ | `06_is_it_really_the_brain.py` | the brain vs blind vs swapped controls |
111
+
112
+ ```python
113
+ from flyplays import Game, State, play
114
+
115
+ class MyGame(Game):
116
+ def start(self, seed): ...
117
+ def tick(self, dt): ... # move the game on by dt seconds -> State(alive, score)
118
+ def sense(self): ... # what the fly sees -> [Thing(bearing, size, kind)]
119
+ def act(self, d): ... # d.left / d.right / d.escape -> your controls
120
+ def frame(self): ... # the screen as an RGB array (for clips)
121
+
122
+ print(play(MyGame(), seconds=30, clip="mine.mp4"))
123
+ ```
124
+
125
+ ## Where the brain comes from
126
+
127
+ The connectome is MaleCNS v1.0, the complete central nervous system of a male *Drosophila melanogaster*. The
128
+ spiking network comes from [`flybrain`](https://pypi.org/project/flybrain/). It's made by
129
+ [fly.ai](https://flyaiworld.com), where the same brain also trades from its own wallet.
130
+
131
+ Web games: only automate games you're allowed to automate, and never games that post to public leaderboards.
132
+ The built-in Flappy game plays Nebez Briefkani's open-source [Floppy Bird](https://github.com/nebez/floppybird).
133
+ Doom is [Freedoom](https://freedoom.github.io/) through [ViZDoom](https://github.com/Farama-Foundation/ViZDoom).
134
+
135
+ MIT license.
@@ -0,0 +1,12 @@
1
+ """The shortest way in: a real fruit fly brain plays Fruit Fall for 30 seconds.
2
+
3
+ pip install flyplays
4
+ python examples/01_quickstart.py
5
+
6
+ The first run downloads the brain (~260 MB, once). No browser, no GPU needed.
7
+ """
8
+ from flyplays import play
9
+ from flyplays.games.fruit import FruitFall
10
+
11
+ result = play(FruitFall(), seed=1, seconds=30)
12
+ print(result) # fruit seed 1: score 9 in 30.0 s (still going; left ..., right ..., escape ...)
@@ -0,0 +1,21 @@
1
+ """Film the fly playing: a 9:16 video (1080x1920, 50 fps) with the game on top and the fly's neurons
2
+ underneath - the eye neurons, the decision neurons and a live strip of spikes. Ready for TikTok / Shorts.
3
+
4
+ python examples/02_film_a_clip.py
5
+
6
+ Play a few seeds and keep the best one: every seed is a different game.
7
+ """
8
+ from flyplays import Fly, play
9
+ from flyplays.games.fruit import FruitFall
10
+
11
+ fly = Fly() # load the brain once, reuse it for every game
12
+ best = None
13
+ for seed in range(1, 6):
14
+ r = play(FruitFall(), fly, seed=seed, seconds=25)
15
+ print(r)
16
+ if best is None or r.score > best.score:
17
+ best = r
18
+
19
+ # a seed replays the same game, so film the best one
20
+ clip = play(FruitFall(), fly, seed=best.seed, seconds=25, clip=f"fruit-seed{best.seed}.mp4")
21
+ print("filmed:", clip.clip)
@@ -0,0 +1,70 @@
1
+ """Your own game in ~50 lines: Sugar Rush. Sugar cubes appear around the fly; it has to walk to them.
2
+
3
+ python examples/03_my_first_game.py # plays 3 games
4
+
5
+ Copied to a file named sugar.py, the command line plays it too:
6
+
7
+ flyplays play sugar:SugarRush --clip sugar.mp4
8
+
9
+ A game only has to: start, move time on (tick), say what the fly sees (sense), and turn the fly's decision
10
+ into moves (act). frame() is only needed for clips. See GUIDE.md for the walk-through.
11
+ """
12
+ import math
13
+
14
+ import numpy as np
15
+
16
+ from flyplays import Decision, Fly, Game, State, play
17
+
18
+ W, H = 480, 480
19
+
20
+
21
+ class SugarRush(Game):
22
+ name = "sugar"
23
+ title = "A REAL FLY BRAIN HUNTS FOR SUGAR"
24
+ score_name = "sugar"
25
+
26
+ def start(self, seed):
27
+ self.rng = np.random.default_rng(seed)
28
+ self.x, self.y, self.heading = W / 2, H / 2, 0.0 # heading: radians, 0 = up the screen
29
+ self.score, self.time_left = 0, 30.0
30
+ self.sugar = self._new_sugar()
31
+
32
+ def _new_sugar(self):
33
+ return (float(self.rng.uniform(40, W - 40)), float(self.rng.uniform(40, H - 40)))
34
+
35
+ def tick(self, dt):
36
+ self.time_left -= dt
37
+ self.x = float(np.clip(self.x + math.sin(self.heading) * 60 * dt, 10, W - 10)) # it always walks forward
38
+ self.y = float(np.clip(self.y - math.cos(self.heading) * 60 * dt, 10, H - 10))
39
+ if math.hypot(self.sugar[0] - self.x, self.sugar[1] - self.y) < 20:
40
+ self.score += 1
41
+ self.sugar = self._new_sugar()
42
+ return State(alive=self.time_left > 0, score=self.score)
43
+
44
+ def sense(self):
45
+ # where the sugar is, seen from the fly: rotate into the fly's own frame (its heading = "ahead")
46
+ dx, dy = self.sugar[0] - self.x, self.y - self.sugar[1]
47
+ right = dx * math.cos(self.heading) - dy * math.sin(self.heading)
48
+ ahead = dx * math.sin(self.heading) + dy * math.cos(self.heading)
49
+ return [self.thing_at(right, ahead, 10, kind="target", key="sugar")]
50
+
51
+ def act(self, d: Decision):
52
+ # the steering neurons turn the fly: 2 degrees per step, the way they said
53
+ self.heading += math.radians(2) * ((1 if d.right else 0) - (1 if d.left else 0))
54
+
55
+ def frame(self):
56
+ from PIL import Image, ImageDraw
57
+ img = Image.new("RGB", (W, H), (245, 240, 225))
58
+ d = ImageDraw.Draw(img)
59
+ sx, sy = self.sugar
60
+ d.rectangle([sx - 10, sy - 10, sx + 10, sy + 10], fill=(255, 255, 255), outline=(180, 170, 150), width=2)
61
+ hx, hy = self.x + math.sin(self.heading) * 16, self.y - math.cos(self.heading) * 16
62
+ d.ellipse([self.x - 10, self.y - 10, self.x + 10, self.y + 10], fill=(40, 40, 40))
63
+ d.ellipse([hx - 6, hy - 6, hx + 6, hy + 6], fill=(200, 30, 30))
64
+ return np.asarray(img)
65
+
66
+
67
+ if __name__ == "__main__":
68
+ fly = Fly()
69
+ for seed in (1, 2, 3):
70
+ print(play(SugarRush(), fly, seed=seed, seconds=30))
@@ -0,0 +1,26 @@
1
+ """Any web game, with no code for the game itself: the fly sees only the pixels.
2
+
3
+ pip install "flyplays[web]" && flyplays install-browser
4
+ python examples/04_any_web_game.py
5
+
6
+ Same thing from the command line:
7
+
8
+ flyplays web https://nebezb.com/floppybird/ --keys left=Space:tap --player 0.3,0.45 --facing right \
9
+ --size 480,720 --seconds 20 --clip web.mp4
10
+
11
+ What the fly sees: anything that moves is a "creature" (it looms as it comes closer); with target_color, things
12
+ of that colour are "targets" to go to. Tell it where the player is (fractions of the screen) and which way it
13
+ faces: "up" for top-down games, "right" for side-scrollers. Map the fly's three actions to the game's keys.
14
+
15
+ It takes a screenshot every brain step, so it plays slower than real time - the clip still plays at real speed.
16
+ Only use games you're allowed to automate, and never games that post to public leaderboards.
17
+ """
18
+ from flyplays import play
19
+ from flyplays.games.web import PixelGame
20
+
21
+ game = PixelGame(
22
+ "https://nebezb.com/floppybird/", # Floppy Bird, Nebez Briefkani's open-source Flappy remake
23
+ keys={"left": "Space:tap"}, # side view: "turn left" = up = flap
24
+ player=(0.3, 0.45), facing="right", viewport=(480, 720),
25
+ )
26
+ print(play(game, seed=1, seconds=20, clip="web.mp4"))