Skip to content
 
 

Latest commit

 

History

266 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TamaPoke

Flash in browser MakerWorld Board Firmware Code Languages Stars

A gen-1-Pokémon-inspired tamagotchi for the Waveshare ESP32-S3-Touch-AMOLED-1.75 (round 466×466 AMOLED, CO5300 driver over QSPI, CST9217 touch over I2C). Raise any of the 1025, evolve it, train it and complete them all (shinies included).

🙏 This is a fork of socquique/TamaPoke by Quique Tortosa

Quique wrote the original TamaPoke — the firmware, the sprite pipeline, the six-language UI, the web installer, the whole thing. This fork builds on that work; it did not start it. If you like this, go star the original.

Original: MIT © 2026 Quique Tortosa. Changes in this fork are MIT on the same terms.

Personal, non-commercial fan project. Code is MIT; the sprites are from PMD SpriteCollab (CC BY-NC, Pokémon © Nintendo/Game Freak), and the 3D case is CC BY-NC-SA. See License and Credits.

🔴 3D-printed Pokéball case + print profiles → on MakerWorld · flash it in your browser → web installer

Screens

All shots are straight off the 466x466 round panel, rendered headlessly by the emulator (tools/emu/tamapoke-emu --shot <name>), so they are exactly what the hardware draws.

Starting out

Pick a region ...then its starter Johto's three

A new game asks which region you are playing before it asks which creature you want. The choice sets both: you pick from that region's three starters, and it becomes where your eggs come from afterwards (changeable later on the egg's region pill). Existing saves never see this screen -- it only appears when the Pokedex is empty.

Raising one

Your creature Its egg Moves it knows

The egg carries a region pill — pick which of the nine regions it hatches from, or ALL for the mixed pool. Switching keeps the rarity it was granted and remembers each region's answer, so it cannot be flipped to farm a legendary. Regions with no sprite pack installed are skipped rather than offered.

Battling

The fight Choosing a move Choosing a team Winning

Turn- and move-based, with the real type chart, ailments and STAB. Moves sit in a 2x2 grid with each one's type and remaining uses. Real Game Boy battle music plays throughout.

Nine gym ladders

Pick a ladder Johto's gyms LAN battle

Kanto, Johto, Hoenn, Sinnoh, Unova, Kalos, Alola, Galar and Paldea each have eight leaders, an Elite 4 and a champion, on easy and hard. Four of the nine are the games' own teams, checked against the pokecrystal, pokeemerald and pokeplatinum disassemblies -- 0 differences across all 39 verified trainers, re-checkable with tools/verify_rosters.py.

Five ladders are NOT verified, and say so. pret's work stops at the DS generation, so Unova, Kalos, Alola, Galar and Paldea are written from knowledge rather than from the games' own tables; verify_rosters.py prints each of them as NOT VERIFIED rather than letting a clean run imply otherwise. Treat their levels as approximate.

Three of those five had to pick a version, because a ladder is one fixed list and cannot represent a choice. Unova follows Black 2 / White 2, which unlike Black/White has no version- or starter-dependent leaders. Galar follows Sword, which settles both of its exclusive slots -- Bea over Allister, Gordie over Melony; the two leaders Sword does not use fill Elite 4 seats instead, since Galar has no Elite 4 of its own. Paldea's Larry is the Normal gym leader here and not also the Elite 4's Flying member -- canon, but it reads as a bug when one name appears twice in the same ladder, so Penny takes that chair.

A ladder is only offered when its sprite pack is on the card, exactly as the Pokedex and the egg pool already were. Without that gate a leader can open a fight with a bare dex number where a creature should be.

Sinnoh follows Platinum, where Fantina is the third gym rather than Diamond/Pearl's fifth; the level ramp only runs 14/22/26/32/37/41/44/50 that way. Same reasoning that makes Hoenn Emerald throughout.

Exploring

Choose where to hunt A wild encounter

Wild creatures are met by exploring rather than only in gyms. The < REGION > selector picks where the next encounter comes from, independently of your egg's region, so changing hunting grounds never rerolls or moves the egg. Regions whose sprite pack is not installed are skipped. Weaken one and throw a ball to keep it; a caught creature goes to the party or the box, and does not fill the Pokédex until you actually raise it.

Collecting

Pick a region Kanto Johto
Trainer card Johto badges The box

Status

Running on hardware. Implemented: 1025 species + shinies animated from microSD, full life cycle (egg by rarity → evolution → farewell/release/runaway, each gated behind a decision dialog), bred-Pokédex with gallery, turn-based trainer, wild and LAN battles, wild capture and a shared bag, battle stats (IVs + training), retention hooks (streak / bond / medals / name), biome + real-time backgrounds, four minigames, animated bath, RTC with offline progression, battery (AXP2101) and PWR button, anti-burn-in dimming with a brightness setting, synthesised per-species chirps, sound (ES8311), 6 UI languages (English default), starter choice on first run, and a one-click web installer.

All nine regions are in the dex, and all nine have a gym ladder and a sprite pack. A region whose pack is not on the card shows as NEEDS PACK and stays out of the egg pool, the gym chooser and the Pokedex grid until it is installed. A board with no microSD at all is unaffected: every region stays available, exactly as before.

Pending: 3D case, soak test. See Roadmap.

Game manual (the actual numbers)

A quick reference to how the game really works (values straight from the code).

