A shimeji is a desktop mascot: a small chibi character that walks along the bottom of your screen, climbs the sides of windows and hangs off the top of them. Yuki Yamada of Group Finity wrote the first one in Java around 2009, and because the code was open, fans have dressed it ever since in art from whatever they're into.1
Left on its default settings it isn't polite. Its behaviour file gives SplitIntoTwo a weight of 50 while fewer than 50 mascots are on screen, and ThrowIEFromLeft a weight of 20, which picks up the window you're working in and throws it.2 The calm versions people pass around are mostly the same program with those weights set to 0.
pokemon-buddy3 is my version of one, for the terminal. It's a Claude Code mod where one Pokémon lives in the band above the prompt, and its "Sky" style draws PMDCollab's SpriteCollab sprites4, which follow the look of Pokémon Mystery Dungeon: Explorers of Sky. Here are Gengar, Lucario and Dialga, running on a browser port of the same engine.
Gengar2 hmeasured
Lucario2 hmeasured
Dialga2 hmeasuredSprites and portraits: PMDCollab SpriteCollab, CC BY-NC 4.0
At ×60, two simulated hours play in two minutes. It looks like three sprites wandering about, and there's more maths under it than I expected when I started: a state machine, a weighted roll that the pet's own tiredness bends, a personality seeded from its Pokédex number, and a clock that keeps overruling all three. That maths is what this post is about.
Take sleep. At its starting energy, my Gengar has a 1.7% chance of choosing sleep on each decision. Across twenty seeded two-hour runs with nobody typing, it spent 32.7% of the time asleep.5
Part of that gap is the timers. A nap's timer is set between 20 and 90 seconds and a walk's between 1 and 6, so a nap's timer averages 55 seconds against a walk's 3.5, about sixteen times longer. But when I froze the odds at their fresh values and ran the same twenty runs, sleep only reached 14.4%.
The rest comes from the odds moving. Walking drains energy, the sleep weight scales with , and after ten minutes without a session event a quiet multiplier of switches on.
The odds come from three small layers, and the first one only decides what's legal.
Legal Moves
The machine has seven states: idle, walk, turn, act, sleep, wake and watch. Most of its edges exist because the sprite sheet would look wrong without them. SpriteCollab draws Sit, Eat, Sleep and the other one-row clips facing the viewer only, so a Lucario walking right can't just sit down. It turns to face you first, and that turn is a clip of its own, never a mirrored row, because a flip would put tails and markings on the wrong side.4
Clips also come in different frame sizes (Gengar's Idle and Sit frames aren't the same box), and in some clips the feet move inside the frame. Every frame marks its ground point with a white pixel in its Shadow sheet, and the engine draws each frame offset so that pixel always lands on the same spot, which is why the feet stay put when one clip cuts to the next.3
Rotate's sheet doesn't label which way each frame faces, but every Rotate frame is pixel for pixel a Walk frame 0 from one of the eight directions: row , frame shows direction , so turning from Right (row 2) to face the viewer plays frames 1 and 2, DownRight then Down. The sheet gives each frame two sixtieths of a second, about 33 ms, and at that speed the turn looked like a snap, so the engine holds each Rotate frame for 70 ms.
Scroll sideways
PF.Magic's Petz team wrote down the target in 1997: "A pet with too much random behavior will appear disconnected. Alternatively, too little randomness creates a robotic and repetitive pet."6 The state machine doesn't touch that problem, because it won't say which legal move comes next. vscode-pets, Shimeji and SpriteWander each answer that differently.
vscode-pets picks uniformly from each state's possibleNextStates, then holds a static state for its holdTime in 100 ms ticks, so 50 ticks is .7 In a single draw from one list, uniform choice can't say "often walk, rarely sleep" unless you write walk into the list several times.
Shimeji-ee gives each behaviour an integer Frequency, and XmlConfiguration.buildBehavior draws Math.random() * total, subtracting weights until the remainder drops below zero. A JavaScript Condition decides who enters the draw, and NextBehaviorList Add="false" clears the pool so only the listed follow-ups can come next.2 SpriteWander, which wanders the same SpriteCollab sprites across a desktop, writes its odds into the Idle case: walk 50%, sleep 40%, pose 5%, eat 5%.8
For an unchanged pool and the same previous behaviour, those Frequency values stay fixed, and the configuration I read has no energy or tiredness term in it. So I built a comparison for this post. It runs the same Gengar clips and seed through 30 simulated minutes of a quiet session under three rules: uniform choice over the legal next states, a fixed-weight table like Shimeji's and SpriteWander's, and my full engine.
Thirty simulated minutes of a quiet session, Gengar, seed 42, as three timelines. Uniform: walk 3.1 %, idle 15.4 %, sleep 56.5 %, acts 24.2 %, 20 naps, longest repeat of one act 3. Fixed weights: walk 28.4 %, idle 25.4 %, sleep 31.1 %, acts 12.9 %, 9 naps, longest repeat of one act 3. This engine: walk 39.6 %, idle 25.3 %, sleep 23.8 %, acts 8.8 %, 9 naps, longest repeat of one act 2. Only the engine applies the quiet ×5 on sleep, from minute 10 of its own clock. Timelines are binned to about 3.3 s.
- walk
- 3.1 %
- idle
- 15.4 %
- sleep
- 56.5 %
- acts
- 24.2 %
- turn+wake
- 0.8 %
- most repeats
- laying ×3
- naps
- 20
- walk
- 28.4 %
- idle
- 25.4 %
- sleep
- 31.1 %
- acts
- 12.9 %
- turn+wake
- 2.2 %
- most repeats
- deep breath ×3
- naps
- 9
- walk
- 39.6 %
- idle
- 25.3 %
- sleep
- 23.8 %
- acts
- 8.8 %
- turn+wake
- 2.7 %
- most repeats
- eat ×2
- naps
- 9
In my engine, energy and restlessness change the weights between decisions.
Weighted Dice
The pet carries two drives. Energy drains while it walks or acts and comes back while it sleeps, sits or lies down, and restlessness builds while it stands about until one walk resets it. Both scale a fixed table of what the pet may do once an idle ends, each row with a base weight.3
const CANDIDATES: readonly Candidate[] = [
{ id: 'walk', weight: 40, needs: 'Walk' },
{ id: 'idle', weight: 18 },
{ id: 'sit', weight: 9, needs: 'Sit' },
{ id: 'lookUp', weight: 7, needs: 'LookUp' },
{ id: 'nod', weight: 5, needs: 'Nod' },
{ id: 'pose', weight: 4, needs: 'Pose' },
{ id: 'eat', weight: 4, needs: 'Eat' },
{ id: 'deepBreath', weight: 3, needs: 'DeepBreath' },
{ id: 'sleep', weight: 3, needs: 'Sleep' },
{ id: 'float', weight: 2, needs: 'Float' },
{ id: 'charge', weight: 2, needs: 'Charge' },
{ id: 'tumble', weight: 2, needs: 'Tumble' },
{ id: 'pull', weight: 1, needs: 'Pull' },
{ id: 'lostBalance', weight: 1, needs: 'LostBalance' },
{ id: 'laying', weight: 1, needs: 'Laying' },
]All fifteen weights sum to 102. To pick, choose() draws one uniform number below the total, then walks down the list subtracting each weight, and the first row that pushes it below zero wins:
so row comes up with probability . It's the same roulette wheel Shimeji spins over its Frequency values. A row whose needs clip is missing from the sprite sheet, or is only a CopyOf another clip, never enters the list, so shrinks with it and the survivors renormalise without a line of extra code.
While Claude works, one branch runs before the wheel. With probability equal to the pet's attachment, it either watches, if it already stands at the right edge, or turns right and starts one walk. More on that at the end.
Before the draw, weightOf() scales each base weight by what's going on inside the pet:
Here , , and are the species' restless, sleepiness, curiosity and temper traits, fixed for life. The session sets the other three, each by default: after ten minutes with no session event, and while Claude works halves sleep and doubles the curious acts. I borrowed the shape from utility scoring in games: rate every option against the current state, then draw in proportion.9
I set the drives to move at fixed rates per second. Walking costs energy and every act other than sitting or lying down costs , while sleep returns and sitting or lying down returns . Idling, turning, watching and waking don't touch energy at all. Restlessness climbs per second in every state except walking, sleeping and the non-resting acts, and one walk sets it back to zero. Both clamp to . A pet starts at , and each Wake adds .
Take , an empty memory and Claude idle. A fresh Gengar () walks on 53.3% of picks and sleeps on 1.7%. At the walk factor falls from to while the sit factor rises from to , so sit reaches 12.8% and sleep 7.9%. Switch on the quiet multiplier as well and sleep takes 29.9% of picks.
The pet also remembers its last three acts and naps. Walks and idles never enter that memory and aren't penalised by it. An act seen times in the window has its weight divided by before the draw, so a Gengar that just sat twice has its sit weight quartered. A few picks then unfold into short scripts in dispatch(), when the clips exist. Sleep opens with a yawn with probability (LookUp if the sheet has it, else DeepBreath), Charge leads to Attack half the time, and LostBalance ends in Cringe.
- walk4053.3
- idle1813.0
- sit95.9
- look up76.8
- nod54.9
- pose43.9
- eat42.9
- deep breath32.2
- sleep31.7
- float21.4
- charge20.6
- tumble21.4
- pull10.7
- lost balance10.7
- laying10.7
Left: base weight. Bars: probability of the pick, 0 to 100 %, fresh memory.

- P(walk)
- 53.3 %
- P(sleep)
- 1.7 %
- P(acts)
- 32.1 %
Drag energy to 0.2 and you'll see sit and sleep take walk's share. Then switch to Dialga.
Of the twenty clips the engine can play, Dialga's sheet has real art for six: Idle, Walk, Sleep, Rotate, Charge and Attack. Rotate drives turns, and Attack isn't a weighted row (Charge can chain into it, and poking the pet can pick it). So the table keeps walk, idle, sleep and charge, and 11 of the 15 rows drop.
Dialga didn't need any code of its own. With the same multiplier formulas, , an empty memory and Claude idle, a fresh Dialga walks on 74.3% of picks, and a tired one with the quiet multiplier on sleeps on 60.4%.
On the Clock
Gengar and Lucario keep the same rows and the same drive rates, but their profiles differ. Each profile holds seven numbers. Four of them, restless, sleepiness, curiosity and temper, enter weightOf(). Pace and stride set how the pet walks, and attachment sets how often it heads for the right edge while Claude works.
The profile comes from mulberry3210, a tiny 32-bit generator, seeded with . Because the seed is fixed per species, every Gengar draws the same traits, and two people running Gengar get the same pet. I haven't added a way to reroll one. Ettinger later wrote that mulberry32 can't produce about a third of all 32-bit values, and I accepted that for cosmetic traits.
Each trait is a draw inside a range: pace 0.8 to 1.3, stride 2.4 to 3.4 columns, sleepiness 0.5 to 2, curiosity 0.5 to 2, temper 0.2 to 1.6, attachment 0.3 to 1 and restless 0.6 to 1.6.
Then the sprite sheet nudges a few of them. A big Pokémon, one whose Idle frame is at least 48 px wide, gets stride and sleepiness . A Pokémon with a Float clip gets pace and stride . One with Pose or Nod has its temper halved, because it has gentler clips to express itself. I don't fetch any species data, so the seed and the sprite are all a pet gets.3
Stride and pace split one job so that changing pace never changes how far one walk cycle carries the sprite. Stride, in columns per cycle, is fixed per species. Pace only changes how fast the cycle plays, and the speed follows from both:
Here is one column in pixels and is the sum of the Walk frame delays. A lower pace stretches the cycle and the speed drops with it, so the legs and the distance travelled change together.
| Gengar #0094 | Lucario #0448 | Dialga #0483 | |
|---|---|---|---|
| pace | 0.70 | 0.94 | 0.97 |
| stride | 2.99 | 3.71 | 3.62 |
| sleepiness | 1.31 | 1.68 | 2.58 |
| curiosity | 1.35 | 1.33 | 1.77 |
| temper | 0.39 | 0.76 | 1.47 |
| attachment | 0.37 | 0.77 | 0.94 |
| restless | 1.42 | 1.55 | 1.42 |
Gengar and Lucario both have Float, so both carry the on pace. Gengar is the slow one because its own draw came out lower. Dialga's 2.58 is the big-sprite rule on top of a high draw.
Moving the sprite at is also the most expensive thing the pet does, because in a terminal every picture is an image sent through the terminal and every move of the band is a redraw right beside the prompt you're typing into. So the engine paints each picture once, as a PNG, keeps it, and sends it only when it differs from the one on show.
The sprite moves in whole sprite pixels inside a picture three columns wider than itself, and the band redraws only when the sprite leaves that picture, so a walk costs one redraw every three columns.
The engine runs on a single timer, and it wakes only when the frame on show is due to change or, during a walk, every 50 ms to move the sprite, never more often than every 33 ms. A pet asleep on a two-frame clip costs two timer wake-ups per loop. The one casualty is Attack, whose 17 ms frames are shorter than that floor, so Gengar's three in a row play as two.
- GengarNo. 0094