Time & leveling

  • 1 real minute = 1 in-game minute. Your Pokémon gains +1 level every hour of real time. Leveling is purely time-based — caring well doesn't speed it up, but neglect delays evolution.
  • Level caps at 100, reached at 4 days 3 hours. Farewell is only offered at 3 days (level 73) — not a deadline: your Pokémon is still growing, and declining it to reach 100 is a real choice. Declining re-offers a day later.
  • It keeps aging while powered off (the RTC runs), catching up to 2 weeks max.

The four stats (0–100)

Needs: FOOD, JOY, ENE (energy), HYG (hygiene). Start 80 / 80 / 80 / 100. While awake, per minute:

Stat Drain/min Notes
FOOD −2
ENE −1 −1 extra if overweight (weight > 50 → sluggish)
HYG −1 −4 more per poop on screen (max 3 poops)
JOY −1 −2 extra if FOOD < 30, −2 extra if HYG < 30
  • ~15 %/min chance to poop (only if FOOD > 40). Poops tank hygiene fast.
  • Care slip-up = letting any stat hit ≤ 10 (60-min cooldown so it counts once). Each slip-up delays evolution by 1 level and cools the bond by 1.

Actions

  • 🍎 Berry (3 flavors): +25 FOOD. Each species has a hidden favorite flavor → +35 FOOD, +10 JOY, ♥, bond, and it gets revealed.
  • 🍬 Candy: +10 FOOD, +12 JOY, but +12 weight (fattening).
  • ⚽ Play / minigame: +5 JOY, plus 2 per rally (max +35), −ENE, burns weight. Pure happiness — it trains nothing, so playing is never a stat grind. Leaving early keeps what you earned.
  • 🎯 Reaction test: a target appears, tap it before it shrinks away. Trains SPEED; the window tightens as you go.
  • 🥊 Training bag: trains STRENGTH (~4 hits = 1 pt, cap +18/session), tires it.
  • 🧠 Memory: four pads flash a sequence and you repeat it; one more step every round, and your score is the longest sequence you got back. +4 JOY per step (max +40) and bond. It trains no stat at all, deliberately — all three trainable stats already have a game, so a fourth would just be another grind. It also costs almost no energy and burns no weight, which makes it the one to reach for when your creature is too worn out for the others.
  • 🫧 Bath: clears poops, HYG → 100.
  • 👆 Pet it: +5 JOY + bond.
  • 🌙 Sleep: rest — ENE +6/min, needs drain ~4× slower with floors (FOOD 30 / JOY 35 / HYG 45). No poops, no slip-ups, can't run away while asleep. Screen off + between midnight and 06:00 puts it to sleep, and it stays asleep until you turn the screen back on -- it wakes when you do, not at a fixed hour. Re-checked every minute, so a device put down at 23:00 nods off when midnight comes. The evening is deliberately outside the window -- a six-hour night is the only part of the day you are not expected to be around. The light button beats both: a creature you sent to bed stays there.

Eggs & who you get (spawn odds)

  • First ever pet: you pick a starter — Bulbasaur / Charmander / Squirtle.
  • Hatch the egg: tap it 3× (or wait — it hatches on its own).
  • Every later egg rolls a rarity tier (over the ~79 base forms that come from eggs):
Tier Base chance After a proper goodbye # species
✨ Legendary ~3 %* ~10 % 5
🔵 Rare ~27 % ~45 % 27
⚪ Common the rest the rest 47

* Legendaries only start appearing once you've registered ≥ 25 Pokémon.

  • A daily streak and high bond push rare/legendary odds higher.
  • A clean goodbye blesses the next egg; a run-away curses it (forces Common).
  • Within a tier it favors species whose evolution line you haven't finished (so all 1025 are completable).
  • Shiny: base 1 / 48 (→ 1 / 24 right after a goodbye), improved by streak/bond down to a best of 1 / 8. Tracked separately in the dex.
  • Every hatch rolls unique IVs (see below) — no two are identical.

Evolution

Cross-generation evolutions are linked. When the dex reached 386 the targets arrived but nothing connected them, so six Kanto species could not evolve into creatures that were sitting right there in the Pokedex: GOLBAT -> CROBAT, ONIX -> STEELIX, CHANSEY -> BLISSEY, SEADRA -> KINGDRA, SCYTHER -> SCIZOR and PORYGON -> PORYGON2. Those six now evolve (friendship pairs at 25, trade pairs at 40), and the six targets stopped hatching straight from eggs -- rarity is derived from being somebody's evolution, so they became evolution-only the way every other evolved form already was.

gen_dex_data.py --link is the rule rather than a one-off edit: it fills in any evolution whose target has since joined the table, and only ever touches rows whose value is 0, so it cannot retune an evolution anybody already has. Sinnoh brings ELECTIVIRE, MAGMORTAR and RHYPERIOR waiting on exactly the same thing.

  • Triggers when level ≥ its evolution level (16 for most base forms; ~30 for stone-style, ~40 for trade-style) and every stat ≥ 40 at that moment.
  • Never automatic — a button appears and you tap to witness it (with a flicker between the old and new form). Each slip-up delays it by 1 level.
  • You can decline ("keep form"); it re-offers at the next level.
  • Eevee branches toward whichever of its eight evolutions you're still missing — Vaporeon, Jolteon, Flareon, Espeon, Umbreon, Leafeon, Glaceon and Sylveon. None of the eight hatches from an egg any more: the only way to a Sylveon is to raise an Eevee. A branch is offered only if that region's sprite pack is on the card, so evolving can never hand you a creature the device cannot draw.

Your party

  • A farewell or a release doesn't end the relationship any more — the creature joins your party (6 slots), keeping its species, nickname, shiny status, IVs, training, moves, the level it reached and its whole care state. It is frozen while stored: it does not age or train until you raise it again.
  • The Box holds 36 Pokemon across six pages. An existing 18-slot Box keeps every creature in place and gains 18 empty slots after updating.
  • Choosing who you raise. Tap a party slot and press MAKE ACTIVE. It is a true exchange — the creature on the main screen takes the slot the newcomer vacates — so it needs no free slot and nothing is ever lost. Both keep their full care state, so swapping away and back returns the creature exactly as it was rather than a blanked copy. The one asymmetric case is an egg: it has nothing to bank, so its slot simply empties.
  • Moving one into storage. Tap a party slot and press TO BOX, then tap the destination box slot. If that slot is occupied, the two creatures exchange. RELEASE does not move it to storage; it permanently removes it after a second confirmation.
  • A runaway does not join. It's the one ending with a cost, and letting a neglected pet come back on the team would remove it. Neither does an early retire — see "Retiring a creature early" below.
  • Letting one go for good. Move a party member to the box, open its sheet, then press RELEASE. It removes that creature permanently and asks first. The creature does not fall through into the box — this is the one way to free a slot without something taking its place. A box slot now opens the same sheet rather than jumping straight into the party, and TO PARTY does that.
  • With a full party you're taken straight to the party screen to pick who the newcomer replaces — or to let it go. Nothing is ever overwritten silently.

Wild encounters, catching and the bag

The BAG holds items. Pokemon storage is the BOX: a successful catch fills the first free party slot, then the first free box slot once the party is full.

EXPLORE is its own stop immediately beside the pet on the horizontal axis. Its large < REGION > selector moves through every installed sprite pack in either direction, independently of the region chosen for the current egg. The screen also offers normal/hard difficulty and one large action; wild encounters no longer hide at the bottom of the gym ladder.

The encounter rolls a rarity tier, then a species of that tier that is in an installed region pack and has art — a caught creature is kept forever, so one that could only ever draw as a dex number would be a permanent defect.

Tier Chance
✨ Legendary 1 %
🔵 Rare 7 %
⚪ Evolved 22 %
⚪ Common the rest
  • Level: within 5 either side of yours on normal; the whole 1–100 ladder on hard.
  • Shiny: exactly 1/4096 per encounter, flooring every IV at 20 without capping one that already rolled higher.
  • Ordinary IVs roll 8–31, independently.

Catching. Pick a ball from the battle BAG. It spends your turn either way, so throwing is never free.

chance = base × hpFactor × statusFactor × ball / 10000, capped at 95 % — a ball is always a gamble. The Master Ball is the one exception and never rolls.

base Common 30 · Evolved 24 · Rare 18 · Legendary 5
hpFactor 100 at full health rising to 180 just before fainting
statusFactor 130 with any ailment, 100 without
ball Poké 100 · Great 150 · Ultra 200 · Master guaranteed

Getting away. Running from a wild fight is a roll, not a certainty: 90 % against something at or below your level, scaled down by the level ratio above it with a 10 % floor. Failing costs the turn.

The wild creature can leave too, and its curve is a tent rather than a slope — it stops running once it is nearly beaten, which is what makes weakening it the right way to catch it instead of a race:

Its HP Chance to flee
above 40 % 0 %
40 % 10 %
20 % 20 % (the peak)
10 % and below 10 %

It spends its action fleeing rather than fleeing and attacking, and a fight it leaves pays nothing.

Rewards. A win grants 1 weighted item on normal and 2 on hard, plus one independent 30 % roll for a bonus. Each draw excludes every earlier one, so a settlement can never list the same item twice.

Item Weight What it does
Poké Ball 30 catch ×100
Potion 20 +20 HP
Great Ball 15 catch ×150
Super Potion / Full Heal 10 each +50 HP · clears an ailment
Protein / Iron / Carbos 8 each +5 STRENGTH / DEFENCE / SPEED
Ultra Ball 6 catch ×200
Hyper Potion 4 +120 HP
Master Ball 1 catches without fail

A new save starts with 5 Poké Balls and 3 Potions. Stacks cap at 99.

The BAG menu row is the field bag, and it only offers the vitamins: the pet has no persistent HP or ailment outside a fight, so a potion in the field would have nothing to act on. Vitamins respect the same IV-bound training ceiling (trMaxFor) as the punching bag and a gym reward — an item that could push past it would make the IV roll decorative.

A caught creature takes a party slot, then a box slot, then asks — the same path a farewell already uses. It is not added to the Pokédex on capture: registration means "you raised this", so it happens when you actually make it the one you raise.

The three endings (you choose & witness each — none auto-fire)

  • 💛 Farewell — when it's a final form that has lived 3 days. A button appears; triggering it blesses your next egg. You can postpone ("stay together", re-offered in a day). The good ending.
  • 💔 Run-away — if you let all four stats sit at 0 for a full hour. A single act of care cancels it. It curses the next egg (forces Common). The sad ending.
  • 👋 Release — long-press the creature to let it go on your terms (neutral).

After any ending, a new egg appears.