- Ghost
- Poison
- seed
- 94
- rows
- 15/15
- sprite
- full
- pace0.70
- stride2.99
- sleepiness1.31
- curiosity1.35
- temper0.39
- attachment0.37
- restless1.42
2 h quiet, 20 runswalk 35.6 %idle 22.9 %sleep 32.7 %acts 6.1 % - LucarioNo. 0448

- Fighting
- Steel
- seed
- 448
- rows
- 15/15
- sprite
- full
- pace0.94
- stride3.71
- sleepiness1.68
- curiosity1.33
- temper0.76
- attachment0.77
- restless1.55
2 h quiet, 20 runswalk 38.7 %idle 17.0 %sleep 35.5 %acts 4.9 % - DialgaNo. 0483

- Steel
- Dragon
- seed
- 483
- rows
- 4/15
- sprite
- exists
- pace0.97
- stride3.62
- sleepiness2.58
- curiosity1.77no clips to show it
- temper1.47
- attachment0.94
- restless1.42
2 h quiet, 20 runswalk 35.0 %idle 37.1 %sleep 24.8 %acts 0.1 %
Sprites and portraits: PMDCollab SpriteCollab, CC BY-NC 4.0
Each card's strip counts minutes, not picks, in a quiet and a busy session, and the traits above it don't change between them. If the choice probabilities and the actual mean time spent in each state stayed fixed, the long-run share of the clock would be
Sleep's long is the timer part of the gap in the opening, and it's why I budget a pet by its clock rather than its table. This engine doesn't hold its still, a walk restarts its timer when it turns at an edge, and in a busy session prompts cut walks short and wake naps, so I measured it instead.
I ran the engine headless on the real sprites for two simulated hours at 80 columns, twenty times each in a quiet session and in a busy one with a prompt every two minutes and a 60-second turn after each.5 One run isn't enough. A sleep share varies with a standard deviation of about 2 points from run to run, so the table gives means over twenty, and it leaves out turn and wake, which make up the rest of each row.
| sleep | walk | idle | act | watch | |
|---|---|---|---|---|---|
| Gengar, quiet | 32.7% | 35.6% | 22.9% | 6.1% | 0% |
| Gengar, busy | 10.6% | 34.2% | 22.7% | 7.7% | 22.1% |
| Lucario, quiet | 35.5% | 38.7% | 17.0% | 4.9% | 0% |
| Lucario, busy | 9.6% | 33.1% | 15.0% | 5.3% | 33.6% |
| Dialga, quiet | 24.8% | 35.0% | 37.1% | 0.1% | 0% |
| Dialga, busy | 6.8% | 30.7% | 30.4% | 0.1% | 29.7% |
I picked Sit, Eat, Pose, Nod, LookUp and the other act clips to carry the personality. Together they fill about 5 to 6% of Gengar's and Lucario's quiet two hours.
Dialga is the awkward row. Its sleepiness of 2.58 is the highest of the three, yet it sleeps least, 24.8% against 32.7% and 35.5%. Its naps last about as long as the others', just under a minute, but it took 32.5 of them per run to Gengar's 43. Part of that is how often it decides. Dialga's Idle loop runs 2.08 seconds to Gengar's 1.05, so it made 660 choices in two hours to Gengar's 926.
The rest is the memory. Per choice, Dialga napped barely more often than Gengar, 4.95% against 4.66%, despite twice the sleepiness. Walks and idles never enter the memory and Charge is Dialga's only act, so its last three entries were mostly naps: 2.02 of them on average at each choice, against Gengar's 0.49.
The rule I wrote to stop "sit, sit, sit" was quartering Dialga's sleep weight. With the memory penalty switched off, Dialga sleeps 36.1% of a quiet run, close to Lucario's 37.3% and above Gengar's 34.5%. The penalty costs Dialga eleven points of sleep, and Gengar and Lucario less than two each. Its curiosity of 1.77 still has no clips to show it at all.
Watching Claude Work
The busy rows have a watch column. While Claude works, the design wants the pet to "prefer idle facing down at the right end", watching you from the right edge of the band. With probability equal to its attachment, each choice made during a turn sends the pet walking right, and the walk case of finish() turns that walk into watching when it gets there:
else if (this.atRightEdge && this.isWorking)
this.enterTurn('down', () => this.enterWatch())That branch has to sit in the walk itself. choose() only runs after an idle, and without the branch a walk that reaches an edge turns round and walks back before any choice can see that the pet is standing where it should watch.