Bonds, streaks, medals, Pokédex

  • Streak (player-wide, survives across pets): first care each real day; milestones at 3 / 7 / 30 / 100 days; skipping a day breaks it.
  • Bond (per pet, resets on hatch): grows with affection (cap +20/day), cools on neglect. Both streak & bond improve egg/shiny odds — and the IVs of your next pet.
  • 8 medals (Lv10/25/50, favorite berry found, 7-day streak, max bond, final form, "fit" = weight 0 & no slip-ups), per-pet + a global counter.
  • Pokédex: raising a species registers it; 809 + shinies to complete. Browsed one region at a time — swipe vertically between regions, horizontally to page within it, so nothing is more than ten pages from the front. Tap the line under the region name to filter: ALL / RAISED / CAUGHT / SHINY. RAISED and CAUGHT are genuinely different answers — the dex records what you have raised as your own creature, so a wild capture sitting in the box has never touched it. CAUGHT reads your party, your box and the creature on the panel, which is why it can show something the dex has never seen. Opening a registered entry plays that species' chirp; a silhouette stays silent, since the sound would give away what the "???" is hiding.
  • Region: the pill under a waiting egg picks which generation it comes from — Kanto / Johto / Hoenn / Sinnoh / Unova / Kalos / Alola / All. A first egg gives that region's starter. A handful of species have no sprite art anywhere (13 of Unova's 156, and Pyroar); they keep their dex numbers but never hatch, since the egg would give a creature that could only ever draw as a number. A region whose sprite pack is not on the card shows as locked and is kept out of the egg pool, so a partial sprite install is a supported state. A pack sent from the web installer is picked up as it lands — the region unlocks, and the Pokedex shows its new thumbnails, without a reboot.

Personality

Every creature has a temperament, shown on the profile page of its card: BOLD / STURDY / BRISK / HARDY / EAGER / CALM. It is read from the IVs, not stored — so it is fixed from the moment it hatches, survives evolving, and is always true: a BRISK creature really is the fast one, and checking its speed will agree. Whichever IV stands out names the trait; a creature with no standout stat is described by its overall quality instead, which is the honest reading of a flat spread.

Today

The player card's last page is a five-line checklist: cared for today, well fed, clean, happy, rested. Every line is computed from what the save already holds — nothing extra is written, there is no reward to claim, and it resets itself with the day because the streak and the stat bars already do. It is there to answer "what does it need right now", not to be another currency.

Battle stats & IVs

Every pet rolls four IVs (individual values, 0–31) at hatch — ATK / DEF / SPD / VIT — that never change and make each one genuinely unique:

stat = base + level + (IV × level)/100 + training

The (IV × level)/100 term is the real formula from Gen III onward — a perfect IV is worth +31 at level 100. But IVs do a second job here that they don't do in the games: they cap training.

Effect
Innate bonus up to +22 at level 73 (end of a normal life)
Training ceiling 70 + (30 × IV)/31 → 77 at IV 8, 100 at IV 31
Total spread ~40 points, about 15 % between a great and a poor individual
  • Rolls are 8–31, never 0. In the real games a 0 IV is survivable because you can breed hundreds of eggs; here a pet lives 3 days, so a dud would just be a punishment.
  • Streak + bond bias the roll upward (up to +7) — using the previous pet's care score. Raising one well genuinely improves the next.
  • Legendaries hatch with 3 of 4 IVs perfect, exactly as they're guaranteed 3 perfect IVs in the games.
  • Shinies floor every IV at 20 — a nod to Gen 2, where shininess was a DV pattern and a shiny was never mediocre.
  • IVs are shown on the Battle page of the stat card; a perfect 31 is highlighted.

Training: STRENGTH ← the bag, SPEED ← the reaction test, DEFENSE ← the ball game (and still 1 h of wellbeing passively). VIT can't be trained. All three live in the training menu now; the ball moved off the home row when it became defence's trainer. The memory game shares that menu and trains nothing — it pays in joy and bond, which is why it is the one row with no progress bar.

TMs unlock at level 40, all of them, and nothing before. A TM carries no level requirement in the data — true of the games, wrong here, because a young creature has few level-up moves and the spare slots were filled with the strongest TMs in the table. A level 1 Squirtle opened with SURF and BLIZZARD and could beat Brock.

One number rather than a curve: the first five leaders sit at 14–43, so you fight the early ladder on what your species actually learns, and TMs arrive as you enter the back half. A creature retires at 73 and caps at 100.

That only works because the move table now carries the cheap early attacks — SCRATCH, PECK, POISON STING, BUBBLE, ABSORB, SPARK, FURY ATTACK and the rest. Before them, ~15 % of species reached level 15 with no attacking move at all and were quietly leaning on TMs to fill the gap.

Gym wins train too, which is what makes the ladder worth replaying rather than a checklist you tick once:

Retiring a creature early

The farewell is only offered at final form and three days. RETIRE on the menu ends a creature whenever you like -- but what that costs depends entirely on whether the farewell had been earned yet.

Retiring one that has earned its farewell free -- it is simply the farewell reached by another button. It joins your party and blesses the next egg
Retiring one that has not it is gone for good -- not banked, not in the box -- the next egg is neutral instead of blessed, and the next creature evolves a day later

An early retire used to bank the creature and bless the next egg exactly as a farewell does, which made it the good ending with a small tax on it: you could retire, check the egg, and retire again, farming blessed rolls at no real cost. Giving the creature up is the price now, and the confirm dialog spells out both halves before you accept.

The penalty is EVO_PENALTY_LEVELS (24 at MINUTES_PER_LEVEL 60) added to every evolution threshold, the same sum careMistakes moves. It lands on the creature that hatches next, is spent by hatching it, and does not compound: three early retires in a row still cost one day. The creature's card says "evolves a day later" while it carries the debt, and the confirm dialog says the price before you accept it.

Settings, brightness and starting over

Settings is two pages, paged from the rim like every other paged screen. Page 1 is the clock. Page 2 is sound, volume, brightness and language, plus RESET GAME.

  • Brightness is a level 1–10 and it is saved, so it survives a reboot and travels in your .tpsave backup. It is a ceiling on the automatic dimming rather than a replacement for it — the idle dimming and the sleep dimming are there to protect the AMOLED, not to be overridden, so a setting that beat them would let a static screen sit at full brightness for a week. The default is the brightness this firmware has always used, so an existing save behaves identically until you touch it.
  • RESET GAME always asks first, and the wipe itself happens on the next boot, before anything is loaded. That ordering is the point: clearing the save while the old creature is still in memory leaves a window where something can write it straight back, and a power cut mid-wipe would otherwise leave a save that is neither the old one nor a new game. The intent is recorded outside the data being erased, so an interrupted reset simply finishes next time.

Choosing your egg's region

The species is decided when the egg appears, not when it cracks, so changing region moves the egg you are holding. Two rules stop that being a re-roll button:

Rule
Rarity the tier the egg was granted is kept — only which species of that tier changes, so flipping can never fish for a legendary
Memory each region's answer is remembered for the current egg, so switching back shows the same creature

The region is first chosen at the very start of a new game, on the screen before the starter -- so the creature you begin with and the eggs that follow come from the same place. Everything below is about changing it afterwards.

A region is decided by the base species, and evolutions follow wherever they lead — a Kanto run still reaches Crobat and Blissey.

Training a win is worth
Easy 3–5 points, +1 per 3 leaders deeper into the ladder
Hard 6–10 points, same ladder bonus
Which stat random, but only among stats not already at their ceiling
Who gets it the live pet, and only if it was in the squad

A random stat that landed on a maxed one would silently evaporate, so it never picks one; and the IV-bound ceiling above still applies, so a win can never push a stat past what its IV allows. Banked members are frozen at what they were banked with, and battling already costs the live pet energy — that, not a cooldown, is what rate-limits rematching. A fully trained creature is told so.