Sprites and portraits: PMDCollab SpriteCollab, CC BY-NC 4.0
Both bands above run Lucario with the same seed while Claude works the whole time, and the top one has that branch removed.
Once it's watching, the pet stays until Claude's turn ends or a reaction pulls it away, so the share it misses is the time it takes to get there. That grows with the width of the band and shrinks with attachment. In ten minutes of unbroken working time:
| Band width | Gengar | Lucario | Dialga |
|---|---|---|---|
| 40 and 60 columns | 96% | 100% | 99% |
| 80 columns | 89% | 98% | 98% |
| 120 columns | 74% | 96% | 92% |
| 160 columns | 60% | 81% | 81% |
Lucario and Dialga, at attachment 0.77 and 0.94, head for the edge on most choices. Gengar, at 0.37, mostly doesn't, and on a 160-column band it spends four of every ten working minutes still on its way to you.
Your Own Buddy
Building this was a lot of fun, mostly because every number in it is something you can watch. Change one weight and the pet on screen behaves differently within a minute, and the measurements kept showing me that the flourishes I'd picked as its personality filled a few percent of its day.
A few things are still open. The memory that stops "sit, sit, sit" also quarters the sleep weight of a Pokémon with almost nothing else to do, which is why Dialga, the sleepiest of the three, naps least. Every Gengar is the same Gengar, and there's no way to reroll one. And Dialga's curiosity of 1.77 still has no clips to spend itself on.
If you'd like one above your own prompt, type this in a Claude Code session:
/plugin install pokemon-buddy --marketplace Shavvimal/pokemon-buddyAnswer y to add the marketplace and pick a scope (the user scope gives every session a pet), and it turns up from the next session you start. You'll need a terminal that can draw pictures, which means Ghostty, kitty or iTerm2 3.5 or later, with allow-passthrough on inside tmux, and curl to fetch the sprites. The style setting in /config switches between the walking Sky pets in this post and the older Crystal-style loop.3
Footnotes
-
The Shimeji-ee README: "Shimeji was originally created by Yuki Yamada of Group Finity" and needs Java (chaoskagami/shimeji-ee); it also notes you can "turn off certain behaviors (hint: set frequency to 0.)". Tatsuya Kato reviewed the original, credited to Yuki Yamada, for Mado no Mori on 12 June 2009, and describes it climbing the screen edges, multiplying while you look away, and carrying windows off the desktop. ↩
-
LavenderSnek, ShimejiEE-cross-platform, a fork of Kilkakon's Shimeji-ee, itself descended from Group Finity's Shimeji. The draw is
XmlConfiguration.buildBehaviorand the weights live inconf/behaviors.xml. ↩ ↩2 -
Shavvimal, pokemon-buddy, a Claude Code mod. The engine is
hooks/sky.tsxand the design isdocs/sky-buddy-design.md. The live figures run a browser port of the same engine (release 0.4.3); the three-rules figure swaps in the other selection rules, and the watch figure removes the walk-at-the-edge branch for its top band. ↩ ↩2 ↩3 ↩4 ↩5 -
PMDCollab, SpriteCollab, CC BY-NC 4.0. Sprites: Gengar by CHUNSOFT and Emboarger, Lucario by CHUNSOFT and Emmuffin, Dialga by CHUNSOFT. Portraits: Gengar by CHUNSOFT; Lucario by Emmuffin (the collection's unused Special0 and Special1 portraits are by Deeshura and G〜); Dialga by G〜, after an earlier set by CamusZekeSirius. The CHUNSOFT sprites remain the property of Spike Chunsoft, Nintendo and The Pokémon Company. ↩ ↩2
-
The harness builds the engine's
Actorfrom the SpriteCollab sprite zips as the sprite server served them on 31 October 2026, gives it a virtual timer queue so two hours run in seconds, and delivers session events between ticks, the way the terminal does. It runs pokemon-buddy 0.4.3. The band is 80 columns, and the time shares, the frozen-odds runs, the Dialga counts and the memory-off runs are means over twenty runs with random seeds 1 to 20. The memory-off engine is the same code with the penalty removed. The frozen-odds run pins energy at 0.8, restlessness at 0 and the quiet flag off after every tick, and leaves the memory penalty live. The watch shares use seeds 1 to 5 and ten minutes with a turn running throughout. The pick percentages in Weighted Dice are computed fromweightOf(), not simulated. ↩ ↩2 -
Ben Resner, Adam Frank and Andrew Stern, "Issues of Autonomous Character Design", Proceedings of the 1997 Computer Game Developers Conference. The phrase "constrained randomness" appears a year earlier in their Catz abstract (Lifelike Computer Characters, 1996). ↩
-
Anthony Shaw, vscode-pets,
src/panel/states.tsandsrc/panel/basepettype.ts(chooseNextState). ↩ -
zerustu, SpriteWander,
SpriteWander/entity/Entity.cs. ↩ -
Rez Graham, "An Introduction to Utility Theory", Game AI Pro (2013), chapter 9. ↩
-
Tommy Ettinger, Mulberry32 (2017, public domain under CC0). The note about missing values is in the gist's comments. ↩

