Hardware

  • Board: ESP32-S3-Touch-AMOLED-1.75 — get the Standard (no case) or -G (GPS, also fits) version; not the "-B" (ships with a protective case that won't fit). The separate "1.75C" is a different board.
  • Round 466×466 AMOLED, CO5300 driver (QSPI, 80 MHz)
  • Capacitive touch CST9217 (I2C, address 0x5A)
  • AXP2101 (power management + battery + PWR button), PCF85063 (RTC), microSD slot, ES8311 audio codec (→ amplifier → external speaker on the MX1.25 connector)
  • Pins taken from the official Waveshare repo (see pin_config.h)

Libraries (Arduino IDE / arduino-cli)

Library Author Use
GFX Library for Arduino (Arduino_GFX) moononournation CO5300 over QSPI + framebuffer in PSRAM
SensorLib Lewis He CST9217 touch + PCF85063 RTC
XPowersLib Lewis He AXP2101 PMU (battery, brightness, PWR button)
ESP_I2S (bundled in the ESP32 core) Espressif I2S to the ES8311 codec

IDE setup / build

  • Board: ESP32S3 Dev Module · Flash 16MB · PSRAM OPI PSRAM (required: the 466×466×16-bit framebuffer ≈ 434 KB lives in PSRAM) · Partition Scheme with FAT (e.g. 16M Flash (3MB APP/9MB FATFS)) · USB CDC On Boot Enabled
FQBN="esp32:esp32:esp32s3:CDCOnBoot=cdc,FlashSize=16M,PSRAM=opi,PartitionScheme=app3M_fat9M_16MB"
arduino-cli compile --fqbn "$FQBN" .
arduino-cli upload -p /dev/cu.usbmodemXXXX --fqbn "$FQBN" .

# or just, which finds the port itself and can open the console:
bash tools/flash.sh --monitor

Run it on your computer

tools/emu/ compiles the real firmware — TamaPoke.ino, pet.cpp, i18n.cpp, party.cpp, unmodified — into a clickable desktop app, stubbing only the hardware. Click to touch, drag to swipe, type serial commands in the terminal, and --fast 60 runs a whole 3-day life in about an hour.

brew install sdl2          # Debian: apt install libsdl2-dev
bash tools/emu/build.sh
tools/emu/tamapoke-emu --scale 2 --fast 60

There's a headless --shot mode for screenshots too. See tools/emu/README.md. It won't tell you anything about timing, DMA tearing, PSRAM or audio — those still need the board.

Easiest install: the web installer

web/index.html flashes the firmware (ESP Web Tools) and pushes the sprites to the SD over Web Serial, no Arduino needed. Serve it over HTTPS or localhost (secure context) and open it in Chrome/Edge. See web/README.md.

Generate and load the sprites yourself

All sprites come from PMD SpriteCollab (CC BY-NC). You can regenerate the whole set and load it onto your board with the pipeline below — the firmware accepts files over USB (PUT protocol with per-block ACK), so you don't have to remove the card. A card the board cannot read (most cards over 32 GB are sold as exFAT, which it does not support) is formatted only when you ask: Format card in the web installer, or SD MOUNT FORMAT on the console. It used to format such a card without asking, whatever was on it.

python3 tools/unpack_bundle.py  # FASTEST: restore tools/sdcard/mons/ from the committed web/sprites-*.pak (no network)
python3 tools/pack_pmd.py       # or fetch from source: PMD sprites, normal + shiny, every region in dex_data.REGIONS
python3 tools/make_thumbs.py    # Pokédex thumbnails (from the PMD sprites) -> thumbs.bin
python3 tools/send_sd.py        # send tools/sdcard/mons/* to the board's SD over USB

unpack_bundle.py is the one you want unless you are adding a region: the .pak bundles are committed, so it needs no network and gives you the exact bytes that ship. pack_pmd.py re-fetches ~40 MB per region from SpriteCollab.

To make the one-click web-installer bundle instead of sending over USB:

python3 tools/pack_bundle.py    # bundle tools/sdcard/mons/* into web/sprites-<region>.pak, one per region

Then install them from the web installer's region list (or send_sd.py above). pack_pmd.py also takes individual dex numbers, e.g. pack_pmd.py 7 25. (About 285 MiB across the nine regions, all PMD. The loose files in tools/sdcard/mons/ are gitignored build intermediates; the .pak bundles ship.)

How to play

On first run you choose a starter (Bulbasaur / Charmander / Squirtle). After that you start with an egg. Tap it 3 times or wait and it hatches. From then on, care for your companion:

Four stats that decay: FOOD, JOY, ENE (energy), HYG (hygiene). If one bottoms out it counts as a slip-up.

Buttons (bottom arc, icons):

  • 🍎 Feed → food menu: 3 berries (each species has a hidden favourite that gives a bonus) and a candy (+happiness but it fattens; weight makes it sluggish).
  • ⚽ Play → the pokeball minigame (joy only).
  • 🌙 Light → sleep/wake (recovers energy, dims the screen). While asleep, needs decay much slower (rest).
  • 🫧 Bath → a foam scene that cleans up the poops.

Touch gestures — three axes, one meaning each. The round panel has no corners to anchor a back button, so the gestures carry the navigation, and each direction means exactly one thing everywhere:

Gesture Means
Horizontal swipe move along the tile axis: PLAYER · PARTY · [PET] · EXPLORE · GYM · POKÉDEX. It bumps at both ends and never closes anything. The six dots show position; the pet screen also names the neighboring PARTY and EXPLORE destinations.
Swipe up go deeper — from the pet, its stat card (4 pages)
Swipe down go back, one level, from anywhere
Drag the rim page whatever is in front of you. The arc at 2–5 o'clock is the scrollbar; it shows position and extent, which a row of dots cannot.

Paging used to live on the horizontal swipe, which meant "next page" on some screens and "exit" on others — the same bug shipped four separate times. Moving it to the rim is what frees the horizontal axis to mean one thing.

  • Tap the name at the top = the menu (Pokédex / Bag / Settings). Close it with the CLOSE row, by tapping anywhere outside the panel, or with any swipe.
  • Tap the creature = pet it (+happiness, bond).
  • Long press (3 s) on the creature = release dialog. Gated to the main screen only, so it can never fire on a party slot.

The four care stats are arcs on the rim rather than bars: colour is severity, exactly as the bars were, and the label says which stat. That is what freed the bottom of the panel for home icons a finger can actually hit — they went from 60 px (5.7 mm) to 80 px (7.6 mm), and hit_test now holds every new control to UI_TAP_FINGER (94 px = 9 mm) rather than the old UI_TAP_MIN of 44, which was 44 points borrowed as pixels.

Physical PWR button: short = screen on/off · long (4 s) = full power-off (the RTC stays alive, so time passes even while it's off).

Decisions: you choose, and you watch

The three life-cycle endings and evolution don't happen on their own — when the conditions are met a button appears and you tap it (so you're present to witness it), each opening a two-option dialog:

  • Evolution (red button): Evolve (epic animation: halo, rays, sparkles and a flicker between the old and new form) or Keep form (re-offered next level).

  • Farewell (gold button, final form + 3 days): Say goodbye (warm farewell, rising hearts → new egg) or Stay together (keep your companion; re-offered in a day). Tension: a maxed-out friend vs. completing the Pokédex.

  • Runaway (dark button, total neglect for 1 h): a somber "feels abandoned" ending in the rain — caring for the creature cancels it.

    It does not ask, and that is the point -- a creature you have to authorise to leave is not really at stake. What it must never be is the price of going to bed, so the screen being off between midnight and 06:00 puts the creature to sleep, and sleep floors the stats at FOOD 30 / JOY 35 / HYG 45 with the neglect check skipped entirely. A night costs you a hungry, grubby creature at breakfast instead of an empty one.

    Both halves are needed. The screen alone would pause the game every time you pocketed the device, and the creature is meant to get hungry during the day. The hour alone would send it to bed while you were still playing. Set the clock in SETTINGS (or RTCSET); a board whose clock was never set simply never auto-sleeps, which fails safe -- it keeps draining and the light button still works by hand.

    This was a real loss: a player left the board running overnight and came back to a Dratini that had gone. The live tick was the only drain path with no floor -- offline floors at 15, sleep at 30/35/45 -- so a board left running was punished where a board switched off was not. night_test runs ten simulated hours and fails if that ever comes back.

Sprites: PMD SpriteCollab everywhere

  • PMD SpriteCollab (everything — main screen, stat card, minigame and the Pokédex grid + detail view): behaviour sprites — tools/pack_pmd.py packs actions (Idle, Walk L/R, Sleep, Eat, Hurt, Attack, Pose, Nod, DeepBreath) into the multi-action TPK2 format (/mons/pNNN.bin). The engine in TamaPoke.ino makes the creature wander, gesture, curl up to sleep, chew and wince. Anchored by the feet (lowest content row), not the canvas. The Pokédex thumbnails (thumbs.bin, TPTH) are derived from these by tools/make_thumbs.py.
  • In-house workshop (tools/sprites.py): 9 primitive-drawn sprites as a no-SD fallback + the UI icons. Generates species.h. Preview in tools/sheet.png, emit with python3 tools/sprites.py emit.

sdmon.h/.cpp loads the PMD sprites into PSRAM (PmdMon for TPK2) plus the thumbnails (SdThumbs). SdMon (TPK1) remains as a dormant legacy fallback only.

Pokédex and species data

tools/dex_data.py is the single source: name, slug, type (accent colour + background biome), evolution line with gen-1 levels, rarities and starters. tools/dex_stats.py has the base stats and tools/dex_types.py the typings and type chart (both from PokéAPI). Note these are current values, not Gen 1 ones — Pidgeot has 101 Speed here, not the 91 it had in Red/Blue. gen_dex.py emits dex.h (the DEX_TBL[152] table). The pet's identity is its Pokédex number (persisted in NVS).

  • Evolution gen-1 style (levels 16/36/…; stones ≈30, trade ≈40; Eevee branches to whichever evolution you're missing). Each slip-up delays it 1 level; it won't evolve with any stat < 40 or while asleep.

Types

Every species carries its real typing (one or two of the 18 types) and the game ships the full 18×18 effectiveness chart — dex.h holds both, generated from tools/dex_types.py.

The chart is the current (Gen 6+) one, not Gen 1's, which is a deliberate call:

  • Gen 1's chart shipped real bugs — Ghost moves did literally nothing to Psychic — and Psychic was resisted only by Psychic, so it ran away with every fight.
  • The base stats here were already pulled from PokéAPI at current values, so modern stats with an ancient chart was the inconsistent pairing.
  • Fairy earns its keep: Dragonite has the highest Attack in the dex, and before Gen 6 Dragon was resisted only by Steel. Dragon → Fairy is 0×, so Clefable hard-walls the strongest thing you can hatch.

Seven of the original 151 differ from their Gen 1 typing: Magnemite and Magneton gained Steel (Gen 2), and Clefairy, Clefable, Jigglypuff, Wigglytuff and Mr. Mime gained Fairy (Gen 6) — the first two losing Normal entirely.

Typing is shown on the Battle page of the stat card. (Battles: on the roadmap.)

Battle stats and training

Each creature has ATK/DEF/SPD/VIT = base stat + level + IV (0–31, rolled at hatch, IV × level/100 exactly as in the real games) + training:

  • SPEED ← the reaction test (~2 reactions = 1 pt, cap +18 per session)
  • DEFENSE ← accumulated wellbeing (1 h resting or well-cared = +1)
  • STRENGTH ← the training bag (~4 hits = 1 pt, cap +18 per session)
  • VIT (vitality, from the base HP stat) — not trainable

Moves

Each creature knows up to 4 moves, from a pool of 77. Two kinds:

  • Level-up moves are gated: Charizard learns FLAMETHROWER at 34, WING ATTACK at 36, DRAGON RAGE at 54. A hatchling starts with only what its species knows at level 1 — a Charmander opens with GROWL alone, and the other three slots stay empty. Crossing a gate fills an empty slot silently; with all four full you get a prompt asking which to forget (or to skip it). Offers queue, so coming back to a pet that aged two weeks offline asks one at a time. Evolving keeps the moves it already has, and the new form's gates take over — moves it would have learned below your current level are not backfilled, same as the real games.
  • TMs have no level gate and are chosen on demand, from the MOVES page of the stats card (swipe across, then tap a slot).

Levels come from FireRed/LeafGreen, the Kanto games that still gate properly. A move that is also a TM keeps its level gate — otherwise every gated move would come free, since most of them were sold as TMs at some point.

Moves freeze when a pet is banked into the party, alongside its level and training: the set you chose while it was alive is what it fights with forever.

Special attack and defence come off the species' own bSpA/bSpD base stats (Alakazam is 50 Attack but 135 Special Attack), reusing the physical IV and training rather than rolling their own: special attack runs off the ATK IV and training, special defence off the DEF IV and training. So the physical/special split lives on the species, not the individual — no extra IVs to roll.

The IV also sets how far each stat can be trained at all (77–100), so a well-rolled individual has a genuinely higher ceiling, not just a head start. See Battle stats & IVs for the numbers.

Shown on the Battle page of the stat card. The (hidden) weight goes up with candy and burns off with training.

Retention: streak, bond, medals, name

  • Streak (the player's, persists across creatures): the first care of each real day advances the streak; 3/7/30/100 milestones are celebrated; skipping a day breaks it. Flame badge on the main screen.
  • Bond (the creature's): rises slowly with care and petting, drops with slip-ups.
  • Medals for the individual (level, berry, streak, bond, final form, fit) + a global counter. Medals page of the stat card.
  • Name: touch keyboard; the nickname rules the header and the card.

High streak and bond improve the egg roll (rarity and shiny): caring well always pays off.

Life cycle, eggs by rarity, languages

The life cycle lasts 3 days of play. Three endings (all leave a new egg): farewell (final form + 3 days), release (long press), runaway (all 4 bars at zero for 1 h). Each bred species is recorded in the bred Pokédex (normal and shiny separately).

The egg rolls rarity over the ~79 base forms (47 common / 27 rare / 5 legendary), biased towards the lines you're missing (all 1025 are completable), blessed by a farewell and punished by a runaway. Legendaries only with 25+ registered. Shiny 1/48 (better with streak/bond/farewell).

Languages: the UI ships in 6 languages — English (default), Spanish, French, German, Italian, Portuguese — switchable from the settings screen (swipe down). Move names on cards, pickers and battle buttons follow that choice, as does the battle narration that names the move being used.

Backgrounds: biome + real time

The idle screen paints the sky from the RTC's real time (dawn / day / dusk / night with moon and stars) and the ground from the type's biome (meadow, beach, forest, volcano, mountain, snow). Sleeping forces night.

Layout

  • TamaPoke.ino — init, game loop, render of every screen, gestures, serial console, audio
  • pet.h / pet.cpp — pet state and logic (stats, evolution, life cycle, streak/bond/medals, NVS)
  • party.h / party.cpp — the 6 retired pets kept from farewells and releases
  • sdmon.h / sdmon.cpp — TPK1 (animated) and TPK2 (PMD) sprites + thumbnails, and file reception over USB (PUT/LS)
  • rtcbat.h / rtcbat.cpp — PCF85063 RTC + AXP2101 PMU (battery, brightness, PWR button)
  • audio.h / audio.cpp — ES8311 + I2S + Game-Boy-style tone synth (non-blocking task)
  • i18n.h / i18n.cpp — the 6-language string tables
  • move_names.h — GENERATED (gen_moves.py): move display names in all 6 languages
  • dex.h — GENERATED (gen_dex.py): the species table, DEX_COUNT 1025
  • species.h — GENERATED (sprites.py): fallback sprites, UI icons, colours
  • pin_config.h — the board's official pins
  • tools/ — pipeline: dex_data.py (data), dex_stats.py, dex_types.py, gen_dex.py, dex_move_names.py / gen_moves.py (localized moves), sprites.py (workshop), pack_pmd.py / make_thumbs.py (packers), pack_bundle.py (web bundle), send_sd.py (SD upload), touch_log.py
  • tools/emu/ — desktop emulator (real firmware + stubbed hardware, SDL)
  • tools/sdcard/mons/ — the generated .bin files (animated, shiny, PMD, thumbnails)
  • web/ — the browser installer (ESP Web Tools + Web Serial sprite loader)

Serial console (115200, debug)

STATS (full state) · SPEC <dex> (change species) · LVL <n> · IV <atk> <def> <spd> <vit> (force individual values) · HATCH · EGG <dex> [shiny] (hatch a chosen species — the only way to test the legendary/shiny IV guarantees, which apply at hatch) · SHINY · NICK <x> · BYE / RUN (farewell / runaway) · ABANDON (force the runaway-ready state) · WIPE (factory reset → new game) · BEEP (audio test) · REG (Pokédex) · EGGS (simulate 20 eggs) · GAL (gallery) · CAREDAY · PARTY / PARTY <dex> / PARTY CLEAR (inspect and fill the party) · FOCUS <slot> (swap the creature you are raising with a party slot) · BAG (list it) · GIVE <key> [n] (stock it) · WILD / WILD HARD (force an encounter) · TIME <epoch> / RTCSET <epoch> · HEALTH (uptime + heap for the soak test) · LS / PUT (SD files) · SUM <path> (CRC32 and size of a file as stored on the card) · PACKS (installed sprite pack versions) · SD MOUNT (mount a card inserted after boot) · SD MOUNT FORMAT (the same, formatting a card the board cannot read — it erases the card).

MAX (level 100, perfect IVs, training at the ceiling) is emulator only. The firmware compiles it and never enables it: this is a game about raising something slowly, and a one-word cheat on a real board is a way to skip the whole point of it by accident. It is a runtime flag rather than an #ifdef on purpose, so both builds keep compiling exactly the same sources.

To test fast: lower PET_TICK_MS, MINUTES_PER_LEVEL and FAREWELL_AGE_MIN in pet.h.

Roadmap

  • Soak test 24–48 h (instrumentation ready: HEALTH command/heartbeat).
  • Cries by ear. The per-species chirps are synthesised from each creature's own typing and base stats, and cry_test proves all 1025 are audible, bounded, distinct and deterministic — but no test can say whether they sound right. That needs tamapoke-emu --wav and a board.

(Done: Galar and Paldea gym ladders and sprite packs; wild encounters, catching and the bag; 3D-printed case published on MakerWorld; repo public with the browser installer + one-click sprite bundle.)

Community forks

  • TamaPoke — Expanded by ShadowEnemy — a substantial community fork (different author/branch): a full type-matchup battle system, all 151 + shinies with a Pokédex / collection box and daily goals, 6 UI languages, ES8311 sound, starter choice and a one-click web installer. Worth a look. 🎮

Credits

All sprites: PMD SpriteCollab (community, CC BY-NC). Base stats: PokéAPI. Pokémon is a ™ of Nintendo / Game Freak / The Pokémon Company. Non-commercial, personal-use project. Full list in CREDITS.md.

License

  • Source code (firmware + tooling): MIT.
  • Sprites & names: © Nintendo / Game Freak / The Pokémon Company; pixel art from PMD SpriteCollab (CC BY-NC 4.0). Non-commercial use only.
  • 3D-printed case: remix of "Pokeball" by yoyothechicken (MakerWorld #839922), licensed CC BY-NC-SA, and shared here under the same terms.

This is an unofficial fan project, not affiliated with or endorsed by Nintendo.

About

A Pokemon Gen-1 Tamagotchi for the Waveshare ESP32-S3-Touch-AMOLED-1.75 round display — open firmware + one-click web installer. Case remixed from yoyothechicken's Pokeball.

Resources

Stars

4 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages