diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0602184c..dc29f093 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -86,6 +86,7 @@ jobs: make -C games/doodlejump host test make -C games/stickmanhook host test make -C games/coreball host test + make -C games/uno host test diff --git a/Makefile b/Makefile index c683fb0e..8a91de02 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ PY ?= python3 CARGO ?= cargo B = build/data # Conventional games honor a configurable output directory; special layouts are kept local. -GAMES_REDIRECT = balatro blockblast breakout buckshot celeste championisland chess connectfour cookieclicker coreball dinorun donkeykong doodlejump doom dune finalearth2 flappybird flappydunk fnaf g2048 hillclimb hollowknight invaders isaac jetpackjoyride leveldevil minesweeper monkeymart numblocks numpowder numvisuals pacman portal pvz snake sokoban solitaire sonic stickmanhook subway sudoku suika tombofthemask towerblock twoplayer undertale uttt +GAMES_REDIRECT = balatro blockblast breakout buckshot celeste championisland chess connectfour cookieclicker coreball dinorun donkeykong doodlejump doom dune finalearth2 flappybird flappydunk fnaf g2048 hillclimb hollowknight invaders isaac jetpackjoyride leveldevil minesweeper monkeymart numblocks numpowder numvisuals pacman portal pvz snake sokoban solitaire sonic stickmanhook subway sudoku suika tombofthemask towerblock twoplayer undertale uno uttt GAMES_OUT = dinorun GAME_REDIRECT_ARG = $(if $(filter $(1),$(GAMES_OUT)),OUT=../../$(B)/games/$(1),BUILD_DIR=../../$(B)/games/$(1)) # the RAM the oldest calculator software that runs apps gives them (23.2: 148928 bytes; 25.2 gives 153676). @@ -29,7 +29,7 @@ RAM_LIMIT = 148928 # custom builds of Epsilon 25.2's source give apps less: 133652 bytes; every game fits there too, in NumPlay and on # its own (their big buffers are on the stack, 32 KB apart from that RAM, or in the RAM the software has left) CUSTOM_RAM = 133652 -BIG_APPS = Celeste HollowKnight Undertale ChampionIsland FinalEarth2 NumPowder NumBlocks NumDrive SubwaySurfers FiveNightsAtFreddys Doom BindingOfIsaac HillClimbRacing PlantsVsZombies SonicTheHedgehog CookieClicker LevelDevil JetpackJoyride StickmanHook DonkeyKong Dune MonkeyMart TombOfTheMask +BIG_APPS = Celeste HollowKnight Undertale ChampionIsland FinalEarth2 NumPowder NumBlocks NumDrive SubwaySurfers FiveNightsAtFreddys Doom BindingOfIsaac HillClimbRacing PlantsVsZombies SonicTheHedgehog CookieClicker LevelDevil JetpackJoyride StickmanHook DonkeyKong Dune MonkeyMart TombOfTheMask UNO # the calculator's space for installed apps: from the end of its software (64 KiB aligned) to 64 KiB before the # end of its 4 MiB slot of flash, 0x903F0000. The software's size sets the start: 0x90200000 is the latest a # NumPlay counts on (1.94 MB, APP_SPACE); Epsilon 26.4 on an N0120 ends at 0x90180000 (2.44 MB, the @@ -321,6 +321,7 @@ APP_donkeykong = $(B)/games/donkeykong/donkeykong.nwa:DonkeyKong.nwa APP_dune = $(B)/games/dune/dune.nwa:Dune.nwa APP_monkeymart = $(B)/games/monkeymart/monkeymart.nwa:MonkeyMart.nwa APP_tombofthemask = $(B)/games/tombofthemask/tombofthemask.nwa:TombOfTheMask.nwa +APP_uno = $(B)/games/uno/uno.nwa:UNO.nwa # the big games' modules are made from these (the kit) $(foreach g,$(XL_GAMES),$(eval MOD_$(g) = $$(word 1,$$(subst :, ,$$(APP_$(g)))))) @@ -354,6 +355,7 @@ apps: | $(B) $(MAKE) -C games/dune build NWLINK="$(NWLINK)" BUILD_DIR=../../$(B)/games/dune $(MAKE) -C games/monkeymart build NWLINK="$(NWLINK)" BUILD_DIR=../../$(B)/games/monkeymart $(MAKE) -C games/tombofthemask build NWLINK="$(NWLINK)" BUILD_DIR=../../$(B)/games/tombofthemask + $(MAKE) -C games/uno build NWLINK="$(NWLINK)" BUILD_DIR=../../$(B)/games/uno cd games/tetris/tetris && NWLINK="$(NWLINK)" $(CARGO) build --release --quiet cp $(word 1,$(subst :, ,$(APP_numdash))) $(B)/apps/$(word 2,$(subst :, ,$(APP_numdash))) cp $(word 1,$(subst :, ,$(APP_crossyroad))) $(B)/apps/$(word 2,$(subst :, ,$(APP_crossyroad))) @@ -385,6 +387,7 @@ apps: | $(B) cp $(word 1,$(subst :, ,$(APP_dune))) $(B)/apps/$(word 2,$(subst :, ,$(APP_dune))) cp $(word 1,$(subst :, ,$(APP_monkeymart))) $(B)/apps/$(word 2,$(subst :, ,$(APP_monkeymart))) cp $(word 1,$(subst :, ,$(APP_tombofthemask))) $(B)/apps/$(word 2,$(subst :, ,$(APP_tombofthemask))) + cp $(word 1,$(subst :, ,$(APP_uno))) $(B)/apps/$(word 2,$(subst :, ,$(APP_uno))) arm-none-eabi-strip --strip-unneeded $(B)/apps/Tetris.nwa STD_APPS = $(foreach g,$(STD_GAMES),$(B)/apps/$(NWA_$(g))) @@ -456,5 +459,6 @@ clean: -$(MAKE) -C games/dune clean -$(MAKE) -C games/monkeymart clean -$(MAKE) -C games/tombofthemask clean + -$(MAKE) -C games/uno clean -cd games/tetris/tetris && $(CARGO) clean -$(foreach g,$(STD_GAMES),$(MAKE) -C games/$(g) clean;) diff --git a/README.md b/README.md index ece2d770..f2fc2527 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

29 free games for your NumWorks calculator, in one app.
- Plus 21 big games that install on their own. For the N0110, N0115 and N0120, in English, French and Chinese. + Plus 22 big games that install on their own. For the N0110, N0115 and N0120, in English, French and Chinese.

@@ -130,8 +130,11 @@ These are too big to fit next to NumPlay, so each one is its own app. Install it Tomb of the Mask
Tomb of the Mask

Dash through the tomb: the arcade climb and all 200 stages
Download + UNO
UNO

The card game against one to three bots, to 500 points
Download Super Mario 64
In a repository of its own: NumWorks-Super-Mario-64 Super Mario Bros.
All 32 stages, in a repository of its own: NumWorks-Super-Mario-Bros + + More on the way
Minecraft 1.18.2 and more: see the roadmap @@ -274,6 +277,7 @@ Read **[Make your own NumWorks game](docs/make-a-game.md)** to go from there, an - [x] Dune - [x] Monkey Mart - [x] Tomb of the Mask +- [x] UNO - [x] [NumPlay Studio](https://mason363.github.io/NumPlay/customize/), the website where you pick the games in your NumPlay, with Enlarged Extra Apps to fit several big games - [ ] Full Minecraft 1.18.2 as a standalone app - [ ] Among Us, against bots @@ -337,6 +341,7 @@ NumPlay is a fan project, made out of love for these games and for the NumWorks - **Dune!** by Team Lava inspired NumPlay's Dune, which ports Dune Rider, Inlogic Software's web version: its pictures, Bango Pro lettering and numbers, and its physics, [p2.js](https://github.com/schteppe/p2.js) by Stefan Hedman (MIT License), written again in C. **[Bobydeluxe](https://github.com/Bobydeluxe)** brought it to NumPlay. Not affiliated with Team Lava or Inlogic Software. - **Monkey Mart** by TinyDobbins inspired NumPlay's Monkey Mart, which follows the rules, timings and numbers of the game's web version and uses its pictures, its monkeys and animals and the **Riffic** font from it. **[Bobydeluxe](https://github.com/Bobydeluxe)** brought it to NumPlay. Not affiliated with TinyDobbins. - **Tomb of the Mask** by Playgendary inspired NumPlay's Tomb of the Mask, which follows the rules of the game's web version and uses its stages, pictures and pixel font. **[Bobydeluxe](https://github.com/Bobydeluxe)** brought it to NumPlay. Not affiliated with Playgendary. +- **UNO** by Mattel inspired NumPlay's UNO, which plays by the official rules with cards drawn for the calculator. **[Bobydeluxe](https://github.com/Bobydeluxe)** brought it to NumPlay. Not affiliated with Mattel. - **[Level Devil](https://store.steampowered.com/app/3242750/)** by Unept: NumPlay's Level Devil is written in C with no code from the game, but its 80 rooms (their tiles, traps and timing) are the game's own, taken from its web build by games/leveldevil/tools/fetch.py, and its runner is drawn from the game's sprites. - **Pong** by Atari (1972) inspired NumPlay's 2 Players, with air hockey, tanks in a maze in the spirit of Tank Trouble and the light cycles of Tron. All four are written in C for NumPlay and drawn from shapes, with no code or art from them. @@ -365,7 +370,7 @@ NumPlay is a fan project, made out of love for these games and for the NumWorks NumPlay is licensed under the [GNU General Public License v3.0](LICENSE). Copyright (c) 2026 Mason Chen. Code and fonts from others keep their own licenses: the `UNLICENSE` files in the games' folders and the [LICENSES](LICENSES) folder. The names, artwork and music of the games NumPlay is inspired by belong to their makers and are not covered by it. -Unofficial fan games, not affiliated with NumWorks, RobTop Games (Geometry Dash), Hipster Whale (Crossy Road), the makers of Drive Mad, LocalThunk or Playstack (Balatro), Mike Klubnika or Critical Reflex (Buckshot Roulette), Hungry Studio (Block Blast!), Aladdin X (Suika Game), Voodoo (Flappy Dunk), Lima Sky (Doodle Jump), Mojang or Microsoft (Minecraft), Maddy Makes Games (Celeste), Team Cherry (Hollow Knight), Toby Fox (Undertale), id Software (Doom), Edmund McMillen, Florian Himsl or Nicalis (The Binding of Isaac), PopCap Games or Electronic Arts (Plants vs. Zombies), Scott Cawthon (Five Nights at Freddy's), SEGA (Sonic the Hedgehog, Master System, Game Gear), SYBO or Kiloo (Subway Surfers), Fingersoft (Hill Climb Racing), Unept (Level Devil), Halfbrick (Jetpack Joyride), Madbox (Stickman Hook), Nintendo (Donkey Kong, Game Boy, NES), Team Lava (Dune!), Inlogic Software (Dune Rider), TinyDobbins (Monkey Mart), Playgendary (Tomb of the Mask), Atari (Pong), Disney (Tron), Orteil or DashNet (Cookie Clicker), Taito or Midway (Space Invaders), Thinking Rabbit or Falcon (Sokoban), Florian van Strien (The Final Earth 2), The Powder Toy's developers (The Powder Toy), MateoConLechuga (Portal Returns), Valve (Portal), .GEARS (Flappy Bird), Bandai Namco (Pac-Man), Hasbro (Connect Four), Google (Snake, Solitaire, Block Breaker, Chrome Dino, Doodle Champion Island Games), STUDIO4°C or Microsoft (Minesweeper), ducalex (Retro-Go). Tetris is a trademark of The Tetris Company, Chrome of Google LLC, Minecraft of Mojang Synergies AB, Pac-Man of Bandai Namco, Connect Four of Hasbro, DOOM of ZeniMax Media, Sonic the Hedgehog, Master System and Game Gear of SEGA, Space Invaders of Taito, and Donkey Kong, Game Boy, Game Boy Color and NES of Nintendo. +Unofficial fan games, not affiliated with NumWorks, RobTop Games (Geometry Dash), Hipster Whale (Crossy Road), the makers of Drive Mad, LocalThunk or Playstack (Balatro), Mike Klubnika or Critical Reflex (Buckshot Roulette), Hungry Studio (Block Blast!), Aladdin X (Suika Game), Voodoo (Flappy Dunk), Lima Sky (Doodle Jump), Mojang or Microsoft (Minecraft), Maddy Makes Games (Celeste), Team Cherry (Hollow Knight), Toby Fox (Undertale), id Software (Doom), Edmund McMillen, Florian Himsl or Nicalis (The Binding of Isaac), PopCap Games or Electronic Arts (Plants vs. Zombies), Scott Cawthon (Five Nights at Freddy's), SEGA (Sonic the Hedgehog, Master System, Game Gear), SYBO or Kiloo (Subway Surfers), Fingersoft (Hill Climb Racing), Unept (Level Devil), Halfbrick (Jetpack Joyride), Madbox (Stickman Hook), Nintendo (Donkey Kong, Game Boy, NES), Team Lava (Dune!), Inlogic Software (Dune Rider), TinyDobbins (Monkey Mart), Playgendary (Tomb of the Mask), Mattel (UNO), Atari (Pong), Disney (Tron), Orteil or DashNet (Cookie Clicker), Taito or Midway (Space Invaders), Thinking Rabbit or Falcon (Sokoban), Florian van Strien (The Final Earth 2), The Powder Toy's developers (The Powder Toy), MateoConLechuga (Portal Returns), Valve (Portal), .GEARS (Flappy Bird), Bandai Namco (Pac-Man), Hasbro (Connect Four), Google (Snake, Solitaire, Block Breaker, Chrome Dino, Doodle Champion Island Games), STUDIO4°C or Microsoft (Minesweeper), ducalex (Retro-Go). Tetris is a trademark of The Tetris Company, Chrome of Google LLC, Minecraft of Mojang Synergies AB, Pac-Man of Bandai Namco, Connect Four of Hasbro, DOOM of ZeniMax Media, Sonic the Hedgehog, Master System and Game Gear of SEGA, Space Invaders of Taito, UNO of Mattel, and Donkey Kong, Game Boy, Game Boy Color and NES of Nintendo. ## Gallery @@ -453,5 +458,6 @@ NumPlay is licensed under the [GNU General Public License v3.0](LICENSE). Copyri Monkey Mart: the monkey carries bananas and milk to the shelves while the customers shop
Monkey Mart Tomb of the Mask: dashing up the tomb ahead of the rising flood
Tomb of the Mask + UNO: a hand against three bots, a Draw Two and a colour called
UNO diff --git a/docs/media/thumb_uno.png b/docs/media/thumb_uno.png new file mode 100644 index 00000000..c9196927 Binary files /dev/null and b/docs/media/thumb_uno.png differ diff --git a/docs/media/uno.gif b/docs/media/uno.gif new file mode 100644 index 00000000..341eae3a Binary files /dev/null and b/docs/media/uno.gif differ diff --git a/docs/play.md b/docs/play.md index 76432b24..ae9d6b81 100644 --- a/docs/play.md +++ b/docs/play.md @@ -565,6 +565,22 @@ Dash up the tomb ahead of the rising flood, picking up dots and coins and dodgin
+## UNO + +UNO + +| Key | What it does | +| --- | --- | +| **Left, Right** | Pick a card | +| **OK** | Play it | +| **Up** | Draw | +| **shift** | Say UNO, or catch a bot that didn't | +| **Back** | Pause (with New game, How to play and Quit) | + +Match the top card by colour or number, or play a Wild. Skip, Reverse, Draw Two and Wild Draw Four work as in the official rules, and you can challenge a Wild Draw Four. Say UNO when you're down to one card. The first to empty their hand scores the others' cards; the first to 500 wins. Your game is saved. + +
+ ## Portal Returns Portal Returns: falling through a portal and flying across a test chamber diff --git a/games/games.json b/games/games.json index a8a86cfb..75ccd0e6 100644 --- a/games/games.json +++ b/games/games.json @@ -677,7 +677,7 @@ }, { "id": "championisland", - "rank": 18, + "rank": 19, "title": "Champion Island", "tagline": "Seven sports, one island", "credit": "Based on Google's Champion Island Games", @@ -699,7 +699,7 @@ }, { "id": "finalearth2", - "rank": 19, + "rank": 20, "title": "The Final Earth 2", "tagline": "Build a city in space", "credit": "Based on the game by Florian van Strien", @@ -721,7 +721,7 @@ }, { "id": "numpowder", - "rank": 14, + "rank": 15, "title": "NumPowder", "tagline": "Sand, water, fire and more", "credit": "Inspired by The Powder Toy", @@ -775,7 +775,7 @@ }, { "id": "fnaf", - "rank": 12, + "rank": 13, "title": "Five Nights at Freddy's", "tagline": "Survive five nights", "credit": "Based on the game by Scott Cawthon", @@ -836,7 +836,7 @@ }, { "id": "isaac", - "rank": 16, + "rank": 17, "title": "The Binding of Isaac", "tagline": "Cry through the basement", "credit": "Based on the game by Edmund McMillen", @@ -858,7 +858,7 @@ }, { "id": "hillclimb", - "rank": 17, + "rank": 18, "title": "Hill Climb Racing", "tagline": "Drive up, don't flip", "credit": "Based on Hill Climb Racing by Fingersoft", @@ -902,7 +902,7 @@ }, { "id": "sonic", - "rank": 15, + "rank": 16, "title": "Sonic the Hedgehog", "tagline": "Run through six zones", "credit": "Based on the game by Sonic Team", @@ -923,7 +923,7 @@ }, { "id": "cookieclicker", - "rank": 21, + "rank": 22, "title": "Cookie Clicker", "tagline": "Bake billions of cookies", "credit": "Based on Cookie Clicker by Orteil", @@ -944,7 +944,7 @@ }, { "id": "leveldevil", - "rank": 20, + "rank": 21, "title": "Level Devil", "tagline": "Run to the door", "credit": "Based on Level Devil by Unept", @@ -1007,7 +1007,7 @@ }, { "id": "donkeykong", - "rank": 13, + "rank": 14, "title": "Donkey Kong", "tagline": "Climb the girders and save Pauline", "credit": "Based on Donkey Kong by Nintendo", @@ -1088,5 +1088,26 @@ "repo": "https://github.com/Mason363/NumPlay/tree/main/games/tombofthemask", "default": false, "xl": true + }, + { + "id": "uno", + "rank": 12, + "title": "UNO", + "tagline": "Match the cards, shout UNO", + "credit": "Based on UNO by Mattel", + "top": "0x1F6B4A", + "bottom": "0x0B3324", + "accent": "0xE53935", + "colors": 16, + "records": [ + "uno.sav", + "uno_saves.py" + ], + "shots": [ + "docs/shot.png" + ], + "repo": "https://github.com/Mason363/NumPlay/tree/main/games/uno", + "default": false, + "xl": true } ] diff --git a/games/uno/.gitignore b/games/uno/.gitignore new file mode 100644 index 00000000..342d9dda --- /dev/null +++ b/games/uno/.gitignore @@ -0,0 +1,2 @@ +output/ +build/ diff --git a/games/uno/Makefile b/games/uno/Makefile new file mode 100644 index 00000000..1bb3def9 --- /dev/null +++ b/games/uno/Makefile @@ -0,0 +1,56 @@ +# UNO on the NumWorks calculator. +# make output/uno.nwa +# make check link it like the calculator does (output/uno.bin) +# make host build/play: the game on a computer, for tests and pictures +# make test the rules against the instruction sheet, bots playing whole games to 500, random keys, saves +# (with the sanitizers) +Q ?= @ +CC = arm-none-eabi-gcc +NWLINK ?= npx --yes -- nwlink@1.0.0 +BUILD_DIR = output +L = ../../launcher/src + +SRC = src/main.c src/game.c src/bot.c src/draw.c src/save.c src/plat_eadk.c $(L)/gfx.c $(L)/font_data.c $(L)/xfont.c +HEADERS = $(wildcard src/*.h) $(L)/gfx.h $(L)/font.h $(L)/np.h ../common/epsilon_app.h ../common/epsilon_files.h + +CFLAGS = -std=gnu11 $(shell $(NWLINK) eadk-cflags-device) -DPLATFORM_DEVICE=1 -DEPSILON_XL=1 -I$(L) +CFLAGS += -Os -Wall -Wextra -Wno-unused-parameter -fno-math-errno -fsingle-precision-constant +CFLAGS += -fno-tree-loop-distribute-patterns -flto -fno-fat-lto-objects -fwhole-program -fvisibility=internal +CFLAGS += -ffunction-sections -fdata-sections +LDFLAGS = -Wl,--relocatable -nostartfiles --specs=nano.specs +LDFLAGS += -Wl,-e,main -Wl,-u,eadk_app_name -Wl,-u,eadk_app_icon -Wl,-u,eadk_api_level +LDFLAGS += -Wl,--gc-sections -flinker-output=nolto-rel + +.PHONY: build check clean host test +build: $(BUILD_DIR)/uno.nwa + +# apps get 133652 bytes of RAM on custom builds of Epsilon 25.2 (148928 or more on official ones) +check: $(BUILD_DIR)/uno.nwa + $(Q) $(NWLINK) nwa-bin --ram-length 133652 $< $(BUILD_DIR)/uno.bin + @echo "BIN $(BUILD_DIR)/uno.bin: $$(wc -c < $(BUILD_DIR)/uno.bin) bytes" + +$(BUILD_DIR)/uno.nwa: $(SRC) $(HEADERS) $(BUILD_DIR)/icon.o + @echo "LD $@" + $(Q) $(CC) $(CFLAGS) $(LDFLAGS) $(SRC) $(BUILD_DIR)/icon.o -lm -o $@ + $(Q) arm-none-eabi-strip --strip-unneeded $@ + +$(BUILD_DIR)/icon.o: src/icon.png | $(BUILD_DIR) + @echo "ICON $<" + $(Q) $(NWLINK) png-icon-o $< $@ + +$(BUILD_DIR): + $(Q) mkdir -p $@ + +# on a computer: tests/play.c checks the rules, plays whole games with the bots and random keys, and saves pictures +HOST_SRC = tests/play.c src/game.c src/bot.c src/draw.c src/save.c $(L)/gfx.c $(L)/font_data.c $(L)/xfont.c +HOST_FLAGS = -std=gnu11 -O2 -g -Wall -Wextra -Wno-unused-parameter -ffp-contract=off -Isrc -I$(L) -DHOST +host: build/play +build/play: $(HOST_SRC) src/main.c $(wildcard src/*.h) + $(Q) mkdir -p build + $(Q) cc $(HOST_FLAGS) -fsanitize=address,undefined -fno-sanitize-recover=undefined $(HOST_SRC) -lm -o $@ + @echo "HOST $@" +test: build/play + build/play test build + +clean: + $(Q) rm -rf $(BUILD_DIR) build diff --git a/games/uno/README.md b/games/uno/README.md new file mode 100644 index 00000000..6cfbb98d --- /dev/null +++ b/games/uno/README.md @@ -0,0 +1,88 @@ +> **UNO is a [NumPlay](../../README.md) game that comes on its own.** It is too big to fit next to NumPlay on the calculator. Get `UNO.nwa` from the [latest release](https://github.com/Mason363/NumPlay/releases/latest). + +

+ UNO icon +

+ +

UNO

+ +

+ The card game against one to three bots, on the NumWorks calculator.
+ The official rules, the 108-card deck and scoring to 500 points. +

+ +

+ Download UNO.nwa +  ·  Controls +

+ +

+ UNO: the bots play their cards in turn, then you pick a card in your hand and play it +

+ +## Highlights + +* **The official rules:** the 108-card deck, Skip, Reverse, Draw Two, Wild and Wild Draw Four, with the first card's effect, drawing a card you may play at once, and the two-player rules (Reverse and Skip let you play again). +* **The Wild Draw Four challenge:** it's only allowed with no card of the colour in play, but anyone can bluff. Challenge it and see the hand: a bluffer draws 4, otherwise you draw 6. +* **UNO!** Say it as you play your next-to-last card. Forget, and a bot may catch you for 2 cards. Catch a bot that forgot. +* **Scoring to 500:** the winner of each hand scores the cards left in the others' hands, and the deal moves to the left, until someone has 500 points. Or play a single hand. +* **Bots at three levels:** Easy plays any card that goes; Normal saves its Wilds and stops a player about to go out; Hard also counts the cards it has seen, plays to the colours you lack and may bluff. +* **House rules, if you want them:** stacking Draw Twos or Wild Draw Fours, drawing until you can play, having to play a drawn card, and UNO said for everyone. All off by default. +* **Saved as you go:** Home saves and quits, from anywhere. Your game is also copied into `uno_saves.py`, so installing the app again doesn't erase it. + + + + + + + + + + + + + + + + + + +
Your turn at a table of four
Your turn: the cards that go are lit
Calling a colour
A Wild: call a colour
A Wild Draw Four played on you
A Wild Draw Four: take it or challenge
The challenged hand
The challenged hand, shown to you
Two cards left
Two cards left: time to say UNO
The scores after a hand
A hand won, on the way to 500
A new game
A new game: players, bots, length and house rules
The title screen
The title screen
+ +## How to play + +Match the top card of the discard pile by colour, number or symbol, or play a Wild, which goes on anything. You can also draw a card instead; if it goes, you may play it at once. The coloured frame round the discard pile shows the colour to match, also after a Wild. + +Skip makes the next player miss a turn, Reverse turns play round, and Draw Two makes the next player draw 2 and miss a turn. With a Wild you call the colour. A Wild Draw Four also makes the next player draw 4 and miss a turn, and it's only allowed when you have no card of the colour in play. The next player may challenge it: if it was a bluff, its player draws the 4; if not, the challenger draws 6. + +Press **shift** as you play your next-to-last card, to say UNO. If you forget, anyone who catches you before the next player starts makes you draw 2; say it in time and you're safe. When a bot forgets, **shift** catches it. The first to play all their cards wins the hand and scores the cards left in the others' hands: numbers at face value, Skip, Reverse and Draw Two 20, Wilds 50. The first to 500 points wins the game. + +## Controls + +| Key | What it does | +| --- | --- | +| Left and Right | Pick a card in your hand | +| OK | Play the card | +| Up | Draw a card, keep the card you drew, or take the cards you have to | +| shift | Say UNO, or catch a bot that forgot | +| Back | Pause (Resume, New game, How to play, Quit game) | +| Home | Save and quit | + +## Good to know + +* **On its own:** UNO is too big to fit next to NumPlay. Installing it from the NumWorks website swaps out NumPlay and the other apps, and installing NumPlay again swaps it back out. +* **The classic deck:** the 108 cards of the classic game, without the blank cards and Wild Shuffle Hands of newer decks. +* **The table:** you and one to three bots. The original takes up to ten players. + +## Credits + +Inspired by [UNO](https://www.mattel.com/brands/uno), the card game by Mattel, not affiliated with Mattel. Go play the original with your friends, or the official app. + +* The rules and scoring follow Mattel's instruction sheet. The cards are drawn by the game itself, after the classic deck's look, and the text uses NumPlay's **Nunito** font (SIL Open Font License). +* The calculator adaptation is by **[Bobydeluxe](https://github.com/Bobydeluxe)**, who brought it to NumPlay. + +Made by Mason Chen as part of NumPlay. + +## License + +The game's code is licensed under the GNU General Public License v3.0: see the `LICENSE` file at the root of the repository. UNO is a trademark of Mattel. diff --git a/games/uno/docs/challenge.png b/games/uno/docs/challenge.png new file mode 100644 index 00000000..4b5c7a02 Binary files /dev/null and b/games/uno/docs/challenge.png differ diff --git a/games/uno/docs/colour.png b/games/uno/docs/colour.png new file mode 100644 index 00000000..f93acbee Binary files /dev/null and b/games/uno/docs/colour.png differ diff --git a/games/uno/docs/reveal.png b/games/uno/docs/reveal.png new file mode 100644 index 00000000..c664a687 Binary files /dev/null and b/games/uno/docs/reveal.png differ diff --git a/games/uno/docs/scores.png b/games/uno/docs/scores.png new file mode 100644 index 00000000..e9588a2c Binary files /dev/null and b/games/uno/docs/scores.png differ diff --git a/games/uno/docs/setup.png b/games/uno/docs/setup.png new file mode 100644 index 00000000..392608d5 Binary files /dev/null and b/games/uno/docs/setup.png differ diff --git a/games/uno/docs/shot.png b/games/uno/docs/shot.png new file mode 100644 index 00000000..0457c60f Binary files /dev/null and b/games/uno/docs/shot.png differ diff --git a/games/uno/docs/title.png b/games/uno/docs/title.png new file mode 100644 index 00000000..b2778179 Binary files /dev/null and b/games/uno/docs/title.png differ diff --git a/games/uno/docs/uno.png b/games/uno/docs/uno.png new file mode 100644 index 00000000..a91e7309 Binary files /dev/null and b/games/uno/docs/uno.png differ diff --git a/games/uno/src/bot.c b/games/uno/src/bot.c new file mode 100644 index 00000000..35321606 --- /dev/null +++ b/games/uno/src/bot.c @@ -0,0 +1,161 @@ +/* The bots. Each one sees what a player at the table sees: its own hand, the discard pile, how many cards the + * others hold, who drew on which colour. Easy plays any card that goes and often forgets UNO; Normal keeps its + * Wilds, plays its high cards and its biggest colour, and stops a player about to go out; Hard also plays to the + * colours the next player lacks, gets rid of its points when someone is about to win, challenges by counting + * the cards it has seen, and may bluff a Wild Draw Four to stop the last card. */ +#include "game.h" + +static uint32_t brand(uno_game *g) { + uint32_t x = g->brng; + x ^= x << 13, x ^= x >> 17, x ^= x << 5; + return g->brng = x ? x : 0x2545F491u; +} +static bool chance(uno_game *g, int pct) { return (int)(brand(g) % 100) < pct; } + +static int colour_count(const uno_game *g, int p, int col) { + int n = 0; + for (int i = 0; i < g->count[p]; i++) n += uno_color(g->hand[p][i]) == col; + return n; +} + +int bot_best_color(const uno_game *g, int p, int level) { + int s[4] = {0, 0, 0, 0}; + for (int i = 0; i < g->count[p]; i++) { + int c = g->hand[p][i]; + if (c < 52) s[uno_color(c)] += 10 + uno_points(c) / 4; /* the colour it holds most, then the points */ + } + if (level == LEVEL_HARD) { + int nx = uno_next(g, p); + for (int col = 0; col < 4; col++) + if (g->lacks[nx] >> col & 1) s[col] += g->count[nx] <= 3 ? 14 : 4; + } + int best = 0; + for (int col = 1; col < 4; col++) + if (s[col] > s[best]) best = col; + return best; +} + +static bool says_uno(uno_game *g, int p, int level) { + if (g->count[p] != 2) return false; + return level == LEVEL_HARD ? true : !chance(g, level == LEVEL_EASY ? 25 : 8); +} + +static bool attacks(const uno_game *g, int c) { + int v = uno_value(c); + return v == V_SKIP || v == V_DRAW2 || c == C_WILD4 || (v == V_REVERSE && g->r.players == 2); +} + +/* Hard only: an illegal Wild Draw Four, when the next player is down to one card and nothing else stops them */ +static bool bluff_ok(const uno_game *g, int p) { + if (g->count[uno_next(g, p)] != 1) return false; + for (int i = 0; i < g->count[p]; i++) { + int c = g->hand[p][i]; + if (c != C_WILD4 && attacks(g, c) && uno_matches(g, c)) return false; + } + return true; +} + +static int score(const uno_game *g, int p, int i, int level) { + int c = g->hand[p][i], v = uno_value(c), col = uno_color(c), n = g->r.players, nx = uno_next(g, p); + bool threat = g->count[nx] <= 2; + int s; + if (c >= 52) { + s = g->count[p] <= 2 ? 60 : -40; /* Wilds are kept for when nothing else goes */ + if (c == C_WILD4 && !threat) s -= 20; + } else { + s = uno_points(c) + 6 * (colour_count(g, p, col) - 1); /* high cards first, staying in a colour it holds */ + } + if (threat && attacks(g, c)) s += level == LEVEL_HARD ? 120 : 70; + if (v == V_REVERSE && n > 2) { + int back = (p + n - g->dir) % n; /* who plays next after it */ + if (g->count[back] <= 2 && !threat) s -= 60; + else if (threat && g->count[back] > 2) s += 50; + } + if (level == LEVEL_HARD) { + if (col < 4 && (g->lacks[nx] >> col & 1)) s += 15; + bool close = false; + for (int q = 0; q < n; q++) close |= q != p && g->count[q] <= 2; + if (close && !g->r.one_hand) s += uno_points(c) / 2; /* about to lose the hand: shed the points */ + } + return s; +} + +/* how likely the Wild Draw Four's player held the colour before it, from the cards this player has seen, in + * 65536ths: their other cards drawn at random from the ones it hasn't seen */ +static uint32_t guilt(const uno_game *g, int p) { + int col = g->prev_color, unseen = UNO_DECK - g->count[p] - g->pn, left = 25 - colour_count(g, p, col); + for (int i = 0; i < g->pn; i++) left -= uno_color(g->pile[i]) == col; + uint32_t none = 65536; + for (int k = 0; k < g->count[g->offender] && none; k++) { + int a = unseen - left - k, b = unseen - k; + if (b <= 0) break; + none = a <= 0 ? 0 : (uint32_t)((uint64_t)none * (uint32_t)a / (uint32_t)b); + } + return 65536 - none; +} + +static bool challenges(uno_game *g, int level) { + if (level == LEVEL_EASY) return chance(g, 20); + /* honest players play it only without the colour: worth it on strong evidence (62.5%, Normal 80%) */ + uint32_t need = level == LEVEL_HARD ? 40960 : 52429; + return guilt(g, g->turn) >= need; +} + +uno_move bot_move(uno_game *g, int level) { + uno_move m = {M_DRAW, 0, false}; + int p = g->turn; + if (g->phase == P_COLOR) { + m.act = M_COLOR; + m.arg = (uint8_t)(level == LEVEL_EASY && chance(g, 40) ? (int)(brand(g) % 4) : bot_best_color(g, p, level)); + return m; + } + if (g->phase == P_DRAWN) { + int c = g->hand[p][g->drawn]; + bool play = true; + if (!(g->r.house & H_MUST_PLAY)) { + if (c == C_WILD4 && !uno_legal_wild4(g, p) && !(level == LEVEL_HARD && bluff_ok(g, p))) play = false; + else if (level == LEVEL_EASY && chance(g, 15)) play = false; + } + m.act = play ? M_PLAY : M_KEEP, m.arg = g->drawn, m.uno = play && says_uno(g, p, level); + return m; + } + /* P_PLAY, or P_PENALTY with a card to stack */ + int best = -1, best_s = -1000000; + for (int i = 0; i < g->count[p]; i++) { + if (!uno_can_play(g, i)) continue; + int c = g->hand[p][i]; + if (c == C_WILD4 && g->phase == P_PLAY && !uno_legal_wild4(g, p) && !(level == LEVEL_HARD && bluff_ok(g, p))) continue; + int s = level == LEVEL_EASY ? (int)(brand(g) % 1000) : score(g, p, i, level); + if (s > best_s) best_s = s, best = i; + } + if (g->phase == P_PENALTY) { + if (best >= 0 && (level != LEVEL_EASY || chance(g, 70))) { + m.act = M_PLAY, m.arg = (uint8_t)best, m.uno = says_uno(g, p, level); + } else { + m.act = g->challengeable && challenges(g, level) ? M_CHALLENGE : M_TAKE; + } + return m; + } + if (best >= 0) m.act = M_PLAY, m.arg = (uint8_t)best, m.uno = says_uno(g, p, level); + return m; +} + +bool bot_do(uno_game *g, uno_move m) { + switch (m.act) { + case M_PLAY: return uno_play(g, m.arg, m.uno); + case M_DRAW: return uno_draw(g); + case M_KEEP: return uno_keep(g); + case M_TAKE: return uno_take(g); + case M_CHALLENGE: return uno_challenge(g); + default: return uno_choose(g, m.arg); + } +} + +bool bot_catches(uno_game *g, int by, int level, uint32_t *ms) { + static const uint8_t pct[3] = {35, 70, 95}; + static const uint16_t base[3] = {1500, 1000, 600}, spread[3] = {700, 500, 400}; + if (g->uno_open == UNO_NONE || by == g->uno_open) return false; + bool yes = chance(g, pct[level]); + *ms = base[level] + brand(g) % spread[level]; + return yes; +} diff --git a/games/uno/src/draw.c b/games/uno/src/draw.c new file mode 100644 index 00000000..3c44445d --- /dev/null +++ b/games/uno/src/draw.c @@ -0,0 +1,840 @@ +/* The screens. Cards are drawn, not pictures: the white border, the colour, the tilted oval and the big number + * or symbol of the classic deck, sized for 320x240. */ +#include +#include "gfx.h" +#include "draw.h" + +void plat_push(int x, int y, int w, int h, const uint16_t *px); /* (plat.h: its key names are the launcher's too) */ + +void np_push(int x, int y, int w, int h, const color_t *px) { plat_push(x, y, w, h, px); } +static color_t strip[SCREEN_W * STRIP_H]; + +#define WHITE 0xFFFF +#define INK HEX(0x111827) +#define FOCUS HEX(0xFFD43B) +#define MUTED HEX(0xA8B3C7) +#define PANEL HEX(0x1B2436) +#define CARD_BLACK HEX(0x1C1C1E) +static const color_t CARD[5] = {HEX(0xE53935), HEX(0xF7C21B), HEX(0x2EA34F), HEX(0x1E7BD8), HEX(0x1C1C1E)}; +static const char *const COLOR_NAME[4] = {"Red", "Yellow", "Green", "Blue"}; +static const char *const NAMES[4] = {"You", "Bot 1", "Bot 2", "Bot 3"}; + +const char *uno_name(int p) { return p >= 0 && p < 4 ? NAMES[p] : ""; } + +static char *put(char *p, const char *s) { + while (*s) *p++ = *s++; + *p = 0; + return p; +} +static char *num(char *p, unsigned v) { + char t[8]; + int n = 0; + do t[n++] = (char)('0' + v % 10); while ((v /= 10) && n < 8); + while (n) *p++ = t[--n]; + *p = 0; + return p; +} + +void uno_card_name(int c, char *out) { + if (c == C_WILD) { put(out, "Wild"); return; } + if (c == C_WILD4) { put(out, "Wild +4"); return; } + char *p = put(out, COLOR_NAME[uno_color(c)]); + *p++ = ' '; + int v = uno_value(c); + if (v < 10) num(p, (unsigned)v); + else put(p, v == V_SKIP ? "Skip" : v == V_REVERSE ? "Reverse" : "+2"); +} + +/* ------------------------------------------------------------------ shapes, smoothed */ +static inline void put32(int x, int y, color_t c, int t) { + if (t <= 0 || x < 0 || x >= SCREEN_W || y < gfx_y0 || y >= gfx_y1) return; + color_t *d = &gfx_buf[(y - gfx_y0) * SCREEN_W + x]; + *d = t >= 32 ? c : gfx_mix(*d, c, t); +} +static inline void plot(int x, int y, color_t c, float cov) { put32(x, y, c, (int)(cov * 32 + 0.5f)); } +static inline float clamp01(float v) { return v < 0 ? 0 : v > 1 ? 1 : v; } +static inline float len2(float x, float y) { return __builtin_sqrtf(x * x + y * y); } + +/* rows of a box that are in the strip */ +static bool rows(float y0, float y1, int *a, int *b) { + *a = NP_MAX((int)y0 - 1, gfx_y0), *b = NP_MIN((int)y1 + 2, gfx_y1); + return *a < *b; +} + +/* an ellipse turned so that its long axis (a) points up and to the right, as on the cards; quarters: the Wild's + * four colours. Each row: the inside filled at once, the edges smoothed. */ +static void oval(float cx, float cy, float a, float b, color_t c, const color_t *quarters) { + const float ax = 0.5f, ay = -0.866f, bx = 0.866f, by = 0.5f; + float ey = __builtin_sqrtf(a * a * ay * ay + b * b * by * by); + int r0, r1; + if (!rows(cy - ey, cy + ey, &r0, &r1)) return; + /* u^2 + v^2 = 1 as A x^2 + B x y + C y^2 = 1, for the ellipse grown and shrunk by 0.75 pixel */ + float Ao[2], Bo[2], Co[2]; + for (int k = 0; k < 2; k++) { + float aa = a + (k ? -0.75f : 0.75f), bb = b + (k ? -0.75f : 0.75f); + aa = aa < 0.5f ? 0.5f : aa, bb = bb < 0.5f ? 0.5f : bb; + Ao[k] = ax * ax / (aa * aa) + bx * bx / (bb * bb); + Bo[k] = 2 * (ax * ay / (aa * aa) + bx * by / (bb * bb)); + Co[k] = ay * ay / (aa * aa) + by * by / (bb * bb); + } + for (int y = r0; y < r1; y++) { + float dy = y + 0.5f - cy, sp[2][2]; + bool has[2]; + for (int k = 0; k < 2; k++) { + float qb = Bo[k] * dy, disc = qb * qb - 4 * Ao[k] * (Co[k] * dy * dy - 1); + has[k] = disc > 0; + if (has[k]) { + float r = __builtin_sqrtf(disc); + sp[k][0] = cx + (-qb - r) / (2 * Ao[k]), sp[k][1] = cx + (-qb + r) / (2 * Ao[k]); + } + } + if (!has[0]) continue; + int xo0 = (int)sp[0][0] - 1, xo1 = (int)sp[0][1] + 2; + int xi0 = has[1] ? (int)sp[1][0] + 1 : xo1, xi1 = has[1] ? (int)sp[1][1] : xo1; + if (xi1 < xi0) xi0 = xi1 = xo1; + for (int x = NP_MAX(xo0, 0); x < NP_MIN(xo1, SCREEN_W); x++) { + bool inside = x >= xi0 && x < xi1; + float dx = x + 0.5f - cx; + float u = (dx * ax + dy * ay) / a, v = (dx * bx + dy * by) / b; + color_t k = quarters ? quarters[(u < 0) * 2 + ((u < 0) != (v > 0))] : c; + if (inside) { + if (!quarters) { /* the whole inside of the row in one go */ + color_t *d = &gfx_buf[(y - gfx_y0) * SCREEN_W + x]; + for (int n = NP_MIN(xi1, SCREEN_W) - x; n > 0; n--) *d++ = c; + x = NP_MIN(xi1, SCREEN_W) - 1; + } else { + gfx_buf[(y - gfx_y0) * SCREEN_W + x] = k; + } + continue; + } + float r = __builtin_sqrtf(u * u + v * v), l = len2(dx, dy), d = r > 0.001f ? l - l / r : -a; + plot(x, y, k, clamp01(0.5f - d)); + } + } +} + +/* a stroke from (x0, y0) to (x1, y1), w wide, round ends */ +static void stroke(float x0, float y0, float x1, float y1, float w, color_t c) { + float r = w * 0.5f; + int r0, r1; + if (!rows((y0 < y1 ? y0 : y1) - r, (y0 > y1 ? y0 : y1) + r, &r0, &r1)) return; + int xa = (int)((x0 < x1 ? x0 : x1) - r) - 1, xb = (int)((x0 > x1 ? x0 : x1) + r) + 2; + float dx = x1 - x0, dy = y1 - y0, ll = dx * dx + dy * dy; + for (int y = r0; y < r1; y++) + for (int x = xa; x < xb; x++) { + float px = x + 0.5f - x0, py = y + 0.5f - y0; + float t = ll > 0 ? clamp01((px * dx + py * dy) / ll) : 0; + plot(x, y, c, clamp01(0.5f - (len2(px - t * dx, py - t * dy) - r))); + } +} + +static void ring(float cx, float cy, float r, float w, color_t c) { + int r0, r1; + if (!rows(cy - r - w, cy + r + w, &r0, &r1)) return; + for (int y = r0; y < r1; y++) + for (int x = (int)(cx - r - w) - 1; x < (int)(cx + r + w) + 2; x++) { + float d = __builtin_fabsf(len2(x + 0.5f - cx, y + 0.5f - cy) - r) - w * 0.5f; + plot(x, y, c, clamp01(0.5f - d)); + } +} + +static void tri(float ax, float ay, float bx, float by, float qx, float qy, float grow, color_t c) { + float lo = ay < by ? ay : by, hi = ay > by ? ay : by; + lo = lo < qy ? lo : qy, hi = hi > qy ? hi : qy; + float le = ax < bx ? ax : bx, ri = ax > bx ? ax : bx; + le = le < qx ? le : qx, ri = ri > qx ? ri : qx; + lo -= grow, hi += grow, le -= grow, ri += grow; + float area = (bx - ax) * (qy - ay) - (by - ay) * (qx - ax), s = area < 0 ? -1.0f : 1.0f; + float ex[3] = {ax, bx, qx}, ey[3] = {ay, by, qy}; + int r0, r1; + if (!rows(lo, hi, &r0, &r1)) return; + for (int y = r0; y < r1; y++) + for (int x = (int)le - 1; x < (int)ri + 2; x++) { + float px = x + 0.5f, py = y + 0.5f, d = -1e9f; + for (int k = 0; k < 3; k++) { + float x0 = ex[k], y0 = ey[k], x1 = ex[(k + 1) % 3], y1 = ey[(k + 1) % 3]; + float nx = (y1 - y0) * s, ny = -(x1 - x0) * s, nl = len2(nx, ny); + float e = ((px - x0) * nx + (py - y0) * ny) / nl; /* > 0 outside this edge */ + if (e > d) d = e; + } + plot(x, y, c, clamp01(0.5f - (d - grow))); + } +} + +/* text from the font's 4-bit letters, scaled (q8: 256 = its size) and leant (shear: pixels per pixel up), with a + * dark edge e pixels wide (one at the font's own size); scaled letters are smoothed and sharpened back */ +static inline int cov_at(const uint8_t *d, const np_glyph_t *g, int i, int j) { + if ((unsigned)i >= g->w || (unsigned)j >= g->h) return 0; + int n = j * g->w + i; + return d[g->offset + (n >> 1)] >> ((n & 1) * 4) & 15; +} +static float sample(const uint8_t *d, const np_glyph_t *g, float sx, float sy) { + int ix = (int)(sx + 8) - 8, iy = (int)(sy + 8) - 8; + float fx = sx - ix, fy = sy - iy; + return (cov_at(d, g, ix, iy) * (1 - fx) + cov_at(d, g, ix + 1, iy) * fx) * (1 - fy) + + (cov_at(d, g, ix, iy + 1) * (1 - fx) + cov_at(d, g, ix + 1, iy + 1) * fx) * fy; +} +static int big_width(const np_font_t *f, const char *s, int q8) { + int w = 0; + for (; *s; s++) w += f->glyphs[(uint8_t)*s - f->first].advance; + return w * q8 >> 8; +} +static void big_text(const np_font_t *f, int x, int base, const char *s, int q8, float shear, color_t c, color_t edge, + int e) { + int top = base - (f->ascent * q8 >> 8) - 3, bot = base + (f->descent * q8 >> 8) + 3; + if (bot <= gfx_y0 || top >= gfx_y1) return; + const uint8_t *data = f->data; + float k = 256.0f / q8, pen = (float)x, ek = e * k; + for (; *s; s++) { + const np_glyph_t *g = &f->glyphs[(uint8_t)*s - f->first]; + float gx = pen + g->x * q8 / 256.0f, gy = base + g->y * q8 / 256.0f, gw = g->w * q8 / 256.0f, gh = g->h * q8 / 256.0f; + int y0 = NP_MAX((int)gy - 1 - e, gfx_y0), y1 = NP_MIN((int)(gy + gh) + 2 + e, gfx_y1); + for (int y = y0; y < y1; y++) { + float lean = (base - (y + 0.5f)) * shear; + int xa = (int)(gx + lean) - 1 - e, xb = (int)(gx + lean + gw) + 2 + e; + if (q8 == 256) { /* its own size: the letter's pixels, no smoothing needed */ + float fo = gx + lean; + int ox = (int)(fo + 8) - 8, oy = (int)(gy + 0.5f), j = y - oy, fx = (int)((fo - ox) * 16); /* (1/16ths) */ + for (int xx = xa; xx < xb; xx++) { + int i = xx - ox, v = (cov_at(data, g, i, j) * (16 - fx) + cov_at(data, g, i - 1, j) * fx + 8) >> 4; + if (e) { + int m = 0; + for (int dj = -1; dj <= 1; dj++) + for (int di = -1; di <= 1; di++) { + int w = cov_at(data, g, i + di, j + dj); + m = w > m ? w : m; + } + if (m > v) put32(xx, y, edge, (m * 32 + 7) / 15); + } + put32(xx, y, c, (v * 32 + 7) / 15); + } + continue; + } + float sy = (y + 0.5f - gy) * k - 0.5f; + for (int xx = xa; xx < xb; xx++) { + float sx = (xx + 0.5f - gx - lean) * k - 0.5f; + float v = clamp01((sample(data, g, sx, sy) / 15.0f - 0.5f) * 2.2f + 0.5f); + if (e) { + float m = sample(data, g, sx - ek, sy); + float t = sample(data, g, sx + ek, sy); + m = t > m ? t : m, t = sample(data, g, sx, sy - ek), m = t > m ? t : m, t = sample(data, g, sx, sy + ek); + m = clamp01(((t > m ? t : m) / 15.0f - 0.5f) * 2.2f + 0.5f); + if (m > v) plot(xx, y, edge, m); + } + plot(xx, y, c, v); + } + } + pen += g->advance * q8 / 256.0f; + } +} +/* the same, centred */ +static void big_label(const np_font_t *f, int cx, int base, const char *s, int q8, float shear, color_t c, color_t edge, + int e) { + big_text(f, cx - big_width(f, s, q8) / 2, base, s, q8, shear, c, edge, e); +} + +static void text(int x, int y, const char *s, color_t c) { gfx_text(&np_font_small, x, y, s, c, 256); } +static void ctext(const np_font_t *f, int x, int y, const char *s, color_t c) { gfx_text_center(f, x, y, s, c, 256); } + +/* ------------------------------------------------------------------ cards */ +/* the symbol of a card, centred at (cx, cy), s high (s: about the height of a digit) */ +static void symbol(int card, float cx, float cy, float s, color_t c, color_t edge) { + int v = uno_value(card); + float w = s * (v == V_REVERSE ? 0.13f : 0.17f) + 1.0f; + for (int pass = 0; pass < 2; pass++) { + color_t k = pass ? c : edge; + float ww = pass ? w : w + 2.0f; + if (v == V_SKIP) { + float r = s * 0.42f; + ring(cx, cy, r, ww, k); + stroke(cx - r * 0.68f, cy - r * 0.68f, cx + r * 0.68f, cy + r * 0.68f, ww, k); + } else { /* Reverse: two arrows side by side, one each way */ + const float dx = 0.7071f, dy = -0.7071f, nx = 0.7071f, ny = 0.7071f; + bool tiny = s < 14; /* (in a corner: bigger heads) */ + float L = s * (tiny ? 0.55f : 0.44f), hl = s * (tiny ? 0.5f : 0.28f), hw = s * (tiny ? 0.32f : 0.2f), o = s * 0.22f; + float g = pass ? 0 : tiny ? 0.8f : 1.0f; + if (tiny) ww = pass ? 1.4f : 2.8f; + for (int i = 0; i < 2; i++) { + float sg = i ? -1.0f : 1.0f, ox = cx - sg * nx * o, oy = cy - sg * ny * o; + float bx = ox + sg * dx * (L - hl), by = oy + sg * dy * (L - hl); + stroke(ox - sg * dx * L, oy - sg * dy * L, bx, by, ww, k); + tri(ox + sg * dx * L, oy + sg * dy * L, bx + nx * hw, by + ny * hw, bx - nx * hw, by - ny * hw, g, k); + } + } + } +} + +static void value_label(int card, char *b) { + int v = uno_value(card); + if (card == C_WILD4) put(b, "+4"); + else if (card == C_WILD) b[0] = 0; + else if (v == V_DRAW2) put(b, "+2"); + else if (v < 10) num(b, (unsigned)v); + else b[0] = 0; +} + +/* the corner index, top left (and bottom right on big cards) */ +static void corner(int card, int x, int y, int w, int h) { + char b[4]; + value_label(card, b); + int v = uno_value(card); + bool small = w < 44; + const np_font_t *f = small ? &np_font_small : &np_font_body; + int ix = x + (small ? 3 : 5), iy = y + (small ? 11 : 15); + for (int k = 0; k < (small ? 1 : 2); k++) { + int px = k ? x + w - (small ? 3 : 5) : ix, py = k ? y + h - (small ? 5 : 6) : iy; + if (card != C_WILD && (v < 10 || v == V_DRAW2 || card == C_WILD4)) { + int tw = gfx_text_width(f, b); + int tx = k ? px - tw : px; + gfx_text(f, tx + 1, py + 1, b, CARD_BLACK, 200); + gfx_text(f, tx, py, b, WHITE, 256); + } else if (card != C_WILD) { + float s = small ? 10.0f : 12.0f, cx = k ? px - s * 0.5f : px + s * 0.5f, cy = py - s * 0.4f; + symbol(card, cx, cy, s, WHITE, CARD_BLACK); + } + } +} + +static void card_face(int card, int x, int y, int w, int h) { + if (y >= gfx_y1 || y + h <= gfx_y0) return; + int r = w / 6 + 1, bd = w >= 44 ? 3 : 2; + gfx_rrect(x - 1, y - 1, w + 2, h + 2, r + 1, HEX(0x374151), 150); + gfx_rrect(x, y, w, h, r, WHITE, 256); + int col = uno_color(card); + gfx_rrect(x + bd, y + bd, w - 2 * bd, h - 2 * bd, r - 1, CARD[col], 256); + float cx = x + w * 0.5f, cy = y + h * 0.5f, a = (h - 2 * bd) * 0.47f, b = (w - 2 * bd) * 0.37f; + const np_font_t *big = &np_font_title; + int q8 = w >= 44 ? 360 : 256; /* the number's size */ + if (card == C_WILD) { + static const color_t quarters[4] = {HEX(0xE53935), HEX(0x1E7BD8), HEX(0xF7C21B), HEX(0x2EA34F)}; + oval(cx, cy, a, b, WHITE, 0); + oval(cx, cy, a - 1.5f, b - 1.5f, 0, quarters); + } else if (card == C_WILD4) { + /* four little cards, one of each colour */ + static const uint8_t order[4] = {BLUE, GREEN, RED, YELLOW}; + int mw = w * 26 / 100 + 1, mh = h * 30 / 100 + 1; + for (int i = 0; i < 4; i++) { + int mx = (int)(cx - mw * 0.5f + (i - 1.5f) * w * 0.12f), my = (int)(cy - mh * 0.5f + (1.5f - i) * h * 0.06f); + gfx_rrect(mx, my, mw, mh, 2, WHITE, 256); + gfx_rrect(mx + 1, my + 1, mw - 2, mh - 2, 2, CARD[order[i]], 256); + } + } else { + oval(cx, cy, a, b, WHITE, 0); + int v = uno_value(card); + if (v == V_SKIP || v == V_REVERSE) { + symbol(card, cx, cy, h * 0.4f, CARD[col], CARD_BLACK); + } else { + char t[4]; + value_label(card, t); + int base = (int)(cy + (big->ascent * q8 >> 8) * 0.5f); + big_label(big, (int)cx, base, t, q8, 0.18f, CARD[col], CARD_BLACK, 1); + if (v == 6 || v == 9) gfx_fill((int)cx - 3, base + 2, 7, 2, CARD[col]); /* 6 and 9 underlined */ + } + } + corner(card, x, y, w, h); +} + +static void card_back(int x, int y, int w, int h) { + if (y >= gfx_y1 || y + h <= gfx_y0) return; + int r = w / 6 + 1, bd = w >= 30 ? 3 : w >= 16 ? 2 : 1; + if (w >= 16) gfx_rrect(x - 1, y - 1, w + 2, h + 2, r + 1, HEX(0x374151), 150); + gfx_rrect(x, y, w, h, r, WHITE, 256); + gfx_rrect(x + bd, y + bd, w - 2 * bd, h - 2 * bd, r - 1, CARD_BLACK, 256); + float cx = x + w * 0.5f, cy = y + h * 0.5f; + oval(cx, cy, (h - 2 * bd) * 0.45f, (w - 2 * bd) * 0.36f, CARD[RED], 0); + if (w >= 30) { + int q8 = w * 256 / 66; + big_label(&np_font_title, (int)cx, (int)(cy + (np_font_title.ascent * q8 >> 8) * 0.5f), "UNO", q8, 0.2f, + CARD[YELLOW], CARD_BLACK, 1); + } +} + +static void dim_card(int x, int y, int w, int h) { gfx_rrect(x, y, w, h, w / 6 + 1, HEX(0x0B1020), 150); } + +/* ------------------------------------------------------------------ pieces of the screens */ +static void background(void) { gfx_vgrad(0, 0, SCREEN_W, SCREEN_H, 0x23304A, 0x0F1626); } +static void table_bg(void) { gfx_vgrad(0, 0, SCREEN_W, SCREEN_H, 0x1F6B4A, 0x0B3324); } + +static void panel(int x, int y, int w, int h) { + gfx_shadow(x, y, w, h, 10, 6, 160); + gfx_rrect(x, y, w, h, 10, PANEL, 250); + gfx_rrect_outline(x, y, w, h, 10, 1, HEX(0x3B4A66), 256); +} + +static void button(int x, int y, int w, int h, const char *s, bool focus) { + gfx_rrect(x, y, w, h, h / 2, focus ? FOCUS : HEX(0x2A3650), 256); + if (focus) gfx_rrect_outline(x - 2, y - 2, w + 4, h + 4, h / 2 + 2, 2, WHITE, 256); + ctext(&np_font_body, x + w / 2, y + h / 2 + 5, s, focus ? INK : WHITE); +} + +/* the key line: "key|what|key|what...", keys in yellow */ +static int keyline_draw(const char *s, bool draw) { + char b[24]; + int x = 6, i = 0; + while (*s) { + int n = 0; + while (*s && *s != '|' && n < 23) b[n++] = *s++; + b[n] = 0; + if (*s == '|') s++; + if (draw) gfx_text(&np_font_small, x, 236, b, i % 2 ? WHITE : FOCUS, 256); + x += gfx_text_width(&np_font_small, b) + (i % 2 ? 9 : 4); + i++; + } + return x - 9; +} +int uno_keyline_width(const char *s) { return keyline_draw(s, false); } +int uno_text_width(const char *s) { return gfx_text_width(&np_font_small, s); } +static void keyline(const char *s) { + gfx_fill(0, 226, SCREEN_W, 14, HEX(0x0A0F1A)); + keyline_draw(s, true); +} + +static void logo(int cx, int cy, int q8) { + oval((float)cx, (float)cy, 30.0f * q8 / 256, 52.0f * q8 / 256, CARD_BLACK, 0); + oval((float)cx, (float)cy, 27.0f * q8 / 256, 49.0f * q8 / 256, CARD[RED], 0); + int tq = q8 * 2; + big_label(&np_font_title, cx, cy + (np_font_title.ascent * tq >> 8) / 2, "UNO", tq, 0.22f, CARD[YELLOW], CARD_BLACK, 2); +} + +/* ------------------------------------------------------------------ the table */ +typedef struct { int x, y; } seat_t; +static seat_t seat(int n, int p) { + if (n == 2) return (seat_t){123, 4}; + if (n == 3) return p == 1 ? (seat_t){4, 50} : (seat_t){242, 50}; + return p == 1 ? (seat_t){4, 56} : p == 2 ? (seat_t){123, 4} : (seat_t){242, 56}; +} + +static void opponent(const uno_game *g, int p, bool live) { + seat_t s = seat(g->r.players, p); + int w = 74, h = 62, x = s.x, y = s.y; + if (y >= gfx_y1 || y + h <= gfx_y0) return; + bool turn = live && g->turn == p && g->phase < P_HAND_OVER; + gfx_rrect(x, y, w, h, 9, HEX(0x0B1A14), 150); + if (turn) gfx_rrect_outline(x - 2, y - 2, w + 4, h + 4, 11, 3, FOCUS, 256); + gfx_text(&np_font_body, x + 6, y + 15, uno_name(p), turn ? FOCUS : WHITE, 256); + char b[8]; + if (!g->r.one_hand) { + num(b, g->score[p]); + gfx_text_right(&np_font_small, x + w - 6, y + 14, b, MUTED, 256); + } + int n = g->count[p], fan = NP_MIN(n, 6); + for (int i = 0; i < fan; i++) card_back(x + 6 + i * 5, y + 21, 13, 19); + num(b, (unsigned)n); + big_label(&np_font_title, x + w - 16, y + 41, b, 256, 0, WHITE, 0, 0); + if (live && g->uno_open == p) { + gfx_rrect(x + 6, y + 44, w - 12, 14, 7, FOCUS, 256); + ctext(&np_font_small, x + w / 2, y + 55, "Forgot UNO!", INK); + } else if (n == 1) { + gfx_rrect(x + 6, y + 44, w - 12, 14, 7, CARD[RED], 256); + ctext(&np_font_small, x + w / 2, y + 55, "UNO!", WHITE); + } +} + +/* two arrows round in a circle: the way play goes */ +static void direction(int cx, int cy, int dir) { + float r = 11; + for (int pass = 0; pass < 2; pass++) { + color_t c = pass ? HEX(0xE8F0EA) : HEX(0x0B3324); + float w = pass ? 2.5f : 4.5f, g = pass ? 0 : 1.2f; + for (int i = 0; i < 2; i++) { + float a0 = i * 3.14159f + 0.5f, a1 = a0 + 1.75f, px = 0, py = 0; + for (int k = 0; k <= 8; k++) { + float a = a0 + (a1 - a0) * k / 8, qx = cx + r * __builtin_cosf(a), qy = cy + r * __builtin_sinf(a) * (float)dir; + if (k) stroke(px, py, qx, qy, w, c); + px = qx, py = qy; + } + /* the head, along the circle at its end */ + float tx = -__builtin_sinf(a1), ty = __builtin_cosf(a1) * (float)dir, nx = __builtin_cosf(a1), ny = __builtin_sinf(a1) * (float)dir; + tri(px + tx * 6, py + ty * 6, px + nx * 4.5f, py + ny * 4.5f, px - nx * 4.5f, py - ny * 4.5f, g, c); + } + } +} + +static int hand_layout(int n, int pick, int *first, int *shown) { + int maxn = 28; + *shown = NP_MIN(n, maxn); + *first = NP_CLAMP(pick - *shown / 2, 0, n - *shown); + return *shown > 1 ? NP_MIN(40, (310 - 36) / (*shown - 1)) : 0; +} + +static void your_hand(const uno_game *g, const uno_ui *v) { + int n = g->count[0]; + if (!n || gfx_y1 <= 156) return; + int first, shown, sp = hand_layout(n, v->pick, &first, &shown); + int w = 36, h = 54, total = w + (shown - 1) * sp, x0 = (SCREEN_W - total) / 2; + bool mine = g->turn == 0 && g->phase < P_HAND_OVER && g->phase != P_COLOR; + bool panel_up = g->phase == P_PENALTY && g->turn == 0 && g->challengeable; + for (int k = 0; k < shown; k++) { + int i = first + k, x = x0 + k * sp, y = i == v->pick ? 160 : 168; + if (i == v->pick) continue; + card_face(g->hand[0][i], x, y, w, h); + if (mine && !panel_up && !uno_can_play(g, i)) dim_card(x, y, w, h); + } + if (v->pick >= first && v->pick < first + shown) { + int x = x0 + (v->pick - first) * sp, y = 160; + card_face(g->hand[0][v->pick], x, y, w, h); + if (mine && !panel_up && !uno_can_play(g, v->pick)) dim_card(x, y, w, h); + gfx_rrect_outline(x - 3, y - 3, w + 6, h + 6, 9, 3, mine ? FOCUS : WHITE, 256); + } + if (first) text(1, 200, "<", FOCUS); + if (first + shown < n) text(313, 200, ">", FOCUS); +} + +static void uno_button(const uno_game *g, const uno_ui *v) { + (void)v; + int x = 262, y = 137, w = 54, h = 18; + if (y >= gfx_y1 || y + h <= gfx_y0) return; + bool catchable = g->uno_open != UNO_NONE && g->uno_open != 0; + bool mine = g->uno_open == 0 || (g->turn == 0 && g->count[0] == 2 && g->uno_said != 0 && + (g->phase == P_PLAY || g->phase == P_DRAWN || g->phase == P_PENALTY)); + if (g->phase >= P_HAND_OVER) catchable = mine = false; + if (catchable) { + gfx_rrect(x, y, w, h, 9, FOCUS, 256); + ctext(&np_font_body, x + w / 2, y + 14, "Catch!", INK); + } else if (mine) { + gfx_rrect(x, y, w, h, 9, CARD[RED], 256); + gfx_rrect_outline(x - 2, y - 2, w + 4, h + 4, 11, 2, FOCUS, 256); + ctext(&np_font_body, x + w / 2, y + 14, "UNO!", WHITE); + } else { + gfx_rrect_outline(x, y, w, h, 9, 1, HEX(0x5C8A73), 256); + ctext(&np_font_body, x + w / 2, y + 14, g->uno_said == 0 ? "Said!" : "UNO", HEX(0x7FA893)); + } +} + +static void info(const uno_game *g) { + if (gfx_y0 >= 44) return; + char b[24], *p; + if (g->r.one_hand) { + text(6, 14, "One hand", MUTED); + } else { + p = put(b, "Hand "); + num(p, g->hands); + text(6, 14, b, MUTED); + text(6 + gfx_text_width(&np_font_small, b) + 6, 14, "to 500", HEX(0x7FA893)); + p = put(b, "You "); + num(p, g->score[0]); + gfx_text(&np_font_body, 6, 32, b, WHITE, 256); + } +} + +static void piles(const uno_game *g) { + if (gfx_y0 >= 142 || gfx_y1 <= 46) return; + card_back(96, 58, 40, 58); + char b[8]; + num(b, g->dn); + ctext(&np_font_small, 116, 128, b, MUTED); + int top = g->pile[g->pn - 1], col = g->color; + if (col < 4) gfx_rrect_outline(143, 47, 60, 84, 12, 4, CARD[col], 256); + card_face(top, 147, 50, 52, 78); + if (col < 4) ctext(&np_font_small, 173, 140, COLOR_NAME[col], col == RED ? HEX(0xFF8A80) : CARD[col]); + if (g->r.players > 2) direction(222, 88, g->dir); +} + +static void color_picker(const uno_ui *v) { + panel(40, 54, 240, 112); + ctext(&np_font_body, 160, 76, "Call a colour", WHITE); + for (int i = 0; i < 4; i++) { + int x = 56 + i * 54, y = 88, s = 44; + bool f = v->sel == i; + if (f) y -= 3, gfx_rrect_outline(x - 4, y - 4, s + 8, s + 8, 12, 3, WHITE, 256); + gfx_rrect(x, y, s, s, 9, CARD[i], 256); + ctext(&np_font_small, x + s / 2, 152, COLOR_NAME[i], f ? WHITE : MUTED); + } +} + +int uno_pen_count(const uno_game *g) { return 2 + uno_can_stack(g); } + +static void penalty_panel(const uno_game *g, const uno_ui *v) { + panel(36, 40, 248, 136); + char b[48], *p = put(b, uno_name(g->offender)); + put(p, " played a Wild Draw Four"); + ctext(&np_font_body, 160, 60, b, WHITE); + p = put(b, "The colour was "); + put(p, COLOR_NAME[g->prev_color]); + ctext(&np_font_small, 160, 78, b, MUTED); + int n = uno_pen_count(g); + for (int i = 0; i < n; i++) { + int item = i; + const char *s = item == PEN_TAKE ? "Take 4 cards" : item == PEN_CHALLENGE ? "Challenge: a bluff?" : "Play your Wild Draw Four"; + if (item == PEN_TAKE) { + p = put(b, "Take "); + p = num(p, g->pending); + put(p, " cards"); + s = b; + } + button(56, 88 + i * 28, 208, 22, s, v->sel == i); + } +} + +static void end_panel(const uno_game *g, const uno_ui *v) { + bool over = g->phase == P_GAME_OVER; + int n = g->r.players, h = 96 + n * 18; + int y = (226 - h) / 2; + panel(28, y, 264, h); + char b[48], *p; + p = put(b, uno_name(g->winner)); + put(p, g->winner ? (over ? " wins the game" : " wins the hand") : (over ? " win the game!" : " win the hand!")); + ctext(&np_font_body, 160, y + 22, b, g->winner == 0 ? FOCUS : WHITE); + p = put(b, "+"); + p = num(p, g->gained); + put(p, " points"); + ctext(&np_font_small, 160, y + 40, b, MUTED); + for (int q = 0; q < n; q++) { + int ry = y + 50 + q * 18; + bool w = q == g->winner; + gfx_text(&np_font_body, 48, ry + 13, uno_name(q), w ? FOCUS : WHITE, 256); + num(b, g->score[q]); + gfx_text_right(&np_font_body, 140, ry + 13, b, w ? FOCUS : WHITE, 256); + if (!g->r.one_hand) { + int bw = 120, fill = NP_MIN(bw, g->score[q] * bw / UNO_GOAL); + gfx_rrect(152, ry + 5, bw, 8, 4, HEX(0x2A3650), 256); + if (fill > 0) gfx_rrect(152, ry + 5, NP_MAX(fill, 8), 8, 4, w ? FOCUS : HEX(0x7FA8D8), 256); + } + } + int by = y + 56 + n * 18; + if (over) { + button(48, by, 104, 24, "Play again", v->sel == 0); + button(168, by, 104, 24, "Menu", v->sel == 1); + } else { + button(90, by, 140, 24, "Next hand", true); + } +} + +static void reveal_panel(const uno_ui *v) { + panel(16, 36, 288, 160); + char b[56], *p = put(b, uno_name(v->reveal_who)); + put(p, "'s hand"); + ctext(&np_font_body, 160, 58, b, WHITE); + int n = v->reveal_n, sp = n > 1 ? NP_MIN(32, 228 / (n - 1)) : 0, w = 30, h = 45, x0 = 160 - (w + (n - 1) * sp) / 2; + for (int i = 0; i < n; i++) { + int c = v->reveal[i], x = x0 + i * sp; + card_face(c, x, 70, w + 2, h); + if (uno_color(c) != v->reveal_color) dim_card(x, 70, w + 2, h); + } + p = v->reveal_guilty ? put(b, "It had ") : put(b, "No "); + p = put(p, COLOR_NAME[v->reveal_color]); + put(p, v->reveal_guilty ? ": a bluff" : ": no bluff"); + ctext(&np_font_body, 160, 140, b, v->reveal_guilty ? FOCUS : WHITE); + p = v->reveal_guilty ? put(b, uno_name(v->reveal_who)) : put(b, "You"); + put(p, v->reveal_guilty ? " takes 4 cards, and you play" : " take 6 cards"); + ctext(&np_font_small, 160, 160, b, MUTED); +} + +static void table(const uno_game *g, const uno_ui *v) { + table_bg(); + bool live = v->screen == V_PLAY || v->screen == V_REVEAL; + info(g); + for (int p = 1; p < g->r.players; p++) opponent(g, p, live); + piles(g); + if (gfx_y1 > 140 && gfx_y0 < 158) { + ctext(&np_font_small, 130, 152, v->msg, WHITE); + uno_button(g, v); + } + if (g->turn == 0 && g->phase < P_HAND_OVER && gfx_y1 > 156) { /* your turn: your side lights up */ + gfx_fill_alpha(0, 157, SCREEN_W, 69, FOCUS, 30); + gfx_fill(0, 156, SCREEN_W, 2, FOCUS); + } + your_hand(g, v); + if (v->screen != V_PLAY) return; + if (g->phase == P_COLOR && g->turn == 0) color_picker(v); + else if (g->phase == P_PENALTY && g->turn == 0 && g->challengeable) penalty_panel(g, v); + else if (g->phase >= P_HAND_OVER && v->show_end) end_panel(g, v); +} + +/* ------------------------------------------------------------------ the other screens */ +int uno_menu_count(const uno_ui *v) { return v->has_game ? 3 : 2; } +int uno_menu_item(const uno_ui *v, int n) { return v->has_game ? n : n + 1; } + +static void title_screen(const uno_ui *v) { + background(); + /* a fan of cards behind the name */ + static const uint8_t fan[5] = {7, 13 + V_SKIP, 26 + V_REVERSE, 39 + V_DRAW2, C_WILD4}; + for (int i = 0; i < 5; i++) { + int x = 180 + i * 22, y = 26 + (i == 2 ? 0 : i == 1 || i == 3 ? 4 : 10); + card_face(fan[i], x, y, 44, 66); + } + logo(84, 62, 250); + int n = uno_menu_count(v); + static const char *const items[3] = {"Continue", "New game", "How to play"}; + for (int i = 0; i < n; i++) button(60, 124 + i * 32, 200, 24, items[uno_menu_item(v, i)], v->sel == i); +} + +static const char *const LEVELS[3] = {"Easy", "Normal", "Hard"}; +static const char *const HOUSE[5] = {"Stack Draw Twos", "Stack Wild Draw Fours", "Draw until you can play", + "Must play a drawn card", "UNO said for everyone"}; + +static void setup_screen(const uno_ui *v) { + background(); + ctext(&np_font_body, 160, 20, "New game", WHITE); + const uno_rules *r = &v->setup; + char b[32], *p; + for (int i = 0; i < 3; i++) { + int y = 28 + i * 21; + bool f = v->sel == i; + gfx_rrect(10, y, 300, 19, 8, f ? FOCUS : HEX(0x1E2940), 256); + static const char *const label[3] = {"Players", "Bots", "Game"}; + gfx_text(&np_font_body, 20, y + 14, label[i], f ? INK : WHITE, 256); + if (i == 0) { + p = put(b, "You + "); + p = num(p, (unsigned)(r->players - 1)); + put(p, r->players == 2 ? " bot" : " bots"); + } else if (i == 1) { + put(b, LEVELS[r->level]); + } else { + put(b, r->one_hand ? "One hand" : "To 500 points"); + } + gfx_text_right(&np_font_body, f ? 286 : 298, y + 14, b, f ? INK : WHITE, 256); + if (f) text(292, y + 13, ">", INK), text(286 - gfx_text_width(&np_font_body, b) - 10, y + 13, "<", INK); + } + text(14, 103, "House rules", MUTED); + for (int k = 0; k < 5; k++) { + int y = 107 + k * 18, row = ROW_HOUSE + k; + bool f = v->sel == row, on = r->house >> k & 1; + gfx_rrect(10, y, 300, 16, 7, f ? FOCUS : HEX(0x1E2940), 256); + gfx_text(&np_font_small, 20, y + 12, HOUSE[k], f ? INK : WHITE, 256); + gfx_rrect(262, y + 2, 40, 12, 6, on ? HEX(0x2EA34F) : f ? HEX(0xC9A92C) : HEX(0x37425A), 256); + ctext(&np_font_small, 282, y + 12, on ? "On" : "Off", WHITE); + } + button(90, 199, 140, 22, "Deal", v->sel == ROW_DEAL); +} + +static const char *const HELP_TITLE[UNO_HELP_PAGES] = {"Your turn", "Action cards", "Wild Draw Four", "UNO!", "Scoring"}; +static const char *const HELP[UNO_HELP_PAGES] = { + "Match the top card by colour, number or symbol, or play a Wild. Or draw a card, and play it if it goes. " + "Play all your cards to win the hand.", + "Skip: the next player misses a turn. Reverse: play changes direction (a Skip with two players). Draw Two: " + "the next player draws 2 and misses a turn.", + "Call a colour; the next player draws 4 and misses a turn. Only allowed with no card of the colour in play. " + "Challenge it: a bluffer draws 4, else you draw 6.", + "Press shift as you play your next-to-last card. Caught without it before the next player starts: draw 2. " + "Shift also catches a bot that forgot.", + "The winner scores the cards left in the others' hands: numbers as shown, Skip, Reverse, Draw Two 20, Wilds " + "50. First to 500 points wins."}; + +/* where page `page`'s text ends (its last baseline + a line): for the tests */ +int uno_help_bottom(int page) { + int y0 = gfx_y0, y1 = gfx_y1; + gfx_y0 = gfx_y1 = -100; + int end = gfx_paragraph(&np_font_body, 160, 86, 268, 19, HELP[page], WHITE, 256); + gfx_y0 = y0, gfx_y1 = y1; + return end; +} + +static void help_screen(const uno_ui *v) { + background(); + panel(12, 10, 296, 208); + char b[24], *p = put(b, "How to play "); + p = num(p, (unsigned)v->page + 1); + p = put(p, "/"); + num(p, UNO_HELP_PAGES); + ctext(&np_font_small, 160, 28, b, MUTED); + ctext(&np_font_title, 160, 56, HELP_TITLE[v->page], FOCUS); + gfx_paragraph(&np_font_body, 160, 86, 268, 19, HELP[v->page], WHITE, 256); + /* a card to look at on each page */ + static const uint8_t show[UNO_HELP_PAGES][3] = {{3, 16, C_WILD}, + {V_SKIP, 13 + V_REVERSE, 26 + V_DRAW2}, + {C_WILD4, C_WILD4, C_WILD4}, + {39 + 5, 39 + 5, 39 + 5}, + {9, 26 + V_SKIP, C_WILD}}; + int n = show[v->page][0] == show[v->page][1] ? 1 : 3; + for (int i = 0; i < n; i++) card_face(show[v->page][i], 160 - (n * 40 - 4) / 2 + i * 40, 158, 36, 54); +} + +static void scene(void *ctx) { + const void *const *pair = ctx; + const uno_game *g = pair[0]; + const uno_ui *v = pair[1]; + switch (v->screen) { + case V_TITLE: title_screen(v); break; + case V_SETUP: setup_screen(v); break; + case V_HELP: help_screen(v); break; + default: table(g, v); break; + } + if (v->screen == V_REVEAL) reveal_panel(v); + if (v->screen == V_PAUSE) { + panel(60, 40, 200, 160); + ctext(&np_font_body, 160, 62, "Paused", WHITE); + static const char *const items[PAUSE_COUNT] = {"Resume", "New game", "How to play", "Quit game"}; + for (int i = 0; i < PAUSE_COUNT; i++) button(84, 76 + i * 30, 152, 24, items[i], v->sel == i); + } else if (v->screen == V_QUIT) { + panel(50, 66, 220, 104); + ctext(&np_font_body, 160, 90, "Quit the game?", WHITE); + ctext(&np_font_small, 160, 108, "It's saved for next time", MUTED); + button(64, 122, 92, 24, "Keep playing", v->sel == 0); + button(164, 122, 92, 24, "Quit", v->sel == 1); + } + if (gfx_y1 > 226) keyline(uno_keys(g, v)); +} + +const char *uno_keys(const uno_game *g, const uno_ui *v) { + switch (v->screen) { + case V_TITLE: return "Up Down|choose|OK|open|Back|quit"; + case V_SETUP: return v->sel < ROW_HOUSE ? "Up Down|choose|Left Right|change|OK|deal|Back|menu" : + v->sel < ROW_DEAL ? "Up Down|choose|OK|on or off|Back|menu" : "Up Down|choose|OK|deal|Back|menu"; + case V_HELP: return v->first_run ? "Left Right|pages|OK|next|Back|skip" : "Left Right|pages|Back|close"; + case V_REVEAL: return "OK|go on"; + case V_PAUSE: return "Up Down|choose|OK|select|Back|resume"; + case V_QUIT: return "Left Right|choose|OK|select"; + default: break; + } + if (g->phase >= P_HAND_OVER) return !v->show_end ? "Back|menu" : g->phase == P_GAME_OVER ? "Left Right|choose|OK|select" : "OK|next hand|Back|menu"; + bool catchable = g->uno_open != UNO_NONE && g->uno_open != 0; + if (g->turn != 0) + return catchable ? "Left Right|look|shift|catch!|Back|menu" : g->uno_open == 0 ? "Left Right|look|shift|UNO!|Back|menu" : + "Left Right|look at your cards|Back|menu"; + switch (g->phase) { + case P_COLOR: return "Left Right|colour|OK|call it"; + case P_DRAWN: + return g->r.house & H_MUST_PLAY ? "OK|play the new card|shift|UNO|Back|menu" : "OK|play it|Up|keep it|shift|UNO|Back|menu"; + case P_PENALTY: + if (g->challengeable) return "Up Down|choose|OK|select"; + return "Left Right|pick|OK|stack|Up|take|Back|menu"; + default: + if (catchable) return "Left Right|pick|OK|play|Up|draw|shift|catch!"; + if (g->count[0] == 2) return "Left Right|pick|OK|play|Up|draw|shift|UNO!"; + return "Left Right|pick|OK|play|Up|draw|Back|menu"; + } +} + +void uno_render(const uno_game *g, const uno_ui *v) { + const void *pair[2] = {g, v}; + color_t *old = gfx_buf; + gfx_buf = strip; + gfx_render(scene, (void *)pair, 0, SCREEN_H); + gfx_buf = old; +} + +#ifdef HOST +/* every kind of card, at the hand's size and the discard pile's: for looking at them */ +static void cards_scene(void *ctx) { + (void)ctx; + table_bg(); + static const uint8_t kinds[15] = {0, 14, 28, 42, 4, 18, 32, 46, V_SKIP, 13 + V_REVERSE, 26 + V_DRAW2, C_WILD, C_WILD4, 39 + 6, 9}; + for (int i = 0; i < 8; i++) card_face(kinds[i], 4 + i * 39, 4, 36, 54); + for (int i = 8; i < 15; i++) card_face(kinds[i], 4 + (i - 8) * 39, 62, 36, 54); + for (int i = 0; i < 6; i++) card_face(kinds[8 + i], 4 + i * 53, 122, 52, 78); + card_back(4 + 6 * 53, 160, 13, 19); +} +/* the app's icon, 55 x 56, over a background colour (twice: black and white make its transparency) */ +static color_t icon_bg; +static void icon_scene(void *ctx) { + (void)ctx; + gfx_fill(0, gfx_y0, SCREEN_W, gfx_y1 - gfx_y0, icon_bg); + gfx_rrect(0, 0, 55, 56, 12, HEX(0x1F6B4A), 256); + card_face(13 + 1, 3, 11, 25, 37); + card_face(7, 13, 6, 25, 37); + card_back(23, 9, 30, 44); +} +void uno_render_icon(color_t bg) { + color_t *old = gfx_buf; + gfx_buf = strip, icon_bg = bg; + gfx_render(icon_scene, 0, 0, 64); + gfx_buf = old; +} +void uno_render_cards(void) { + color_t *old = gfx_buf; + gfx_buf = strip; + gfx_render(cards_scene, 0, 0, SCREEN_H); + gfx_buf = old; +} +#endif diff --git a/games/uno/src/draw.h b/games/uno/src/draw.h new file mode 100644 index 00000000..921c88b5 --- /dev/null +++ b/games/uno/src/draw.h @@ -0,0 +1,40 @@ +/* The screens (draw.c), drawn with NumPlay's launcher graphics and fonts, in strips. */ +#ifndef UNO_DRAW_H +#define UNO_DRAW_H +#include "game.h" + +enum { V_TITLE, V_SETUP, V_HELP, V_PLAY, V_REVEAL, V_PAUSE, V_QUIT }; +enum { MENU_CONTINUE, MENU_NEW, MENU_HELP }; +enum { PAUSE_RESUME, PAUSE_NEW, PAUSE_HELP, PAUSE_QUIT, PAUSE_COUNT }; +enum { ROW_PLAYERS, ROW_LEVEL, ROW_LENGTH, ROW_HOUSE, ROW_DEAL = ROW_HOUSE + 5, SETUP_ROWS }; +enum { PEN_TAKE, PEN_CHALLENGE, PEN_STACK }; +#define UNO_HELP_PAGES 5 + +typedef struct { + uint8_t screen, under; /* under: what a pause or how to play sits on */ + uint8_t sel; /* the menu's or the panel's choice */ + uint8_t page; /* how to play */ + uint8_t first_run; /* how to play before the first game: OK deals */ + uint8_t pick; /* the card chosen in your hand */ + uint8_t has_game; + uint8_t show_end; /* the hand's end: the scores are up */ + uno_rules setup; + /* a challenge you made: the hand you were shown */ + uint8_t reveal[UNO_DECK], reveal_n, reveal_who, reveal_color, reveal_guilty; + char msg[72]; + uint32_t clock; +} uno_ui; + +const char *uno_name(int p); +void uno_card_name(int card, char *out); +const char *uno_keys(const uno_game *g, const uno_ui *v); +void uno_render(const uno_game *g, const uno_ui *v); +int uno_keyline_width(const char *keys); /* (pixels) */ +int uno_text_width(const char *s); /* the small font's */ +int uno_help_bottom(int page); +/* the menus' items: how many there are, and the n-th */ +int uno_menu_count(const uno_ui *v); +int uno_menu_item(const uno_ui *v, int n); +/* the +4 panel's choices: PEN_TAKE, PEN_CHALLENGE and, stacking, PEN_STACK */ +int uno_pen_count(const uno_game *g); +#endif diff --git a/games/uno/src/game.c b/games/uno/src/game.c new file mode 100644 index 00000000..d5eaf4ad --- /dev/null +++ b/games/uno/src/game.c @@ -0,0 +1,417 @@ +/* UNO's rules: see game.h, and the instruction sheet's numbers in the spec kept with the reference. */ +#include +#include "game.h" + +static uint32_t next_rand(uint32_t *s) { + uint32_t x = *s; + x ^= x << 13, x ^= x >> 17, x ^= x << 5; + return *s = x ? x : 0x6C8E9CF5u; +} + +int uno_points(int c) { + if (c >= 52) return 50; + int v = uno_value(c); + return v >= 10 ? 20 : v; +} + +int uno_next(const uno_game *g, int p) { + int n = g->r.players; + return (p + n + g->dir) % n; +} + +static void event(uno_game *g, int e, int who, int card, int n) { + g->ev = (uint8_t)e, g->ev_who = (uint8_t)who, g->ev_card = (uint8_t)card, g->ev_n = (uint8_t)n; + g->ev_victim = g->ev_by = UNO_NONE; +} + +void uno_fill_deck(uint8_t *d) { + int n = 0; + for (int c = 0; c < 52; c++) { + d[n++] = (uint8_t)c; + if (c % 13) d[n++] = (uint8_t)c; /* one 0 of each colour, two of everything else */ + } + for (int i = 0; i < 4; i++) d[n++] = C_WILD, d[n++] = C_WILD4; +} + +static void shuffle(uno_game *g, uint8_t *a, int n) { + for (int i = n - 1; i > 0; i--) { + int j = (int)(next_rand(&g->rng) % (uint32_t)(i + 1)); + uint8_t t = a[i]; + a[i] = a[j], a[j] = t; + } +} + +/* one card from the draw pile into p's hand; an empty pile is made again from the discards under the top one */ +static bool take_one(uno_game *g, int p) { + if (!g->dn && g->pn > 1) { + uint8_t top = g->pile[g->pn - 1]; + g->dn = (uint8_t)(g->pn - 1); + memcpy(g->deck, g->pile, g->dn); + g->pile[0] = top, g->pn = 1; + shuffle(g, g->deck, g->dn); + } + if (!g->dn) return false; + g->hand[p][g->count[p]++] = g->deck[--g->dn]; + return true; +} + +static int take(uno_game *g, int p, int n) { + int k = 0; + while (k < n && take_one(g, p)) k++; + if (n > 1) g->lacks[p] = 0; /* new cards: what the table knew of that hand is gone */ + return k; +} + +/* the player whose turn it is starts doing something: too late to catch whoever forgot UNO */ +static void begin(uno_game *g) { g->uno_open = g->uno_said = UNO_NONE; } + +static void hand_over(uno_game *g, int w) { + int pts = 0; + for (int p = 0; p < g->r.players; p++) + if (p != w) + for (int i = 0; i < g->count[p]; i++) pts += uno_points(g->hand[p][i]); + g->winner = (uint8_t)w, g->gained = (uint16_t)pts; + g->score[w] = (uint16_t)(g->score[w] + pts > 60000 ? 60000 : g->score[w] + pts); + g->out = g->uno_open = g->uno_said = UNO_NONE; + g->pending = 0; + g->phase = g->r.one_hand || g->score[w] >= UNO_GOAL ? P_GAME_OVER : P_HAND_OVER; +} + +void uno_deal(uno_game *g) { + int n = g->r.players; + memset(g->hand, 0, sizeof g->hand); + memset(g->count, 0, sizeof g->count); + memset(g->lacks, 0, sizeof g->lacks); + g->pn = 0, g->dir = 1, g->pending = 0, g->challengeable = g->guilty = 0, g->opening = 0; + g->offender = g->drawn = g->uno_open = g->uno_said = g->out = g->winner = UNO_NONE; + g->gained = 0; + /* seven cards each, one at a time, from the dealer's left */ + for (int k = 0; k < 7; k++) + for (int i = 1; i <= n; i++) take_one(g, (g->dealer + i) % n); + /* the first card: a Wild Draw Four goes back into the deck, which is shuffled, and another is turned */ + for (int tries = 0; g->deck[g->dn - 1] == C_WILD4; tries++) { + if (tries < 32) { + shuffle(g, g->deck, g->dn); + } else { + int i = g->dn - 1; + while (g->deck[i] == C_WILD4) i--; + g->deck[g->dn - 1] = g->deck[i], g->deck[i] = C_WILD4; + } + } + int c = g->deck[--g->dn], left = uno_next(g, g->dealer), v = uno_value(c); + g->pile[g->pn++] = (uint8_t)c; + g->color = (uint8_t)uno_color(c), g->turn = (uint8_t)left, g->phase = P_PLAY; + event(g, E_DEAL, g->dealer, c, 0); + if (c == C_WILD) { + g->phase = P_COLOR, g->opening = 1; /* the player on the dealer's left calls the colour, then plays */ + } else if (v == V_SKIP) { + g->turn = (uint8_t)uno_next(g, left); + } else if (v == V_REVERSE) { + if (n > 2) g->dir = -1; /* the dealer starts, and play goes to the right */ + g->turn = g->dealer; + } else if (v == V_DRAW2) { + g->ev_n = (uint8_t)take(g, left, 2), g->ev_victim = (uint8_t)left; + g->turn = (uint8_t)uno_next(g, left); + } +} + +void uno_next_hand(uno_game *g) { + if (g->hands) g->dealer = (uint8_t)((g->dealer + 1) % g->r.players); /* the deal passes to the left */ + g->hands++; + uno_fill_deck(g->deck); + g->dn = UNO_DECK; + shuffle(g, g->deck, UNO_DECK); + uno_deal(g); +} + +void uno_new_game(uno_game *g, uno_rules r, uint32_t seed) { + memset(g, 0, sizeof *g); + g->r.players = (uint8_t)(r.players < 2 ? 2 : r.players > 4 ? 4 : r.players); + g->r.level = (uint8_t)(r.level > LEVEL_HARD ? LEVEL_NORMAL : r.level); + g->r.one_hand = r.one_hand ? 1 : 0; + g->r.house = (uint8_t)(r.house & H_ALL); + g->rng = seed ? seed : 1; + g->brng = seed * 2654435761u + 0x9E3779B9u; + if (!g->brng) g->brng = 1; + g->dir = 1; + /* the dealer: everyone picks a card, the highest number deals (a symbol counts as zero), the tied pick again */ + int n = g->r.players, at = 0; + uint8_t pool[UNO_DECK]; + uno_fill_deck(pool); + shuffle(g, pool, UNO_DECK); + unsigned in = (1u << n) - 1; + while ((in & (in - 1)) && at + n <= UNO_DECK) { + int best = -1; + unsigned tied = 0; + for (int p = 0; p < n; p++) { + if (!(in >> p & 1)) continue; + int v = uno_value(pool[at++]); + if (v > 9) v = 0; + if (v > best) best = v, tied = 1u << p; + else if (v == best) tied |= 1u << p; + } + in = tied; + } + for (int p = n - 1; p >= 0; p--) + if (in >> p & 1) g->dealer = (uint8_t)p; + uno_next_hand(g); +} + +bool uno_matches(const uno_game *g, int c) { + int top = g->pile[g->pn - 1]; + return c >= 52 || uno_color(c) == g->color || (top < 52 && uno_value(c) == uno_value(top)); +} + +static bool stacks(const uno_game *g, int c) { + if (g->out != UNO_NONE) return false; /* no stacking on a last card: the hand is over */ + if (g->pending_kind == V_DRAW2) return (g->r.house & H_STACK2) && uno_value(c) == V_DRAW2; + return (g->r.house & H_STACK4) && c == C_WILD4; +} + +bool uno_can_play(const uno_game *g, int i) { + if (g->phase == P_COLOR || g->phase >= P_HAND_OVER || i < 0 || i >= g->count[g->turn]) return false; + int c = g->hand[g->turn][i]; + if (g->phase == P_PENALTY) return stacks(g, c); + if (g->phase == P_DRAWN && i != g->drawn) return false; + return uno_matches(g, c); +} + +bool uno_can_stack(const uno_game *g) { + if (g->phase != P_PENALTY) return false; + for (int i = 0; i < g->count[g->turn]; i++) + if (stacks(g, g->hand[g->turn][i])) return true; + return false; +} + +bool uno_legal_wild4(const uno_game *g, int p) { + for (int i = 0; i < g->count[p]; i++) + if (uno_color(g->hand[p][i]) == g->color) return false; + return true; +} + +/* a card's effect, once its colour is known (p played it) */ +static void effect(uno_game *g, int p, int c) { + int v = uno_value(c); + g->phase = P_PLAY; + if (c == C_WILD4) { + g->pending = (uint8_t)(g->pending + 4), g->pending_kind = C_WILD4, g->offender = (uint8_t)p; + g->turn = (uint8_t)uno_next(g, p), g->phase = P_PENALTY; + return; + } + if (v == V_DRAW2) { + int victim = uno_next(g, p); + if ((g->r.house & H_STACK2) && g->out == UNO_NONE) { + g->pending = (uint8_t)(g->pending + 2), g->pending_kind = V_DRAW2, g->offender = (uint8_t)p; + g->challengeable = 0, g->turn = (uint8_t)victim, g->phase = P_PENALTY; + return; + } + g->ev_n = (uint8_t)take(g, victim, g->pending + 2), g->ev_victim = (uint8_t)victim; + g->pending = 0; + g->turn = (uint8_t)uno_next(g, victim); + } else if (v == V_SKIP) { + g->turn = (uint8_t)uno_next(g, uno_next(g, p)); + } else if (v == V_REVERSE) { + if (g->r.players == 2) { + g->turn = (uint8_t)p; /* with two players, a Reverse is a Skip */ + } else { + g->dir = (int8_t)-g->dir; + g->turn = (uint8_t)uno_next(g, p); + } + } else { + g->turn = (uint8_t)uno_next(g, p); + } + if (g->out != UNO_NONE) hand_over(g, g->out); +} + +bool uno_play(uno_game *g, int i, bool say_uno) { + if (!uno_can_play(g, i)) return false; + int p = g->turn, c = g->hand[p][i]; + bool stacking = g->phase == P_PENALTY; + bool said = say_uno || g->uno_said == p || (g->r.house & H_AUTO_UNO); + if (c == C_WILD4) { + /* a Wild Draw Four may be played with a card of the colour in play: that's a bluff, which only a challenge + finds out */ + g->guilty = !uno_legal_wild4(g, p); + g->prev_color = g->color; + g->challengeable = !stacking; + } + begin(g); + memmove(&g->hand[p][i], &g->hand[p][i + 1], (size_t)(g->count[p] - i - 1)); + g->count[p]--; + g->pile[g->pn++] = (uint8_t)c; + g->drawn = UNO_NONE; + if (c < 52) g->lacks[p] &= (uint8_t)~(1u << uno_color(c)); + event(g, E_PLAY, p, c, 0); + if (g->count[p] == 1) { + if (said) g->ev_by = (uint8_t)p; /* (with UNO!) */ + else g->uno_open = (uint8_t)p; + } + if (!g->count[p]) g->out = (uint8_t)p; + if (c >= 52) { + if (c == C_WILD && g->out == p) { /* out on a Wild: no colour to call */ + hand_over(g, p); + return true; + } + g->phase = P_COLOR, g->opening = 0, g->color = NO_COLOR; + return true; + } + g->color = (uint8_t)uno_color(c); + effect(g, p, c); + return true; +} + +bool uno_choose(uno_game *g, int color) { + if (g->phase != P_COLOR || color < 0 || color > 3) return false; + int p = g->turn, c = g->pile[g->pn - 1]; + g->color = (uint8_t)color; + event(g, E_COLOR, p, c, color); + if (g->opening) { + g->opening = 0, g->phase = P_PLAY; + return true; + } + effect(g, p, c); + return true; +} + +bool uno_draw(uno_game *g) { + if (g->phase != P_PLAY) return false; + int p = g->turn, k = 0; + begin(g); + g->lacks[p] |= (uint8_t)(1u << g->color); + do { + if (!take_one(g, p)) break; + k++; + } while ((g->r.house & H_DRAW_UNTIL) && !uno_matches(g, g->hand[p][g->count[p] - 1])); + event(g, E_DRAW, p, 0, k); + if (k && uno_matches(g, g->hand[p][g->count[p] - 1])) { + g->phase = P_DRAWN, g->drawn = (uint8_t)(g->count[p] - 1); + return true; + } + /* (nothing to draw: every other card is in someone's hand, so someone can play) */ + g->turn = (uint8_t)uno_next(g, p); + return true; +} + +bool uno_keep(uno_game *g) { + if (g->phase != P_DRAWN || (g->r.house & H_MUST_PLAY)) return false; + int p = g->turn; + g->drawn = UNO_NONE, g->phase = P_PLAY, g->turn = (uint8_t)uno_next(g, p); + event(g, E_KEEP, p, 0, 0); + return true; +} + +bool uno_take(uno_game *g) { + if (g->phase != P_PENALTY) return false; + int p = g->turn; + begin(g); + int k = take(g, p, g->pending); + event(g, E_TAKE, p, g->pending_kind, k); + g->pending = 0, g->challengeable = 0, g->phase = P_PLAY, g->turn = (uint8_t)uno_next(g, p); + if (g->out != UNO_NONE) hand_over(g, g->out); + return true; +} + +bool uno_challenge(uno_game *g) { + if (g->phase != P_PENALTY || !g->challengeable) return false; + int p = g->turn, o = g->offender; + begin(g); + g->phase = P_PLAY, g->challengeable = 0; + if (g->guilty) { + /* the bluffer draws the 4 instead, and the challenger plays (never someone out: a last card is always allowed) */ + int k = take(g, o, g->pending); + g->pending = 0; + event(g, E_CHALLENGE_WON, p, C_WILD4, k); + g->ev_victim = (uint8_t)o; + } else { + /* the challenger draws 4 and 2 more, and loses the turn */ + int k = take(g, p, g->pending + 2); + g->pending = 0; + event(g, E_CHALLENGE_LOST, p, C_WILD4, k); + g->ev_victim = (uint8_t)p; + g->turn = (uint8_t)uno_next(g, p); + if (g->out != UNO_NONE) hand_over(g, g->out); + } + return true; +} + +bool uno_call(uno_game *g, int p) { + if (g->phase >= P_HAND_OVER || p < 0 || p >= g->r.players) return false; + if (g->uno_open == p) { /* caught yourself before anyone else did: safe */ + g->uno_open = UNO_NONE; + event(g, E_UNO, p, 0, 0); + return true; + } + if (p == g->turn && g->count[p] == 2 && g->uno_said != p && + (g->phase == P_PLAY || g->phase == P_DRAWN || g->phase == P_PENALTY)) { + g->uno_said = (uint8_t)p; /* said with the next-to-last card */ + event(g, E_UNO, p, 0, 0); + return true; + } + return false; +} + +bool uno_catch(uno_game *g, int by) { + int o = g->uno_open; + if (o == UNO_NONE || by == o || by < 0 || by >= g->r.players || g->phase >= P_HAND_OVER) return false; + g->uno_open = UNO_NONE; + int k = take(g, o, 2); + event(g, E_CAUGHT, o, 0, k); + g->ev_by = (uint8_t)by; + return true; +} + +bool uno_valid(const uno_game *g) { + const uno_rules *r = &g->r; + int n = r->players; + if (n < 2 || n > 4 || r->level > LEVEL_HARD || r->one_hand > 1 || r->house > H_ALL) return false; + if (!g->rng || !g->brng || g->phase > P_GAME_OVER || (g->dir != 1 && g->dir != -1)) return false; + if (g->turn >= n || g->dealer >= n || !g->pn || g->pn > UNO_DECK || g->dn > UNO_DECK || g->opening > 1) return false; + if (g->color > NO_COLOR || (g->color == NO_COLOR) != (g->phase == P_COLOR)) return false; + if (g->challengeable > 1 || g->guilty > 1 || g->prev_color > 3 || g->pending > UNO_DECK) return false; + if (g->phase != P_DRAWN && g->drawn != UNO_NONE) return false; + const uint8_t who[] = {g->offender, g->uno_open, g->uno_said, g->out}; + for (unsigned i = 0; i < sizeof who; i++) + if (who[i] != UNO_NONE && who[i] >= n) return false; + if (g->winner != UNO_NONE && g->winner >= n) return false; + for (int p = 0; p < 4; p++) + if (g->score[p] > 60000 || (p >= n && (g->count[p] || g->score[p]))) return false; + int top = g->pile[g->pn - 1]; + if (top >= UNO_KINDS) return false; + switch (g->phase) { + case P_PLAY: if (g->pending || g->out != UNO_NONE) return false; break; + case P_DRAWN: if (g->pending || g->out != UNO_NONE || g->drawn >= g->count[g->turn]) return false; break; + case P_COLOR: if (top < 52 || (g->opening && g->pn != 1)) return false; break; + case P_PENALTY: + if (!g->pending || g->pending & 1 || g->offender == UNO_NONE) return false; + if (g->pending_kind != C_WILD4 && g->pending_kind != V_DRAW2) return false; + if (g->challengeable && (g->pending_kind != C_WILD4 || g->pending != 4)) return false; + break; + case P_GAME_OVER: if (g->winner == UNO_NONE) return false; break; + default: break; + } + if (g->out != UNO_NONE && (g->count[g->out] || (g->phase != P_COLOR && g->phase != P_PENALTY))) return false; + if (g->uno_open != UNO_NONE && g->count[g->uno_open] != 1) return false; + /* every card of the deck, once */ + uint8_t seen[UNO_KINDS] = {0}; + int total = g->dn + g->pn; + for (int i = 0; i < g->dn; i++) { + if (g->deck[i] >= UNO_KINDS) return false; + seen[g->deck[i]]++; + } + for (int i = 0; i < g->pn; i++) { + if (g->pile[i] >= UNO_KINDS) return false; + seen[g->pile[i]]++; + } + for (int p = 0; p < 4; p++) { + if (g->count[p] > UNO_DECK) return false; + total += g->count[p]; + for (int i = 0; i < g->count[p]; i++) { + if (g->hand[p][i] >= UNO_KINDS) return false; + seen[g->hand[p][i]]++; + } + } + if (total != UNO_DECK) return false; + for (int c = 0; c < UNO_KINDS; c++) + if (seen[c] != (c >= 52 ? 4 : c % 13 ? 2 : 1)) return false; + return true; +} diff --git a/games/uno/src/game.h b/games/uno/src/game.h new file mode 100644 index 00000000..0c207d47 --- /dev/null +++ b/games/uno/src/game.h @@ -0,0 +1,109 @@ +/* UNO's rules (game.c) and its bots (bot.c), as Mattel's instruction sheet has them: the 108-card deck, the + * action cards, the Wild Draw Four and its challenge, UNO calls and their penalty, and hands scored until + * someone has 500 points. No screen, clock or files here: the tests drive it on a computer. */ +#ifndef UNO_GAME_H +#define UNO_GAME_H +#include +#include + +#define UNO_DECK 108 +#define UNO_NONE 0xFF +#define UNO_GOAL 500 + +/* A card: colour * 13 + value for the 100 coloured ones (0 to 9, then Skip, Reverse, Draw Two), then the Wild + * and the Wild Draw Four. */ +enum { V_SKIP = 10, V_REVERSE = 11, V_DRAW2 = 12, C_WILD = 52, C_WILD4 = 53, UNO_KINDS = 54 }; +enum { RED, YELLOW, GREEN, BLUE, NO_COLOR = 4 }; +static inline int uno_color(int c) { return c < 52 ? c / 13 : NO_COLOR; } +static inline int uno_value(int c) { return c < 52 ? c % 13 : c; } + +/* house rules, none of them official: all off by default */ +enum { H_STACK2 = 1, H_STACK4 = 2, H_DRAW_UNTIL = 4, H_MUST_PLAY = 8, H_AUTO_UNO = 16, H_ALL = 31 }; +enum { LEVEL_EASY, LEVEL_NORMAL, LEVEL_HARD }; + +typedef struct { + uint8_t players; /* 2 to 4: you (player 0) and 1 to 3 bots */ + uint8_t level; /* the bots' */ + uint8_t one_hand; /* 1: a single hand; 0: hands until someone has 500 points */ + uint8_t house; +} uno_rules; + +/* what the player whose turn it is has to do */ +enum { + P_PLAY, /* play a card or draw one */ + P_DRAWN, /* play the card just drawn (only that one), or keep it */ + P_COLOR, /* call the colour of the Wild just played */ + P_PENALTY, /* a Wild Draw Four (or, stacking, a Draw Two) waits: take it, challenge it, or stack on it */ + P_HAND_OVER, /* the hand is over and scored */ + P_GAME_OVER, +}; + +/* what happened last, for the screen */ +enum { E_NONE, E_DEAL, E_PLAY, E_DRAW, E_KEEP, E_COLOR, E_TAKE, E_CHALLENGE_WON, E_CHALLENGE_LOST, E_UNO, E_CAUGHT }; + +typedef struct { + uno_rules r; + uint8_t hand[4][UNO_DECK]; + uint8_t count[4]; + uint8_t deck[UNO_DECK]; /* the draw pile, its top the last */ + uint8_t pile[UNO_DECK]; /* the discard pile, its top the last */ + uint8_t dn, pn; + uint16_t score[4]; + uint16_t hands; /* hands dealt in this game */ + uint32_t rng; /* the shuffles' */ + uint32_t brng; /* the bots' choices */ + uint8_t dealer, turn, phase; + int8_t dir; /* 1: clockwise, to the left; -1 */ + uint8_t color; /* the colour to match: the top card's, or the one called for a Wild */ + uint8_t opening; /* P_COLOR: the first player calls the colour of the first card, then plays */ + uint8_t pending; /* P_PENALTY: the cards to take */ + uint8_t pending_kind; /* V_DRAW2 or C_WILD4 */ + uint8_t offender; /* who played the Wild Draw Four waiting */ + uint8_t challengeable; /* it can be challenged (not one stacked on another) */ + uint8_t guilty; /* its player had a card of the colour before it */ + uint8_t prev_color; /* that colour */ + uint8_t drawn; /* P_DRAWN: the card's place in the hand */ + uint8_t uno_open; /* someone down to one card who did not call UNO, until the next player starts */ + uint8_t uno_said; /* the player whose turn it is called UNO before playing the next-to-last card */ + uint8_t out; /* who played the last card, while its Draw Two or Wild Draw Four waits */ + uint8_t winner; /* the hand's, or the game's at P_GAME_OVER */ + uint8_t spare0; + uint16_t gained; /* the points the hand's winner scored */ + uint8_t lacks[4]; /* colours a player drew on rather than play (what everyone at the table saw) */ + uint8_t ev, ev_who, ev_card, ev_n, ev_victim, ev_by; + uint8_t spare[2]; +} uno_game; +_Static_assert(sizeof(uno_game) == 708, "uno_game: packed bytes, no padding"); + +int uno_points(int card); +int uno_next(const uno_game *g, int p); +void uno_new_game(uno_game *g, uno_rules r, uint32_t seed); +void uno_next_hand(uno_game *g); +void uno_fill_deck(uint8_t *deck); /* the 108 cards, in order */ +void uno_deal(uno_game *g); /* deals from g->deck as it is (uno_next_hand shuffles first) */ +bool uno_matches(const uno_game *g, int card); +bool uno_can_play(const uno_game *g, int i); /* card i of the hand of the player whose turn it is */ +bool uno_legal_wild4(const uno_game *g, int p); /* p holds no card of the colour in play */ +bool uno_play(uno_game *g, int i, bool say_uno); +bool uno_choose(uno_game *g, int color); +bool uno_draw(uno_game *g); +bool uno_keep(uno_game *g); +bool uno_take(uno_game *g); +bool uno_challenge(uno_game *g); +bool uno_call(uno_game *g, int p); +bool uno_catch(uno_game *g, int by); +bool uno_can_stack(const uno_game *g); /* P_PENALTY: the player has a card to stack */ +bool uno_valid(const uno_game *g); /* every card once, every field in range (a save to trust) */ + +/* the bots (bot.c): what the player whose turn it is does, at a level, from what that player can see */ +enum { M_PLAY, M_DRAW, M_KEEP, M_TAKE, M_CHALLENGE, M_COLOR }; +typedef struct { + uint8_t act, arg; /* arg: the card's place (M_PLAY), the colour (M_COLOR) */ + bool uno; /* say UNO with this card */ +} uno_move; +uno_move bot_move(uno_game *g, int level); +bool bot_do(uno_game *g, uno_move m); +/* whether bot `by` catches the player who forgot UNO, and how long it takes (ms) */ +bool bot_catches(uno_game *g, int by, int level, uint32_t *ms); +int bot_best_color(const uno_game *g, int p, int level); +#endif diff --git a/games/uno/src/icon.png b/games/uno/src/icon.png new file mode 100644 index 00000000..c5bb1037 Binary files /dev/null and b/games/uno/src/icon.png differ diff --git a/games/uno/src/main.c b/games/uno/src/main.c new file mode 100644 index 00000000..50a5e3f9 --- /dev/null +++ b/games/uno/src/main.c @@ -0,0 +1,409 @@ +/* UNO on the NumWorks calculator: you against one to three bots, with the official rules and scoring to 500 + * points (game.c), the bots (bot.c) and the screens (draw.c). + * + * Keys: Left and Right pick a card, OK plays it, Up draws, shift says UNO (or catches a bot that forgot), Back + * pauses; Home saves and quits. */ +#include +#include "draw.h" +#include "game.h" +#include "plat.h" +#include "save.h" + +static uno_progress sv; +#define G (&sv.g) +static uno_ui ui; +static uint32_t now, bot_at, catch_at, end_at, saved_at; +static uint8_t catcher = UNO_NONE, open_seen = UNO_NONE; +static bool dirty; +/* frames drawn, the slowest (ms) and those over 33 ms: read by tools/emu.py */ +#ifndef HOST +__attribute__((used, externally_visible)) +#endif +uint32_t perf_frames, perf_max, perf_slow; + +static const char *const COLOR_NAME[4] = {"Red", "Yellow", "Green", "Blue"}; + +static char *cat(char *p, const char *s) { + while (*s) *p++ = *s++; + *p = 0; + return p; +} +static char *num(char *p, unsigned v) { + char t[8]; + int n = 0; + do t[n++] = (char)('0' + v % 10); while ((v /= 10) && n < 8); + while (n) *p++ = t[--n]; + *p = 0; + return p; +} +/* "Bot 1 draws" / "You draw" (mid: "you draw") */ +static char *verb_as(char *p, int who, const char *one, const char *you, bool mid) { + p = cat(p, who || !mid ? uno_name(who) : "you"); + *p++ = ' '; + return cat(p, who ? one : you); +} +static char *verb(char *p, int who, const char *one, const char *you) { return verb_as(p, who, one, you, false); } +static char *verb_mid(char *p, int who, const char *one, const char *you) { return verb_as(p, who, one, you, true); } + +static void save(void) { + sv.setup = ui.setup, sv.has_game = ui.has_game; + save_write(&sv); + saved_at = now; +} + +/* the message line, from what just happened */ +static void describe(void) { + uno_game *g = G; + char *p = ui.msg, card[24]; + int who = g->ev_who; + switch (g->ev) { + case E_DEAL: + p = verb(p, g->dealer, "deals: ", "deal: "); + uno_card_name(g->ev_card, card); + p = cat(p, card); + if (g->ev_victim != UNO_NONE) p = cat(p, ", "), p = verb_mid(p, g->ev_victim, "draws 2", "draw 2"); + break; + case E_PLAY: + uno_card_name(g->ev_card, card); + p = verb(p, who, "played ", "played "); + p = cat(p, card); + if (g->ev_victim != UNO_NONE) { + p = cat(p, ": "); + p = verb_mid(p, g->ev_victim, "draws ", "draw "); + p = num(p, g->ev_n); + } + if (g->ev_by == who) p = cat(p, ", UNO!"); + break; + case E_COLOR: p = verb(p, who, "called ", "called "), p = cat(p, COLOR_NAME[g->ev_n & 3]); break; + case E_DRAW: + if (!g->ev_n) p = verb(p, who, "had nothing to draw", "had nothing to draw"); + else if (g->ev_n == 1) p = verb(p, who, "drew a card", "drew a card"); + else p = verb(p, who, "drew ", "drew "), p = num(p, g->ev_n), p = cat(p, " cards"); + break; + case E_KEEP: p = verb(p, who, "kept it", "kept it"); break; + case E_TAKE: p = verb(p, who, "took ", "took "), p = num(p, g->ev_n), p = cat(p, " cards"); break; + case E_CHALLENGE_WON: + p = verb(p, who, "challenged: ", "challenged: "); + p = verb_mid(p, g->ev_victim, "bluffed, draws 4", "bluffed, draw 4"); + break; + case E_CHALLENGE_LOST: p = verb(p, who, "challenged and took ", "challenged and took "), p = num(p, g->ev_n); break; + case E_UNO: p = cat(p, uno_name(who)), p = cat(p, ": UNO!"); break; + case E_CAUGHT: + p = verb(p, g->ev_by, "caught ", "caught "); + p = cat(p, who ? uno_name(who) : "you"); + p = cat(p, ": 2 cards"); + break; + default: break; + } + *p = 0; +} + +/* someone just forgot UNO: the bots that will catch them, and when */ +static void plan_catch(void) { + uno_game *g = G; + open_seen = g->uno_open, catcher = UNO_NONE; + if (open_seen == UNO_NONE) return; + uint32_t best = 0xFFFFFFFFu; + for (int b = 1; b < g->r.players; b++) { + uint32_t ms; + if (b != open_seen && bot_catches(g, b, g->r.level, &ms) && ms < best) best = ms, catcher = (uint8_t)b; + } + if (catcher != UNO_NONE) catch_at = now + best; + /* time for the forgetful to catch themselves, or for you to catch a bot, before the next player starts */ + if ((int32_t)(bot_at - (now + 1800)) < 0) bot_at = now + 1800; +} + +static void after(uint32_t wait) { + uno_game *g = G; + bot_at = now + wait; + if (g->uno_open != open_seen) plan_catch(); + if (g->phase >= P_HAND_OVER) { + end_at = now + 1500, ui.show_end = 0, ui.sel = 0; + if (g->phase == P_GAME_OVER) ui.has_game = 0; + save(); + } + if (g->turn == 0 && g->phase == P_DRAWN) ui.pick = g->drawn; + if (g->count[0] && ui.pick >= g->count[0]) ui.pick = (uint8_t)(g->count[0] - 1); + if (!g->count[0]) ui.pick = 0; + dirty = true; +} + +static void start(void) { + uno_new_game(G, ui.setup, plat_random() ^ now ^ 0x5EED); + ui.has_game = 1, ui.pick = 0, ui.show_end = 0, ui.screen = V_PLAY; + open_seen = catcher = UNO_NONE; + describe(); + after(1200); + save(); +} + +/* Deal: how to play first, the first time */ +static void deal(void) { + if (!sv.guide) { + sv.guide = 1; + ui.first_run = 1, ui.page = 0, ui.under = V_SETUP, ui.screen = V_HELP; + return; + } + start(); +} + +static void bot_step(void) { + uno_game *g = G; + int p = g->turn; + uno_move m = bot_move(g, g->r.level); + bot_do(g, m); + describe(); + if (g->phase == P_COLOR && g->turn == p) { /* its Wild: the colour at once */ + bool said = g->ev_by == p; + bot_do(g, bot_move(g, g->r.level)); + char card[24], *q = ui.msg; + uno_card_name(g->pile[g->pn - 1], card); + q = cat(q, uno_name(p)), q = cat(q, " played "), q = cat(q, card), q = cat(q, ": "); + q = cat(q, COLOR_NAME[g->color & 3]); + if (said) cat(q, ", UNO!"); + } + after(m.act == M_DRAW && g->phase == P_DRAWN ? 700 : 1000); +} + +static void shift_key(void) { + uno_game *g = G; + if (g->uno_open != UNO_NONE && g->uno_open != 0) { + if (uno_catch(g, 0)) describe(), after(1000); + } else if (uno_call(g, 0)) { + describe(); + after(g->turn == 0 ? 0 : 1000); + } else { + strcpy(ui.msg, "UNO is for your next-to-last card"); + dirty = true; + } +} + +static void human_play(int i) { + uno_game *g = G; + if (!uno_play(g, i, false)) { + strcpy(ui.msg, g->phase == P_DRAWN ? "Only the new card can be played" : "That card doesn't go"); + dirty = true; + return; + } + describe(); + if (g->phase == P_COLOR) ui.sel = (uint8_t)bot_best_color(g, 0, LEVEL_NORMAL); + after(1000); +} + +static void play_keys(uint32_t hit) { + uno_game *g = G; + if (hit & K_BACK) { + ui.screen = V_PAUSE, ui.sel = PAUSE_RESUME; + save(); + return; + } + if (g->phase >= P_HAND_OVER) { + if (!ui.show_end) return; + if (g->phase == P_GAME_OVER) { + if (hit & (K_LEFT | K_RIGHT | K_UP | K_DOWN)) ui.sel ^= 1; + if (hit & K_OK) { + if (ui.sel == 0) start(); + else ui.screen = V_TITLE, ui.sel = 0, save(); + } + } else if (hit & K_OK) { + uno_next_hand(g); + ui.pick = 0, ui.show_end = 0; + describe(); + after(1200); + } + return; + } + if (hit & K_SHIFT) shift_key(); + int n = g->count[0]; + bool mine = g->turn == 0; + if (mine && g->phase == P_COLOR) { + if (hit & (K_LEFT | K_UP)) ui.sel = (uint8_t)((ui.sel + 3) % 4); + if (hit & (K_RIGHT | K_DOWN)) ui.sel = (uint8_t)((ui.sel + 1) % 4); + if ((hit & K_OK) && uno_choose(g, ui.sel)) describe(), after(1000); + return; + } + if (mine && g->phase == P_PENALTY && g->challengeable) { + int k = uno_pen_count(g); + if (hit & K_UP) ui.sel = (uint8_t)((ui.sel + k - 1) % k); + if (hit & K_DOWN) ui.sel = (uint8_t)((ui.sel + 1) % k); + if (!(hit & K_OK)) return; + int item = ui.sel % k; + if (item == PEN_TAKE) { + uno_take(g); + } else if (item == PEN_CHALLENGE) { + /* the challenged player shows their hand to the challenger */ + int o = g->offender; + memcpy(ui.reveal, g->hand[o], g->count[o]); + ui.reveal_n = g->count[o], ui.reveal_who = (uint8_t)o, ui.reveal_color = g->prev_color, ui.reveal_guilty = g->guilty; + uno_challenge(g); + ui.screen = V_REVEAL; + } else { + for (int i = 0; i < n; i++) + if (uno_can_play(g, i)) { + human_play(i); + return; + } + } + describe(); + after(1000); + return; + } + if (n && (hit & (K_LEFT | K_RIGHT)) && !(mine && g->phase == P_DRAWN)) { + ui.pick = (uint8_t)((ui.pick + (hit & K_RIGHT ? 1 : n - 1)) % n); + dirty = true; + } + if (!mine) return; + if (g->phase == P_DRAWN) { + if (hit & K_OK) human_play(g->drawn); + else if ((hit & K_UP) && uno_keep(g)) describe(), after(1000); + } else if (g->phase == P_PENALTY) { + if ((hit & K_OK) && n) human_play(ui.pick); + else if ((hit & K_UP) && uno_take(g)) describe(), after(1000); + } else if (g->phase == P_PLAY) { + if ((hit & K_OK) && n) human_play(ui.pick); + else if ((hit & K_UP) && uno_draw(g)) describe(), after(g->phase == P_DRAWN ? 0 : 1000); + } +} + +/* what happens by itself: the bots' turns, their catches, a penalty you can only take, the end of a hand */ +static void tick(void) { + uno_game *g = G; + if (ui.screen != V_PLAY) return; + if (g->phase >= P_HAND_OVER) { + if (!ui.show_end && (int32_t)(now - end_at) >= 0) ui.show_end = 1, dirty = true; + return; + } + if (catcher != UNO_NONE && g->uno_open == open_seen && (int32_t)(now - catch_at) >= 0) { + int by = catcher; + catcher = UNO_NONE; + if (uno_catch(g, by)) describe(), after(1000); + return; + } + if ((int32_t)(now - bot_at) < 0) return; + if (g->turn != 0) bot_step(); + else if (g->phase == P_PENALTY && !g->challengeable && !uno_can_stack(g)) uno_take(g), describe(), after(1000); +} + +/* back to the table from a pause: the bots wait a little */ +static void resume(void) { + ui.screen = V_PLAY; + bot_at = now + 600; + if (catcher != UNO_NONE && (int32_t)(catch_at - now) < 600) catch_at = now + 600; +} + +int main(void) { + plat_begin(); + memset(&ui, 0, sizeof ui); + catcher = open_seen = UNO_NONE; + save_load(&sv); + ui.setup = sv.setup, ui.has_game = sv.has_game; + if (ui.has_game) describe(); + ui.screen = V_TITLE, ui.sel = 0; + uint32_t stale = plat_keys(), prev = stale, rep_at = 0; + uint8_t shown = 0xFF; + now = plat_millis(); + bot_at = now + 1000; + dirty = true; + for (;;) { + now = plat_millis(); + uint32_t keys = plat_keys(); + stale &= keys; /* keys still down from the screen before don't count */ + uint32_t live = keys & ~stale, hit = live & ~prev; + /* Left or Right held: again after 350 ms, then every 120 ms */ + uint32_t arrows = live & (K_LEFT | K_RIGHT); + if (hit & arrows) rep_at = now + 350; + else if (arrows && (int32_t)(now - rep_at) >= 0) hit |= arrows, rep_at = now + 120; + prev = live; + if (keys & K_HOME) break; /* Home: saved below, then out */ + if (hit) dirty = true; + bool quit = false; + switch (ui.screen) { + case V_TITLE: { + int n = uno_menu_count(&ui); + if (hit & K_UP) ui.sel = (uint8_t)((ui.sel + n - 1) % n); + if (hit & K_DOWN) ui.sel = (uint8_t)((ui.sel + 1) % n); + if (hit & K_BACK) quit = true; + else if (hit & K_OK) switch (uno_menu_item(&ui, ui.sel % n)) { + case MENU_CONTINUE: ui.screen = V_PLAY, after(1000); break; + case MENU_NEW: ui.screen = V_SETUP, ui.under = V_TITLE, ui.sel = ROW_DEAL; break; + default: ui.screen = V_HELP, ui.under = V_TITLE, ui.page = 0, ui.first_run = 0; break; + } + break; + } + case V_SETUP: { + uno_rules *r = &ui.setup; + if (hit & K_UP) ui.sel = (uint8_t)((ui.sel + SETUP_ROWS - 1) % SETUP_ROWS); + if (hit & K_DOWN) ui.sel = (uint8_t)((ui.sel + 1) % SETUP_ROWS); + int d = hit & K_RIGHT ? 1 : hit & K_LEFT ? -1 : 0; + if (d && ui.sel == ROW_PLAYERS) r->players = (uint8_t)(2 + (r->players - 2 + d + 3) % 3); + if (d && ui.sel == ROW_LEVEL) r->level = (uint8_t)((r->level + d + 3) % 3); + if (d && ui.sel == ROW_LENGTH) r->one_hand ^= 1; + bool toggle = ui.sel >= ROW_HOUSE && ui.sel < ROW_DEAL && (d || (hit & K_OK)); + if (toggle) r->house ^= (uint8_t)(1u << (ui.sel - ROW_HOUSE)); + if (hit & K_BACK) ui.screen = ui.under, ui.sel = ui.under == V_PAUSE ? PAUSE_NEW : (uint8_t)(uno_menu_count(&ui) - 2); + else if ((hit & K_OK) && !toggle) deal(); + break; + } + case V_HELP: + if (ui.first_run && (hit & K_BACK)) { + ui.first_run = 0; + start(); + } else if ((hit & K_RIGHT || (ui.first_run && (hit & K_OK))) && ui.page + 1 < UNO_HELP_PAGES) { + ui.page++; + } else if ((hit & K_LEFT) && ui.page) { + ui.page--; + } else if (ui.first_run && (hit & K_OK)) { + ui.first_run = 0; + start(); + } else if (hit & (K_BACK | K_OK)) { + ui.screen = ui.under, ui.sel = ui.under == V_PAUSE ? PAUSE_HELP : (uint8_t)(uno_menu_count(&ui) - 1); + } + break; + case V_PLAY: play_keys(hit); break; + case V_REVEAL: + if (hit & (K_OK | K_BACK)) resume(); + break; + case V_PAUSE: + if (hit & K_UP) ui.sel = (uint8_t)((ui.sel + PAUSE_COUNT - 1) % PAUSE_COUNT); + if (hit & K_DOWN) ui.sel = (uint8_t)((ui.sel + 1) % PAUSE_COUNT); + if (hit & K_BACK) resume(); + else if (hit & K_OK) switch (ui.sel) { + case PAUSE_RESUME: resume(); break; + case PAUSE_NEW: ui.screen = V_SETUP, ui.under = V_PAUSE, ui.sel = ROW_DEAL; break; + case PAUSE_HELP: ui.screen = V_HELP, ui.under = V_PAUSE, ui.page = 0, ui.first_run = 0; break; + default: ui.screen = V_QUIT, ui.sel = 0; break; + } + break; + case V_QUIT: + if (hit & (K_LEFT | K_RIGHT | K_UP | K_DOWN)) ui.sel ^= 1; + if (hit & K_BACK) ui.screen = V_PAUSE, ui.sel = PAUSE_QUIT; + else if (hit & K_OK) { + if (ui.sel) quit = true; + else ui.screen = V_PAUSE, ui.sel = PAUSE_QUIT; + } + break; + } + if (quit) break; + tick(); + if (ui.has_game && (int32_t)(now - saved_at) >= 30000) save(); + if (ui.screen != shown) { + stale = keys, prev = 0; /* a new screen starts with all keys up */ + shown = ui.screen; + dirty = true; + } + if (dirty) { + ui.clock = now; + uint32_t t0 = plat_millis(); + uno_render(G, &ui); + uint32_t spent = plat_millis() - t0; + perf_frames++; + if (spent > perf_max) perf_max = spent; + if (spent > 33) perf_slow++; + dirty = false; + } else { + plat_sleep(10); + } + } + save(); + return plat_end(); +} diff --git a/games/uno/src/plat.h b/games/uno/src/plat.h new file mode 100644 index 00000000..45b5fca0 --- /dev/null +++ b/games/uno/src/plat.h @@ -0,0 +1,17 @@ +/* What the game needs from the calculator (plat_eadk.c), or from a computer for the tests. */ +#ifndef UNO_PLAT_H +#define UNO_PLAT_H +#include +#include + +enum { K_OK = 1, K_SHIFT = 2, K_UP = 4, K_DOWN = 8, K_LEFT = 16, K_RIGHT = 32, K_BACK = 64, K_HOME = 128 }; +uint32_t plat_keys(void); +uint32_t plat_millis(void); +void plat_sleep(uint32_t ms); +void plat_push(int x, int y, int w, int h, const uint16_t *px); +bool plat_save(const char *name, const void *data, uint32_t len); +const uint8_t *plat_load(const char *name, uint32_t *len); +uint32_t plat_random(void); +void plat_begin(void); +int plat_end(void); +#endif diff --git a/games/uno/src/plat_eadk.c b/games/uno/src/plat_eadk.c new file mode 100644 index 00000000..1b66fc31 --- /dev/null +++ b/games/uno/src/plat_eadk.c @@ -0,0 +1,34 @@ +/* The calculator: keys, time, the screen and files (EADK, and NumPlay's careful file helpers). */ +#include +#include "../../common/epsilon_app.h" +#include "../../common/epsilon_files.h" +#include "plat.h" + +const char eadk_app_name[] __attribute__((section(".rodata.eadk_app_name"))) = "UNO"; +const uint32_t eadk_api_level __attribute__((section(".rodata.eadk_api_level"))) = 0; + +#define KEY(k) ((uint64_t)1 << (k)) + +uint32_t plat_keys(void) { + uint64_t k = eadk_keyboard_scan(); + uint32_t r = 0; + if (k & (KEY(eadk_key_ok) | KEY(eadk_key_exe) | KEY(eadk_key_five))) r |= K_OK; + if (k & KEY(eadk_key_shift)) r |= K_SHIFT; + if (k & (KEY(eadk_key_up) | KEY(eadk_key_eight))) r |= K_UP; + if (k & (KEY(eadk_key_down) | KEY(eadk_key_two))) r |= K_DOWN; + if (k & (KEY(eadk_key_left) | KEY(eadk_key_four))) r |= K_LEFT; + if (k & (KEY(eadk_key_right) | KEY(eadk_key_six))) r |= K_RIGHT; + if (k & (KEY(eadk_key_back) | KEY(eadk_key_backspace))) r |= K_BACK; + if (k & (KEY(eadk_key_home) | KEY(eadk_key_on_off))) r |= K_HOME; + return r; +} +uint32_t plat_millis(void) { return (uint32_t)eadk_timing_millis(); } +void plat_sleep(uint32_t ms) { eadk_timing_msleep(ms); } +void plat_push(int x, int y, int w, int h, const uint16_t *px) { + eadk_display_push_rect((eadk_rect_t){(uint16_t)x, (uint16_t)y, (uint16_t)w, (uint16_t)h}, (const eadk_color_t *)px); +} +bool plat_save(const char *name, const void *data, uint32_t len) { return ef_write(name, data, len); } +const uint8_t *plat_load(const char *name, uint32_t *len) { return ef_read(name, len); } +uint32_t plat_random(void) { return eadk_random(); } +void plat_begin(void) { np_app_begin(); } +int plat_end(void) { return np_app_end(); } diff --git a/games/uno/src/save.c b/games/uno/src/save.c new file mode 100644 index 00000000..0164974e --- /dev/null +++ b/games/uno/src/save.c @@ -0,0 +1,120 @@ +/* The save (save.h): a bytes file with a magic, a version, its size and a checksum, and the game checked card by + * card; anything else (damaged, cut short, from elsewhere) is not read: the copy in the Python script is tried, + * then a fresh start. */ +#include +#include +#include "plat.h" +#include "save.h" + +void uno_fresh(uno_progress *p) { + memset(p, 0, sizeof *p); + p->setup.players = 4, p->setup.level = LEVEL_NORMAL; +} + +uint32_t uno_sum(const void *data, uint32_t n) { + const uint8_t *p = data; + uint32_t h = 2166136261u; + for (uint32_t i = 0; i < n; i++) h = (h ^ p[i]) * 16777619u; + return h; +} + +bool uno_file_ok(const uno_file *f) { + if (f->magic != UNO_MAGIC || f->version != UNO_VERSION || f->size != sizeof *f) return false; + if (f->sum != uno_sum(f, offsetof(uno_file, sum))) return false; + const uno_progress *p = &f->p; + const uno_rules *r = &p->setup; + if (r->players < 2 || r->players > 4 || r->level > LEVEL_HARD || r->one_hand > 1 || r->house > H_ALL) return false; + if (p->guide > 1 || p->has_game > 1) return false; + return !p->has_game || uno_valid(&p->g); +} + +static const char head[] = "# UNO keeps a copy of your game here,\n" + "# so that installing it again doesn't erase it.\n"; +static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static uint8_t copy[1 + sizeof head + 2 + sizeof UNO_SAVE_NAME + 1 + (sizeof(uno_file) + 2) / 3 * 4 + 2]; + +/* the copy: the file in base 64, in a comment line of a script */ +static bool copy_write(const uno_file *f) { + const uint8_t *bytes = (const uint8_t *)f; + uint32_t len = sizeof *f, nl = sizeof UNO_SAVE_NAME - 1; + uint8_t *p = copy; + *p++ = 0; /* (a script's first byte: not imported) */ + memcpy(p, head, sizeof head - 1), p += sizeof head - 1; + *p++ = '#', *p++ = '>'; + memcpy(p, UNO_SAVE_NAME, nl), p += nl; + *p++ = ':'; + for (uint32_t i = 0; i < len; i += 3, p += 4) { + uint32_t left = len - i; + uint32_t v = (uint32_t)bytes[i] << 16 | (left > 1 ? (uint32_t)bytes[i + 1] << 8 : 0) | (left > 2 ? bytes[i + 2] : 0); + p[0] = (uint8_t)b64[v >> 18 & 63]; + p[1] = (uint8_t)b64[v >> 12 & 63]; + p[2] = left > 1 ? (uint8_t)b64[v >> 6 & 63] : '='; + p[3] = left > 2 ? (uint8_t)b64[v & 63] : '='; + } + *p++ = '\n'; + return plat_save(UNO_COPY_NAME, copy, (uint32_t)(p - copy)); +} + +static int b64v(uint8_t c) { + for (int i = 0; i < 64; i++) + if ((uint8_t)b64[i] == c) return i; + return -1; +} + +static bool copy_read(uno_file *f) { + uint32_t n, nl = sizeof UNO_SAVE_NAME - 1; + const uint8_t *c = plat_load(UNO_COPY_NAME, &n); + if (!c) return false; + for (uint32_t i = 0; i + nl + 3 <= n; i++) { + if (c[i] != '#' || c[i + 1] != '>' || memcmp(c + i + 2, UNO_SAVE_NAME, nl) || c[i + 2 + nl] != ':') continue; + const uint8_t *l = c + i + 3 + nl; + uint32_t m = 0; + while (l + m < c + n && l[m] != '\n' && l[m]) m++; + if (m != (sizeof *f + 2) / 3 * 4) return false; + uint8_t *bytes = (uint8_t *)f; + uint32_t o = 0; + for (uint32_t k = 0; k < m; k += 4) { + int v0 = b64v(l[k]), v1 = b64v(l[k + 1]), v2 = l[k + 2] == '=' ? 0 : b64v(l[k + 2]), + v3 = l[k + 3] == '=' ? 0 : b64v(l[k + 3]); + if (v0 < 0 || v1 < 0 || v2 < 0 || v3 < 0) return false; + uint32_t w = (uint32_t)(v0 << 18 | v1 << 12 | v2 << 6 | v3); + if (o < sizeof *f) bytes[o++] = (uint8_t)(w >> 16); + if (o < sizeof *f) bytes[o++] = (uint8_t)(w >> 8); + if (o < sizeof *f) bytes[o++] = (uint8_t)w; + } + return true; + } + return false; +} + +static uno_file file; /* (static: the stack stays small) */ + +bool save_load(uno_progress *p) { + uint32_t n; + const uint8_t *d = plat_load(UNO_SAVE_NAME, &n); + bool ok = false; + if (d && n == sizeof file) { + memcpy(&file, d, sizeof file); + ok = uno_file_ok(&file); + } + if (!ok && copy_read(&file) && uno_file_ok(&file)) { + ok = true; + plat_save(UNO_SAVE_NAME, &file, sizeof file); /* back where it belongs */ + } + if (!ok) { + uno_fresh(p); + return false; + } + *p = file.p; + return true; +} + +bool save_write(const uno_progress *p) { + memset(&file, 0, sizeof file); + file.magic = UNO_MAGIC, file.version = UNO_VERSION, file.size = sizeof file; + file.p = *p; + file.sum = uno_sum(&file, offsetof(uno_file, sum)); + bool ok = plat_save(UNO_SAVE_NAME, &file, sizeof file); + copy_write(&file); + return ok; +} diff --git a/games/uno/src/save.h b/games/uno/src/save.h new file mode 100644 index 00000000..1d7cedf6 --- /dev/null +++ b/games/uno/src/save.h @@ -0,0 +1,34 @@ +/* What's kept (the new game screen's choices, whether how to play was read, the game going on), in uno.sav, + * and a copy of it in a Python script, uno_saves.py (installing apps from the NumWorks website keeps only + * scripts, so the save comes back from it). */ +#ifndef UNO_SAVE_H +#define UNO_SAVE_H +#include "game.h" + +#define UNO_SAVE_NAME "uno.sav" +#define UNO_COPY_NAME "uno_saves.py" +#define UNO_MAGIC 0x314F4E55u /* "UNO1" */ +#define UNO_VERSION 1 + +typedef struct { + uno_rules setup; /* the new game screen's choices */ + uint8_t guide; /* how to play shown before the first game */ + uint8_t has_game; /* a game to go on with */ + uint8_t spare[2]; + uno_game g; +} uno_progress; + +typedef struct { + uint32_t magic; + uint16_t version, size; + uno_progress p; + uint32_t sum; /* FNV-1a of everything before it */ +} uno_file; +_Static_assert(sizeof(uno_file) == 728, "uno.sav: 728 bytes"); + +void uno_fresh(uno_progress *p); +uint32_t uno_sum(const void *data, uint32_t n); +bool uno_file_ok(const uno_file *f); +bool save_load(uno_progress *p); /* the save, its copy, or a fresh start; false: a fresh start */ +bool save_write(const uno_progress *p); +#endif diff --git a/games/uno/tests/play.c b/games/uno/tests/play.c new file mode 100644 index 00000000..554d2932 --- /dev/null +++ b/games/uno/tests/play.c @@ -0,0 +1,937 @@ +/* UNO on a computer, for tests and pictures (make host, make test). + * + * play test DIR the rules against Mattel's instruction sheet (every card, two players, the challenge, UNO and + * its penalty, going out, scoring to 500, the house rules), the bots playing whole games to 500 + * at every level (and the same game twice, to the same hash on every computer), a few hundred + * sessions of the whole app with random keys, saves written and read back, damaged saves + * play shots DIR pictures of the screens (DIR/.ppm) + * play film DIR MS SEED MS milliseconds of a game, a bot playing your cards too (DIR/_.raw, for + * tools/record.py --frames) + */ +#define main uno_app_main +#include "../src/main.c" +#undef main +#include +#include +#include + +/* ---------------------------------------------------------------- the calculator, on a computer */ +static uint16_t fb[240][320]; +static void (*on_frame)(void); +void plat_push(int x, int y, int w, int h, const uint16_t *px) { + for (int j = 0; j < h; j++) + for (int i = 0; i < w; i++) fb[y + j][x + i] = px[j * w + i]; + if (on_frame && y + h == 240) on_frame(); +} +void *np_alloc(uint32_t n) { (void)n; return 0; } +static double clock_ms; +static uint32_t (*script)(uint32_t frame); +static uint32_t frame_no, quit_at; +static void (*watch)(void); +uint32_t plat_keys(void) { + clock_ms += 10; + frame_no++; + if (watch) watch(); + if (frame_no >= quit_at) return K_HOME; + return script ? script(frame_no) : 0; +} +uint32_t plat_millis(void) { return (uint32_t)clock_ms; } +void plat_sleep(uint32_t ms) { clock_ms += ms; } +uint32_t plat_random(void) { return 0x9E3779B9u ^ frame_no; } +void plat_begin(void) {} +int plat_end(void) { return 0; } + +static struct { char name[32]; uint8_t data[4096]; uint32_t len; bool on; } files[4]; +static bool storage_full; +bool plat_save(const char *name, const void *data, uint32_t len) { + if (storage_full) return false; + int k = -1; + for (int i = 0; i < 4; i++) + if (files[i].on && !strcmp(files[i].name, name)) k = i; + for (int i = 0; i < 4 && k < 0; i++) + if (!files[i].on) k = i; + if (k < 0 || len > sizeof files[k].data) return false; + strcpy(files[k].name, name), memcpy(files[k].data, data, len), files[k].len = len, files[k].on = true; + return true; +} +const uint8_t *plat_load(const char *name, uint32_t *len) { + for (int i = 0; i < 4; i++) + if (files[i].on && !strcmp(files[i].name, name)) return *len = files[i].len, files[i].data; + return 0; +} +static void forget(const char *name) { + for (int i = 0; i < 4; i++) + if (files[i].on && !strcmp(files[i].name, name)) files[i].on = false; +} +static uint8_t *file_data(const char *name, uint32_t *len) { + for (int i = 0; i < 4; i++) + if (files[i].on && !strcmp(files[i].name, name)) return *len = files[i].len, files[i].data; + return 0; +} + +static void shot(const char *dir, const char *name) { + char path[512]; + snprintf(path, sizeof path, "%s/%s.ppm", dir, name); + FILE *f = fopen(path, "wb"); + if (!f) exit(2); + fprintf(f, "P6 320 240 255\n"); + for (int y = 0; y < 240; y++) + for (int x = 0; x < 320; x++) { + uint16_t c = fb[y][x]; + fputc((c >> 11) * 255 / 31, f), fputc((c >> 5 & 63) * 255 / 63, f), fputc((c & 31) * 255 / 31, f); + } + fclose(f); +} + +static int fails; +#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL %s:%d: ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0) + +/* ---------------------------------------------------------------- tables made to order */ +#define R(c, v) ((c) * 13 + (v)) +static uno_game T; + +/* n players, the house rules, every card in the draw pile (in order, its top the last), nothing dealt; then + top(card) turns a card, give(p, card) hands one over */ +static void blank(uno_game *g, int n, int house) { + uno_new_game(g, (uno_rules){(uint8_t)n, LEVEL_NORMAL, 0, (uint8_t)house}, 7); + uno_fill_deck(g->deck); + g->dn = UNO_DECK, g->pn = 0; + memset(g->count, 0, sizeof g->count); + memset(g->hand, 0, sizeof g->hand); + g->turn = 0, g->dealer = (uint8_t)(n - 1), g->dir = 1, g->phase = P_PLAY, g->pending = 0, g->opening = 0; + g->out = g->uno_open = g->uno_said = g->winner = g->offender = g->drawn = UNO_NONE; +} +static void pull(uno_game *g, int card) { + for (int i = g->dn - 1; i >= 0; i--) + if (g->deck[i] == card) { + memmove(&g->deck[i], &g->deck[i + 1], (size_t)(g->dn - i - 1)); + g->dn--; + return; + } + printf("no %d left in the deck\n", card); + exit(1); +} +static void top(uno_game *g, int card) { + pull(g, card); + g->pile[g->pn++] = (uint8_t)card; + g->color = (uint8_t)uno_color(card); +} +static void give(uno_game *g, int p, int card) { + pull(g, card); + g->hand[p][g->count[p]++] = (uint8_t)card; +} +/* the draw pile's next card */ +static void next_draw(uno_game *g, int card) { + pull(g, card); + g->deck[g->dn++] = (uint8_t)card; +} +static int find(const uno_game *g, int p, int card) { + for (int i = 0; i < g->count[p]; i++) + if (g->hand[p][i] == card) return i; + return -1; +} + +/* ---------------------------------------------------------------- the rules */ +static void deck_and_points(void) { + uint8_t d[UNO_DECK]; + uno_fill_deck(d); + int per[5] = {0}, zero = 0, skip = 0, rev = 0, d2 = 0, wild = 0, w4 = 0; + for (int i = 0; i < UNO_DECK; i++) { + int c = d[i], v = uno_value(c); + per[uno_color(c)]++; + zero += c < 52 && v == 0, skip += c < 52 && v == V_SKIP, rev += c < 52 && v == V_REVERSE, d2 += c < 52 && v == V_DRAW2; + wild += c == C_WILD, w4 += c == C_WILD4; + } + CHECK(per[0] == 25 && per[1] == 25 && per[2] == 25 && per[3] == 25 && per[4] == 8, "25 cards a colour, 8 Wilds"); + CHECK(zero == 4 && skip == 8 && rev == 8 && d2 == 8 && wild == 4 && w4 == 4, "the action cards"); + int numbers = 0; + for (int i = 0; i < UNO_DECK; i++) numbers += d[i] < 52 && uno_value(d[i]) < 10; + CHECK(numbers == 76, "19 number cards a colour"); + CHECK(uno_points(R(RED, 0)) == 0 && uno_points(R(BLUE, 7)) == 7 && uno_points(R(GREEN, 9)) == 9, "numbers: face value"); + CHECK(uno_points(R(RED, V_SKIP)) == 20 && uno_points(R(RED, V_REVERSE)) == 20 && uno_points(R(RED, V_DRAW2)) == 20, + "Skip, Reverse, Draw Two: 20"); + CHECK(uno_points(C_WILD) == 50 && uno_points(C_WILD4) == 50, "Wilds: 50"); +} + +static void dealing(void) { + /* a new game: a dealer, seven cards each, a first card that isn't a Wild Draw Four */ + int dealers[4] = {0}; + for (uint32_t s = 1; s <= 400; s++) { + uno_new_game(&T, (uno_rules){4, LEVEL_NORMAL, 0, 0}, s); + CHECK(uno_valid(&T), "a valid deal (seed %u)", s); + CHECK(T.pn == 1 && T.pile[0] != C_WILD4, "no Wild Draw Four first (seed %u)", s); + int dealt = 0; + for (int p = 0; p < 4; p++) dealt += T.count[p]; + CHECK(dealt >= 28 && dealt <= 30 && T.dn == UNO_DECK - dealt - 1, "seven cards each (seed %u)", s); + dealers[T.dealer]++; + } + CHECK(dealers[0] > 60 && dealers[1] > 60 && dealers[2] > 60 && dealers[3] > 60, "anyone can deal: %d %d %d %d", + dealers[0], dealers[1], dealers[2], dealers[3]); + /* the first card: on a deck laid out so that it comes up after the deal */ + struct { int card, n, turn, dir, phase, victim_cards; } first[] = { + {R(RED, 5), 4, 0, 1, P_PLAY, 7}, /* the dealer (3)'s left (0) starts */ + {R(RED, V_SKIP), 4, 1, 1, P_PLAY, 7}, /* the dealer's left is skipped */ + {R(RED, V_REVERSE), 4, 3, -1, P_PLAY, 7}, /* the dealer starts, to the right */ + {R(RED, V_REVERSE), 2, 1, 1, P_PLAY, 7}, /* two players: the dealer starts */ + {R(RED, V_DRAW2), 4, 1, 1, P_PLAY, 9}, /* the dealer's left draws 2 and is skipped */ + {C_WILD, 4, 0, 1, P_COLOR, 7}, /* the dealer's left calls the colour, then plays */ + }; + for (unsigned k = 0; k < sizeof first / sizeof first[0]; k++) { + int n = first[k].n; + blank(&T, n, 0); + T.dealer = (uint8_t)(n - 1); + pull(&T, first[k].card); + memmove(&T.deck[UNO_DECK - 7 * n], &T.deck[UNO_DECK - 7 * n - 1], (size_t)(7 * n)); + T.deck[UNO_DECK - 7 * n - 1] = (uint8_t)first[k].card; + T.dn = UNO_DECK; + uno_deal(&T); + CHECK(T.pile[0] == first[k].card && T.turn == first[k].turn && T.dir == first[k].dir && T.phase == first[k].phase, + "first card %d: turn %d dir %d phase %d", first[k].card, T.turn, T.dir, T.phase); + CHECK(T.count[0] == first[k].victim_cards, "first card %d: the dealer's left has %d cards", first[k].card, T.count[0]); + CHECK(uno_valid(&T), "first card %d: valid", first[k].card); + } + /* a Wild first: the first player calls the colour, then plays */ + CHECK(uno_choose(&T, BLUE) && T.phase == P_PLAY && T.turn == 0 && T.color == BLUE, "the first Wild's colour"); + /* a Wild Draw Four turned first goes back in the deck and another is turned */ + blank(&T, 3, 0); + T.dealer = 2; + pull(&T, C_WILD4); + memmove(&T.deck[UNO_DECK - 21], &T.deck[UNO_DECK - 22], 21); + T.deck[UNO_DECK - 22] = C_WILD4; + T.dn = UNO_DECK; + uno_deal(&T); + CHECK(T.pile[0] != C_WILD4 && T.pn == 1 && uno_valid(&T), "a first Wild Draw Four is turned back"); + /* the deal goes to the left */ + uno_new_game(&T, (uno_rules){3, LEVEL_NORMAL, 0, 0}, 99); + int d0 = T.dealer; + uno_next_hand(&T); + CHECK(T.dealer == (d0 + 1) % 3 && T.hands == 2, "the deal passes to the left"); +} + +static void action_cards(void) { + /* matching */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 2)), give(&T, 0, R(BLUE, 5)), give(&T, 0, R(BLUE, 7)), give(&T, 0, C_WILD), give(&T, 0, C_WILD4); + CHECK(uno_can_play(&T, 0) && uno_can_play(&T, 1) && !uno_can_play(&T, 2) && uno_can_play(&T, 3) && uno_can_play(&T, 4), + "colour, number, Wilds"); + CHECK(!uno_play(&T, 2, false) && T.count[0] == 5, "a card that doesn't go"); + /* a number: the next player */ + CHECK(uno_play(&T, 0, false) && T.turn == 1 && T.color == RED && T.pile[T.pn - 1] == R(RED, 2), "a number"); + /* symbols match symbols */ + blank(&T, 3, 0); + top(&T, R(RED, V_SKIP)); + give(&T, 0, R(GREEN, V_SKIP)), give(&T, 0, R(GREEN, 1)); + CHECK(uno_can_play(&T, 0) && !uno_can_play(&T, 1), "a Skip on a Skip"); + /* Skip */ + CHECK(uno_play(&T, 0, false) && T.turn == 2 && T.color == GREEN, "Skip: player 1 misses a turn"); + /* Reverse, three and four players */ + for (int n = 3; n <= 4; n++) { + blank(&T, n, 0); + top(&T, R(RED, 1)); + give(&T, 0, R(RED, V_REVERSE)), give(&T, 0, R(RED, 3)); + CHECK(uno_play(&T, 0, false) && T.dir == -1 && T.turn == n - 1, "Reverse (%d players): to the right", n); + } + /* Draw Two */ + blank(&T, 3, 0); + top(&T, R(BLUE, 1)); + give(&T, 0, R(BLUE, V_DRAW2)), give(&T, 0, R(RED, 3)); + CHECK(uno_play(&T, 0, false) && T.count[1] == 2 && T.turn == 2 && T.phase == P_PLAY, "Draw Two: 2 cards, skipped"); + CHECK(T.ev_victim == 1 && T.ev_n == 2, "Draw Two: the message"); + /* Wild */ + blank(&T, 3, 0); + top(&T, R(BLUE, 1)); + give(&T, 0, C_WILD), give(&T, 0, R(RED, 3)); + CHECK(uno_play(&T, 0, false) && T.phase == P_COLOR && T.turn == 0 && T.color == NO_COLOR, "Wild: call a colour"); + CHECK(uno_valid(&T), "valid while calling"); + CHECK(!uno_choose(&T, 4) && uno_choose(&T, GREEN) && T.color == GREEN && T.turn == 1 && T.phase == P_PLAY, "Wild: green"); + /* Wild Draw Four, taken */ + blank(&T, 3, 0); + top(&T, R(BLUE, 1)); + give(&T, 0, C_WILD4), give(&T, 0, R(RED, 3)); + CHECK(uno_play(&T, 0, false) && T.phase == P_COLOR && uno_choose(&T, RED), "Wild Draw Four: a colour"); + CHECK(T.phase == P_PENALTY && T.turn == 1 && T.pending == 4 && T.challengeable && !T.guilty, "next player: 4 waiting"); + CHECK(uno_valid(&T), "valid with a penalty waiting"); + CHECK(!uno_draw(&T) && uno_take(&T) && T.count[1] == 4 && T.turn == 2 && T.color == RED, "taken: 4 cards, skipped"); +} + +static void challenge(void) { + /* guilty: it had a card of the colour in play */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, C_WILD4), give(&T, 0, R(RED, 8)), give(&T, 0, R(BLUE, 2)); + give(&T, 1, R(GREEN, 1)); + CHECK(!uno_legal_wild4(&T, 0), "a red card: not allowed"); + CHECK(uno_play(&T, 0, false) && uno_choose(&T, BLUE) && T.guilty && T.prev_color == RED, "a bluff"); + CHECK(uno_challenge(&T) && T.count[0] == 6 && T.count[1] == 1 && T.turn == 1 && T.phase == P_PLAY, + "challenge won: the bluffer draws 4, the challenger plays"); + CHECK(T.color == BLUE && T.ev == E_CHALLENGE_WON, "the colour called stays"); + /* not guilty: other numbers and symbols, and Wilds, don't count */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, C_WILD4), give(&T, 0, R(BLUE, 5)), give(&T, 0, C_WILD), give(&T, 0, R(GREEN, V_SKIP)); + give(&T, 1, R(GREEN, 1)); + CHECK(uno_legal_wild4(&T, 0), "no red card: allowed"); + CHECK(uno_play(&T, 0, false) && uno_choose(&T, BLUE) && !T.guilty, "honest"); + CHECK(uno_challenge(&T) && T.count[1] == 7 && T.turn == 2 && T.count[0] == 3, "challenge lost: 6 cards, skipped"); + /* only a waiting Wild Draw Four can be challenged */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, V_DRAW2)), give(&T, 0, R(RED, 1)); + uno_play(&T, 0, false); + CHECK(!uno_challenge(&T), "no challenge on a Draw Two"); + /* two players: after a Wild Draw Four taken, the same player again */ + blank(&T, 2, 0); + top(&T, R(RED, 5)); + give(&T, 0, C_WILD4), give(&T, 0, R(BLUE, 1)); + uno_play(&T, 0, false), uno_choose(&T, BLUE); + CHECK(uno_take(&T) && T.turn == 0 && T.count[1] == 4, "two players: +4 taken, the same player plays"); +} + +static void two_players(void) { + blank(&T, 2, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, V_REVERSE)), give(&T, 0, R(RED, V_SKIP)), give(&T, 0, R(RED, V_DRAW2)), give(&T, 0, R(RED, 1)); + CHECK(uno_play(&T, 0, false) && T.turn == 0 && T.dir == 1, "Reverse is a Skip: play again"); + CHECK(uno_play(&T, 0, false) && T.turn == 0, "Skip: play again"); + CHECK(uno_play(&T, 0, false) && T.turn == 0 && T.count[1] == 2, "Draw Two: 2 cards, play again"); +} + +static void drawing(void) { + /* a card that goes: it may be played, and only it */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(BLUE, 1)), give(&T, 0, R(RED, 2)); + next_draw(&T, R(RED, 9)); + CHECK(uno_draw(&T) && T.phase == P_DRAWN && T.count[0] == 3 && T.drawn == 2 && T.turn == 0, "drew a card that goes"); + CHECK(!uno_can_play(&T, 1) && uno_can_play(&T, 2), "only the card drawn"); + CHECK(uno_play(&T, 2, false) && T.turn == 1 && T.pile[T.pn - 1] == R(RED, 9), "played at once"); + /* or kept */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(BLUE, 1)); + next_draw(&T, R(RED, 9)); + CHECK(uno_draw(&T) && uno_keep(&T) && T.turn == 1 && T.count[0] == 2, "kept"); + /* one that doesn't go: the turn passes */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(BLUE, 1)); + next_draw(&T, R(GREEN, 9)); + CHECK(uno_draw(&T) && T.turn == 1 && T.phase == P_PLAY && T.lacks[0] == 1 << RED, "no luck: next player"); + /* allowed with a card that goes */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 1)); + CHECK(uno_draw(&T), "drawing instead of playing"); + /* the draw pile runs out: the discards but the top one are shuffled into it */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(BLUE, 1)); + while (T.dn > 1) T.pile[T.pn++] = T.deck[--T.dn]; + int last = T.deck[0], topc = T.pile[T.pn - 1], before = T.pn; + T.color = (uint8_t)(uno_color(topc) < 4 ? uno_color(topc) : RED); + T.dn = 0, T.pile[T.pn++] = (uint8_t)last; + T.color = (uint8_t)(uno_color(last) < 4 ? uno_color(last) : RED); + CHECK(uno_valid(&T), "an empty draw pile: valid"); + uno_draw(&T); + CHECK(T.pn == 1 && T.pile[0] == last && T.dn == before - 1 && uno_valid(&T), "a new draw pile, the top card stays"); +} + +static void uno_calls(void) { + /* forgot: caught, 2 cards */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 1)), give(&T, 0, R(BLUE, 2)); + CHECK(uno_play(&T, 0, false) && T.uno_open == 0, "one card left, no UNO"); + CHECK(!uno_catch(&T, 0), "you can't catch yourself"); + CHECK(uno_catch(&T, 2) && T.count[0] == 3 && T.uno_open == UNO_NONE && T.ev == E_CAUGHT, "caught: 2 cards"); + /* said in time, after the card: safe */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 1)), give(&T, 0, R(BLUE, 2)); + uno_play(&T, 0, false); + CHECK(uno_call(&T, 0) && T.uno_open == UNO_NONE && !uno_catch(&T, 1), "said before anyone caught it"); + /* said with the next-to-last card */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 1)), give(&T, 0, R(BLUE, 2)); + CHECK(uno_call(&T, 0) && uno_play(&T, 0, false) && T.uno_open == UNO_NONE, "said first"); + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 1)), give(&T, 0, R(BLUE, 2)); + CHECK(uno_play(&T, 0, true) && T.uno_open == UNO_NONE && T.ev_by == 0, "said with the card"); + /* too late once the next player has started */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 1)), give(&T, 0, R(BLUE, 2)); + give(&T, 1, R(RED, 3)), give(&T, 1, R(RED, 4)); + uno_play(&T, 0, false); + CHECK(uno_play(&T, 0, true) && !uno_catch(&T, 2) && T.count[0] == 1, "too late after the next card"); + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 1)), give(&T, 0, R(BLUE, 2)); + uno_play(&T, 0, false); + CHECK(uno_draw(&T) && !uno_catch(&T, 2), "too late after the next player draws"); + /* not with more cards */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 1)), give(&T, 0, R(BLUE, 2)), give(&T, 0, R(BLUE, 3)); + CHECK(!uno_call(&T, 0), "UNO with three cards"); + /* the window stays open while a Wild's colour is called */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, C_WILD), give(&T, 0, R(BLUE, 2)); + uno_play(&T, 0, false); + CHECK(T.uno_open == 0 && uno_choose(&T, BLUE) && T.uno_open == 0 && uno_catch(&T, 1) && T.count[0] == 3, + "caught after the colour"); + /* house rule: UNO said for everyone */ + blank(&T, 3, H_AUTO_UNO); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 1)), give(&T, 0, R(BLUE, 2)); + CHECK(uno_play(&T, 0, false) && T.uno_open == UNO_NONE, "automatic UNO"); +} + +static void going_out(void) { + /* a number: the hand is over, the others' cards scored */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 1)); + give(&T, 1, R(BLUE, 7)), give(&T, 1, C_WILD), give(&T, 2, R(GREEN, V_SKIP)), give(&T, 2, R(GREEN, 3)); + CHECK(uno_play(&T, 0, false) && T.phase == P_HAND_OVER && T.winner == 0 && T.gained == 7 + 50 + 20 + 3 && + T.score[0] == 80, "out: 80 points"); + CHECK(uno_valid(&T), "valid after the hand"); + /* a Draw Two last: the next player draws and it counts */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, V_DRAW2)); + give(&T, 1, R(BLUE, 7)), give(&T, 2, R(GREEN, 3)); + next_draw(&T, R(YELLOW, 9)), next_draw(&T, R(YELLOW, 8)); + CHECK(uno_play(&T, 0, false) && T.phase == P_HAND_OVER && T.count[1] == 3 && T.gained == 7 + 8 + 9 + 3, + "a last Draw Two counts: %d", T.gained); + /* a Wild last: no colour to call */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, C_WILD), give(&T, 1, R(BLUE, 7)), give(&T, 2, R(GREEN, 3)); + CHECK(uno_play(&T, 0, false) && T.phase == P_HAND_OVER && T.gained == 10, "out on a Wild"); + /* a Wild Draw Four last, taken: counts */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, C_WILD4), give(&T, 1, R(BLUE, 7)), give(&T, 2, R(GREEN, 3)); + next_draw(&T, R(YELLOW, 1)), next_draw(&T, R(YELLOW, 2)), next_draw(&T, R(YELLOW, 3)), next_draw(&T, R(YELLOW, 4)); + CHECK(uno_play(&T, 0, false) && T.phase == P_COLOR && uno_choose(&T, GREEN) && T.phase == P_PENALTY, "a last +4"); + CHECK(uno_valid(&T), "valid with the last +4 waiting"); + CHECK(uno_take(&T) && T.phase == P_HAND_OVER && T.gained == 7 + 10 + 3, "a last +4 taken counts: %d", T.gained); + /* a Wild Draw Four last is always allowed: challenged, the challenger draws 6 and it counts */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, C_WILD4), give(&T, 1, R(BLUE, 7)), give(&T, 2, R(GREEN, 3)); + uno_play(&T, 0, false), uno_choose(&T, GREEN); + CHECK(!T.guilty && uno_challenge(&T) && T.phase == P_HAND_OVER && T.count[1] == 7, "a last +4 challenged: 6 cards"); + /* to 500 */ + blank(&T, 2, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 1)), give(&T, 1, C_WILD); + T.score[0] = 460, T.score[1] = 499; + CHECK(uno_play(&T, 0, false) && T.phase == P_GAME_OVER && T.winner == 0 && T.score[0] == 510, "500: the game is won"); + blank(&T, 2, 0); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 1)), give(&T, 1, R(BLUE, 2)); + T.score[0] = 400; + CHECK(uno_play(&T, 0, false) && T.phase == P_HAND_OVER && T.score[0] == 402, "short of 500: another hand"); + /* a single hand */ + blank(&T, 2, 0); + T.r.one_hand = 1; + top(&T, R(RED, 5)); + give(&T, 0, R(RED, 1)), give(&T, 1, R(BLUE, 2)); + CHECK(uno_play(&T, 0, false) && T.phase == P_GAME_OVER && T.winner == 0, "one hand: over"); +} + +static void house_rules(void) { + /* stacking Draw Twos */ + blank(&T, 3, H_STACK2); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, V_DRAW2)), give(&T, 0, R(RED, 1)); + give(&T, 1, R(BLUE, V_DRAW2)), give(&T, 1, R(BLUE, 1)); + give(&T, 2, R(GREEN, 1)); + CHECK(uno_play(&T, 0, false) && T.phase == P_PENALTY && T.turn == 1 && T.pending == 2 && !T.challengeable, "+2 waits"); + CHECK(uno_can_stack(&T) && !uno_can_play(&T, 1) && uno_play(&T, 0, false) && T.turn == 2 && T.pending == 4, "stacked"); + CHECK(!uno_can_stack(&T) && uno_take(&T) && T.count[2] == 5 && T.turn == 0, "4 taken"); + /* stacking Wild Draw Fours: a stacked one can't be challenged */ + blank(&T, 3, H_STACK4); + top(&T, R(RED, 5)); + give(&T, 0, C_WILD4), give(&T, 0, R(GREEN, 1)), give(&T, 1, C_WILD4), give(&T, 1, R(BLUE, 1)), give(&T, 2, R(GREEN, 2)); + uno_play(&T, 0, false), uno_choose(&T, BLUE); + CHECK(T.challengeable && uno_can_stack(&T) && uno_play(&T, 0, false) && uno_choose(&T, GREEN), "+4 on +4"); + CHECK(T.turn == 2 && T.pending == 8 && !T.challengeable && !uno_challenge(&T) && uno_take(&T) && T.count[2] == 9, + "8 taken, no challenge"); + /* no stacking on a last card */ + blank(&T, 3, H_STACK2); + top(&T, R(RED, 5)); + give(&T, 0, R(RED, V_DRAW2)), give(&T, 1, R(BLUE, V_DRAW2)), give(&T, 2, R(GREEN, 1)); + CHECK(uno_play(&T, 0, false) && T.phase == P_HAND_OVER && T.count[1] == 3, "a last Draw Two is taken at once"); + /* without stacking a Draw Two can't answer a waiting Wild Draw Four */ + blank(&T, 3, 0); + top(&T, R(RED, 5)); + give(&T, 0, C_WILD4), give(&T, 0, R(GREEN, 1)), give(&T, 1, R(BLUE, V_DRAW2)), give(&T, 1, C_WILD4); + uno_play(&T, 0, false), uno_choose(&T, BLUE); + CHECK(!uno_can_stack(&T) && !uno_can_play(&T, 0) && !uno_can_play(&T, 1), "official: no stacking"); + /* draw until you can play */ + blank(&T, 3, H_DRAW_UNTIL); + top(&T, R(RED, 5)); + give(&T, 0, R(BLUE, 1)); + next_draw(&T, R(RED, 9)), next_draw(&T, R(GREEN, 2)), next_draw(&T, R(GREEN, 3)); + CHECK(uno_draw(&T) && T.count[0] == 4 && T.phase == P_DRAWN && T.ev_n == 3, "drew 3 until one goes"); + /* a playable drawn card must be played */ + blank(&T, 3, H_MUST_PLAY); + top(&T, R(RED, 5)); + give(&T, 0, R(BLUE, 1)); + next_draw(&T, R(RED, 9)); + CHECK(uno_draw(&T) && !uno_keep(&T) && uno_play(&T, 1, false), "must play it"); +} + +static void validity(void) { + uno_new_game(&T, (uno_rules){4, LEVEL_HARD, 0, H_ALL}, 5); + CHECK(uno_valid(&T), "a fresh game"); + uno_game b = T; + b.hand[1][0] = b.hand[2][0]; + CHECK(!uno_valid(&b), "a card twice"); + b = T, b.count[0]++; + CHECK(!uno_valid(&b), "a card too many"); + b = T, b.turn = 4; + CHECK(!uno_valid(&b), "a turn past the table"); + b = T, b.phase = 9; + CHECK(!uno_valid(&b), "a phase that isn't"); + b = T, b.color = NO_COLOR; + CHECK(!uno_valid(&b), "no colour outside a Wild"); +} + +/* ---------------------------------------------------------------- the bots */ +static uint32_t hash_game(const uno_game *g, uint32_t h) { + return uno_sum(g, sizeof *g) ^ (h * 16777619u); +} + +/* a whole game, bots in every seat (their levels), catches included; the hash of every step */ +static int bot_game(uno_rules r, uint32_t seed, const int *levels, uint32_t *hash, int *hands_out) { + uno_new_game(&T, r, seed); + uint32_t h = 0; + int steps = 0, gained = 0, hands = 1; + while (T.phase != P_GAME_OVER && steps < 400000) { + if (T.phase == P_HAND_OVER) { + gained += T.gained; + uno_next_hand(&T); + hands++; + CHECK(uno_valid(&T), "a new hand: valid"); + continue; + } + int p = T.turn; + uno_move m = bot_move(&T, levels[p]); + if (!bot_do(&T, m)) { + CHECK(0, "seed %u step %d: the bot's move %d (%d) was refused (phase %d)", seed, steps, m.act, m.arg, T.phase); + return -1; + } + if (T.uno_open != UNO_NONE) + for (int b = 0; b < r.players; b++) { + uint32_t ms; + if (b != T.uno_open && bot_catches(&T, b, levels[b], &ms)) { + uno_catch(&T, b); + break; + } + } + if (!uno_valid(&T)) { + CHECK(0, "seed %u step %d: not valid after move %d", seed, steps, m.act); + return -1; + } + h = hash_game(&T, h); + steps++; + } + CHECK(T.phase == P_GAME_OVER, "seed %u: the game ends (%d steps)", seed, steps); + gained += T.gained; + int total = 0; + for (int p = 0; p < r.players; p++) total += T.score[p]; + CHECK(total == gained, "seed %u: the scores add up (%d, %d)", seed, total, gained); + CHECK(r.one_hand || T.score[T.winner] >= UNO_GOAL, "seed %u: 500 reached", seed); + if (hash) *hash = h; + if (hands_out) *hands_out = hands; + return T.winner; +} + +static void bots(void) { + int games = 0, hands = 0; + for (int n = 2; n <= 4; n++) + for (int level = 0; level < 3; level++) + for (int house = 0; house < 4; house++) { + static const uint8_t mix[4] = {0, H_STACK2 | H_STACK4, H_DRAW_UNTIL | H_MUST_PLAY, H_ALL}; + int levels[4] = {level, level, level, level}; + for (uint32_t s = 1; s <= 4; s++) { + int k = 0; + bot_game((uno_rules){(uint8_t)n, (uint8_t)level, 0, mix[house]}, s * 7919 + (uint32_t)(n * 100 + level * 10 + house), + levels, 0, &k); + games++, hands += k; + } + } + printf("bots: %d games to 500 (%d hands), every step valid\n", games, hands); + /* the levels: who wins, two players to 500 */ + struct { int a, b, min; } duel[] = {{LEVEL_HARD, LEVEL_EASY, 60}, {LEVEL_NORMAL, LEVEL_EASY, 55}, {LEVEL_HARD, LEVEL_NORMAL, 50}}; + for (int d = 0; d < 3; d++) { + int wins = 0, n = 200; + for (int s = 0; s < n; s++) { + /* each seat in turn, so the dealer's luck evens out */ + int levels[4] = {duel[d].a, duel[d].b, 0, 0}; + if (s & 1) levels[0] = duel[d].b, levels[1] = duel[d].a; + int w = bot_game((uno_rules){2, LEVEL_NORMAL, 0, 0}, 1000u + (uint32_t)s, levels, 0, 0); + wins += w == (s & 1 ? 1 : 0); + } + printf("bots: %s beat %s in %d of %d games\n", (const char *[]){"Easy", "Normal", "Hard"}[duel[d].a], + (const char *[]){"Easy", "Normal", "Hard"}[duel[d].b], wins, n); + CHECK(wins * 100 >= duel[d].min * n, "level %d against %d: %d of %d", duel[d].a, duel[d].b, wins, n); + } + /* four at the table, every level, many hands: the same game twice, to the same hash on every computer */ + uint32_t h1, h2; + int levels[4] = {LEVEL_HARD, LEVEL_EASY, LEVEL_NORMAL, LEVEL_HARD}; + bot_game((uno_rules){4, LEVEL_NORMAL, 0, 0}, 2026, levels, &h1, 0); + bot_game((uno_rules){4, LEVEL_NORMAL, 0, 0}, 2026, levels, &h2, 0); + printf("bots: a game's hash %08X\n", h1); + CHECK(h1 == h2 && h1 == 0x6205C6AFu, "the same game twice, and as on every computer: %08X", h1); +} + +/* ---------------------------------------------------------------- saves */ +static void saves(void) { + memset(files, 0, sizeof files); + uno_progress a, b; + uno_fresh(&a); + CHECK(!save_load(&b) && b.setup.players == 4 && b.setup.level == LEVEL_NORMAL && !b.has_game, "no save: a fresh start"); + a.setup = (uno_rules){3, LEVEL_HARD, 1, H_STACK2 | H_AUTO_UNO}; + a.guide = 1, a.has_game = 1; + uno_new_game(&a.g, a.setup, 31337); + for (int i = 0; i < 40 && a.g.phase < P_HAND_OVER; i++) bot_do(&a.g, bot_move(&a.g, LEVEL_NORMAL)); + CHECK(save_write(&a), "written"); + uint32_t n; + uint8_t *d = file_data(UNO_SAVE_NAME, &n); + CHECK(d && n == sizeof(uno_file) && n == 728 && !memcmp(d, "UNO1", 4), "uno.sav: %u bytes", n); + CHECK(save_load(&b) && !memcmp(&a, &b, sizeof a), "read back exactly"); + /* damaged: the copy in the script brings it back */ + d[100] ^= 0x55; + CHECK(save_load(&b) && !memcmp(&a, &b, sizeof a), "a damaged save: back from the copy"); + d = file_data(UNO_SAVE_NAME, &n); + CHECK(d && !memcmp(d + 4, (uint8_t[]){1, 0, 216, 2}, 4), "and written again"); + /* cut short */ + files[0].len = 300; + CHECK(save_load(&b) && !memcmp(&a, &b, sizeof a), "a save cut short: back from the copy"); + /* both damaged: a clean fresh start */ + d = file_data(UNO_SAVE_NAME, &n); + d[200] ^= 1; + uint8_t *c = file_data(UNO_COPY_NAME, &n); + c[n - 10] = c[n - 10] == 'A' ? 'B' : 'A'; + CHECK(!save_load(&b) && !b.has_game && b.setup.players == 4, "both damaged: a fresh start"); + /* the script gone too */ + forget(UNO_COPY_NAME); + CHECK(!save_load(&b) && !b.has_game, "damaged, no copy: a fresh start"); + /* the right checksum over a wrong game: refused */ + save_write(&a); + d = file_data(UNO_SAVE_NAME, &n); + uno_file f; + memcpy(&f, d, sizeof f); + f.p.g.hand[0][0] = f.p.g.hand[1][0]; + f.sum = uno_sum(&f, offsetof(uno_file, sum)); + memcpy(d, &f, sizeof f); + forget(UNO_COPY_NAME); + CHECK(!save_load(&b) && !b.has_game, "a card twice, summed right: refused"); + /* another version: refused */ + save_write(&a); + d = file_data(UNO_SAVE_NAME, &n); + memcpy(&f, d, sizeof f); + f.version = 2; + f.sum = uno_sum(&f, offsetof(uno_file, sum)); + memcpy(d, &f, sizeof f); + forget(UNO_COPY_NAME); + CHECK(!save_load(&b), "another version: refused"); + /* where the emulator test looks: has_game, the players, the score */ + CHECK(offsetof(uno_file, p.has_game) == 13 && offsetof(uno_file, p.g.r.players) == 16 && + offsetof(uno_file, p.g.score) == 674 && offsetof(uno_file, p.g.hands) == 682, + "offsets %zu %zu %zu %zu", offsetof(uno_file, p.has_game), offsetof(uno_file, p.g.r.players), + offsetof(uno_file, p.g.score), offsetof(uno_file, p.g.hands)); +} + +/* ---------------------------------------------------------------- the whole app, with keys */ +static uint32_t lcg; +static uint32_t rnd(void) { return lcg = lcg * 1103515245u + 12345u, lcg >> 8; } +static uint32_t held_keys, held_until; +static uint32_t random_keys(uint32_t f) { + if (f >= held_until) { + static const uint32_t pick[] = {K_OK, K_OK, K_OK, K_LEFT, K_RIGHT, K_RIGHT, K_UP, K_UP, K_DOWN, K_SHIFT, K_BACK, 0, 0, 0}; + held_keys = pick[rnd() % (sizeof pick / sizeof pick[0])]; + held_until = f + 2 + rnd() % 12; + } + return held_keys; +} +static int bad_frames; +static void check_state(void) { + if (ui.has_game && !uno_valid(G)) bad_frames++; + if (ui.screen > V_QUIT || (ui.screen == V_PLAY && !ui.has_game && G->phase != P_GAME_OVER)) bad_frames++; +} + +static void fuzz(void) { + memset(files, 0, sizeof files); + int quits = 0, games = 0; + for (int s = 0; s < 300; s++) { + lcg = (uint32_t)s * 2654435761u + 1; + if (s % 50 == 0) memset(files, 0, sizeof files); + storage_full = s % 37 == 36; + script = random_keys, watch = check_state; + frame_no = 0, quit_at = 1500 + rnd() % 3000, held_until = 0; + uno_app_main(); + quits++; + storage_full = false; + uno_progress p; + if (save_load(&p)) games += p.has_game; + CHECK(!bad_frames, "session %d: %d bad frames", s, bad_frames); + bad_frames = 0; + } + script = 0, watch = 0; + printf("keys: %d sessions of random keys, quit with Home each time (%d left a game going)\n", quits, games); +} + +/* a game played through the app's own keys: the menu, how to play, a deal; your turns played by the Normal bot + through the keys a player would press */ +static bool game_done; +static uint32_t key_every = 2; /* (frames from a press to the next) */ +static uint32_t bot_keys(uint32_t f) { + if (f % key_every || game_done) return 0; /* keys up between presses */ + uno_game *g = G; + if (ui.screen != V_PLAY) return K_OK; + if (g->phase >= P_HAND_OVER) return ui.show_end ? K_OK : 0; + if (g->turn != 0) return 0; + if (g->uno_open == 0) return K_SHIFT; + uno_move m = bot_move(g, LEVEL_NORMAL); + switch (m.act) { + case M_COLOR: return ui.sel == m.arg ? K_OK : K_RIGHT; + case M_DRAW: return K_UP; + case M_KEEP: return K_UP; + case M_TAKE: return g->challengeable ? (ui.sel == PEN_TAKE ? K_OK : K_UP) : (uno_can_stack(g) ? K_UP : 0); + case M_CHALLENGE: return ui.sel == PEN_CHALLENGE ? K_OK : K_DOWN; + default: + if (g->phase == P_PENALTY && g->challengeable) return ui.sel == PEN_STACK ? K_OK : K_DOWN; + if (g->phase == P_DRAWN) return K_OK; + if (m.uno && g->uno_said != 0) return K_SHIFT; + return ui.pick == m.arg ? K_OK : K_RIGHT; + } +} + +static void until_won(void) { + check_state(); + if (!game_done && ui.screen == V_PLAY && G->phase == P_GAME_OVER && ui.show_end) game_done = true, quit_at = frame_no + 2; +} +static void through_the_keys(void) { + memset(files, 0, sizeof files); + script = bot_keys, watch = until_won; + frame_no = 0, quit_at = 2000000, game_done = false, clock_ms = 0; + uno_app_main(); + uno_progress p; + CHECK(save_load(&p) && p.guide, "how to play shown"); + CHECK(game_done && G->score[G->winner] >= UNO_GOAL, "a game to 500 through the keys"); + printf("keys: a game to 500 through the app's keys, %u hands in %.0f minutes, scores %u %u %u %u\n", G->hands, + clock_ms / 60000, G->score[0], G->score[1], G->score[2], G->score[3]); + CHECK(!bad_frames, "%d bad frames", bad_frames); + script = 0, watch = 0; +} + +/* every key line fits */ +static void texts(void) { + uno_ui v; + memset(&v, 0, sizeof v); + uno_new_game(&T, (uno_rules){4, LEVEL_NORMAL, 0, H_ALL}, 3); + int widest = 0; + for (int screen = 0; screen <= V_QUIT; screen++) + for (int phase = 0; phase <= P_GAME_OVER; phase++) + for (int sel = 0; sel < SETUP_ROWS; sel++) + for (int k = 0; k < 8; k++) { + v.screen = (uint8_t)screen, v.sel = (uint8_t)sel, v.first_run = k & 1, v.show_end = k >> 1 & 1; + T.phase = (uint8_t)phase, T.turn = (uint8_t)(k >> 2 & 1), T.challengeable = k & 1, T.count[0] = (uint8_t)(1 + (k & 2)); + T.uno_open = k == 6 ? 1 : UNO_NONE, T.r.house = k & 4 ? H_MUST_PLAY : 0; + int w = uno_keyline_width(uno_keys(&T, &v)); + if (w > widest) widest = w; + CHECK(w <= 314, "key line too wide (%d): %s", w, uno_keys(&T, &v)); + } + printf("texts: the widest key line is %d pixels\n", widest); + for (int pg = 0; pg < UNO_HELP_PAGES; pg++) + CHECK(uno_help_bottom(pg) <= 86 + 4 * 19, "how to play, page %d: more than four lines (%d)", pg + 1, uno_help_bottom(pg)); + /* the longest messages */ + static const char *const msgs[] = {"Bot 3 played Wild +4: Yellow, UNO!", "Bot 2 played Yellow +2: Bot 3 draws 2, UNO!", + "Bot 1 challenged: Bot 3 bluffed, draws 4", "UNO is for your next-to-last card", + "Bot 3 deals: Yellow +2, you draw 2", "Only the new card can be played"}; + for (unsigned i = 0; i < sizeof msgs / sizeof msgs[0]; i++) + CHECK(uno_text_width(msgs[i]) <= 252, "message too wide: %s (%d)", msgs[i], uno_text_width(msgs[i])); +} + +static void test(const char *dir) { + (void)dir; + deck_and_points(); + dealing(); + action_cards(); + challenge(); + two_players(); + drawing(); + uno_calls(); + going_out(); + house_rules(); + validity(); + printf("rules: checked\n"); + bots(); + saves(); + texts(); + through_the_keys(); + fuzz(); +} + +/* ---------------------------------------------------------------- pictures */ +static void picture(const char *dir, const char *name) { + uno_render(G, &ui); + shot(dir, name); +} + +/* the bots play on until it's your turn in phase `phase` (and `count` cards or fewer in your hand, when not 0) */ +static bool until_mine(int phase, int most) { + for (int i = 0; i < 20000; i++) { + uno_game *g = G; + if (g->phase >= P_HAND_OVER) uno_next_hand(g); + if (g->turn == 0 && g->phase == phase && (!most || g->count[0] <= most)) return true; + bot_do(g, bot_move(g, g->turn ? g->r.level : LEVEL_NORMAL)); + describe(); + } + return false; +} + +void uno_render_cards(void); +void uno_render_icon(uint16_t bg); +static void shots(const char *dir) { + uno_render_cards(); + shot(dir, "cards"); + uno_render_icon(0); + shot(dir, "icon_black"); + uno_render_icon(0xFFFF); + shot(dir, "icon_white"); + memset(files, 0, sizeof files); + memset(&ui, 0, sizeof ui); + sv.guide = 1; + ui.setup = (uno_rules){4, LEVEL_NORMAL, 0, 0}; + ui.has_game = 1; + /* the table: your turn */ + uno_new_game(G, ui.setup, 12); + for (int i = 0; i < 9; i++) bot_do(G, bot_move(G, LEVEL_NORMAL)), describe(); + until_mine(P_PLAY, 0); + ui.screen = V_PLAY, ui.pick = 2; + for (int i = 0; i < G->count[0]; i++) + if (uno_can_play(G, i)) { ui.pick = (uint8_t)i; break; } + picture(dir, "shot"); + /* a Wild: calling the colour */ + uno_new_game(G, (uno_rules){3, LEVEL_HARD, 0, 0}, 77); + for (int k = 0; k < 400; k++) { + until_mine(P_PLAY, 0); + int w = find(G, 0, C_WILD); + if (w >= 0 && uno_can_play(G, w)) { uno_play(G, w, false), describe(); break; } + bot_do(G, bot_move(G, LEVEL_NORMAL)), describe(); + } + ui.sel = (uint8_t)bot_best_color(G, 0, LEVEL_NORMAL), ui.pick = 0; + picture(dir, "colour"); + /* a Wild Draw Four played on you */ + uno_new_game(G, (uno_rules){2, LEVEL_HARD, 0, 0}, 5); + for (int k = 0; k < 100000 && !(G->turn == 0 && G->phase == P_PENALTY && G->challengeable); k++) { + if (G->phase >= P_HAND_OVER) uno_next_hand(G); + bot_do(G, bot_move(G, LEVEL_NORMAL)), describe(); + } + ui.sel = 1; + picture(dir, "challenge"); + /* the hand you were shown */ + int o = G->offender; + memcpy(ui.reveal, G->hand[o], G->count[o]); + ui.reveal_n = G->count[o], ui.reveal_who = (uint8_t)o, ui.reveal_color = G->prev_color, ui.reveal_guilty = G->guilty; + uno_challenge(G), describe(); + ui.screen = V_REVEAL; + picture(dir, "reveal"); + /* UNO! */ + uno_new_game(G, (uno_rules){4, LEVEL_NORMAL, 0, 0}, 41); + ui.screen = V_PLAY; + for (int k = 0; k < 400000; k++) { + if (G->phase >= P_HAND_OVER) uno_next_hand(G); + if (G->turn == 0 && G->phase == P_PLAY && G->count[0] == 2 && uno_can_play(G, 0)) break; + bot_do(G, bot_move(G, LEVEL_NORMAL)), describe(); + } + ui.pick = 0; + picture(dir, "uno"); + /* the end of a hand */ + uno_new_game(G, (uno_rules){4, LEVEL_NORMAL, 0, 0}, 8); + G->score[1] = 120, G->score[2] = 245, G->score[3] = 60; + for (int k = 0; k < 100000 && G->phase < P_HAND_OVER; k++) bot_do(G, bot_move(G, LEVEL_HARD)), describe(); + ui.show_end = 1, ui.sel = 0; + picture(dir, "scores"); + /* the end of a game */ + G->phase = P_GAME_OVER, G->winner = 0, G->score[0] = 512, G->gained = 96; + picture(dir, "won"); + /* two players */ + uno_new_game(G, (uno_rules){2, LEVEL_EASY, 0, 0}, 3); + until_mine(P_PLAY, 0); + ui.show_end = 0, ui.pick = 1; + picture(dir, "duel"); + /* the menus */ + ui.screen = V_PAUSE, ui.sel = 0; + picture(dir, "pause"); + ui.screen = V_QUIT, ui.sel = 0; + picture(dir, "quit"); + ui.screen = V_TITLE, ui.sel = 0, ui.has_game = 1; + picture(dir, "title"); + ui.screen = V_SETUP, ui.sel = ROW_DEAL, ui.setup = (uno_rules){4, LEVEL_NORMAL, 0, 0}; + picture(dir, "setup"); + ui.sel = ROW_HOUSE, ui.setup.house = H_STACK2; + picture(dir, "setup_house"); + for (int pg = 0; pg < UNO_HELP_PAGES; pg++) { + char name[16]; + snprintf(name, sizeof name, "help%d", pg + 1); + ui.screen = V_HELP, ui.page = (uint8_t)pg, ui.first_run = 0; + picture(dir, name); + } +} + +/* a game, your cards played by a bot, for the GIF: every frame drawn at the table, for ms milliseconds */ +static const char *film_dir; +static uint32_t film_ms, film_k; +static double film_t0 = -1; +static void film_frame(void) { + if (ui.screen != V_PLAY) return; + if (film_t0 < 0) film_t0 = clock_ms; + char path[512]; + snprintf(path, sizeof path, "%s/%06u_%06u.raw", film_dir, film_k++, (unsigned)(clock_ms - film_t0)); + FILE *f = fopen(path, "wb"); + if (!f) exit(2); + fwrite(fb, 2, 320 * 240, f); + fclose(f); + if (clock_ms - film_t0 >= film_ms) quit_at = frame_no + 1; +} + +int main(int argc, char **argv) { + if (argc > 2 && !strcmp(argv[1], "test")) { + test(argv[2]); + if (fails) printf("%d FAILED\n", fails); + else printf("all passed\n"); + return fails ? 1 : 0; + } + if (argc > 2 && !strcmp(argv[1], "shots")) { + shots(argv[2]); + return 0; + } + if (argc > 4 && !strcmp(argv[1], "film")) { + /* a table already dealt (how to play read), from seed argv[4] */ + memset(files, 0, sizeof files); + uno_progress p; + uno_fresh(&p); + p.guide = 1, p.has_game = 1; + uno_new_game(&p.g, p.setup, (uint32_t)atoi(argv[4])); + save_write(&p); + film_dir = argv[2], film_ms = (uint32_t)atoi(argv[3]); + script = bot_keys, on_frame = film_frame, key_every = 24; + frame_no = 0, quit_at = 10000000; + uno_app_main(); + return 0; + } + printf("usage: play test DIR | shots DIR | film DIR MS SEED\n"); + return 2; +} diff --git a/tests/test_games.py b/tests/test_games.py index 8c397437..79ff3e9d 100644 --- a/tests/test_games.py +++ b/tests/test_games.py @@ -246,6 +246,10 @@ def every(t0, t1, step, key, d=80): "tombofthemask": ("TombOfTheMask.nwa", "tombofthemask.sav", presses((1500, "ok"), (2500, "ok"), (3200, "up"), (3800, "left"), (4400, "back")), 5500, presses((1500, "ok"), (2500, "up"), (3200, "back")), 4500), + # the title, New game, Deal, how to play (the first time: Back skips it), then the table: the bots play; Home + # keeps the game. Next time, from the copy, Continue: back at the table, quit with On/Off + "uno": ("UNO.nwa", "uno.sav", presses((1500, "ok"), (2200, "ok"), (2900, "back")), 6000, + presses((1500, "ok")), 4000), # a level, its message, then play; then back to the level select "portal": ("PortalReturns.nwa", "portal.sav", presses((1500, "ok"), (2500, "ok"), (3500, "ok")) + every(4000, 7000, 400, "right", 300), 8000, @@ -311,7 +315,7 @@ def monkeymart_save(): "cookieclicker": "cookieclicker_saves.py", "leveldevil": "leveldevil_saves.py", "jetpackjoyride": "jetpackjoyride_saves.py", "stickmanhook": "stickmanhook_saves.py", "donkeykong": "donkeykong_saves.py", "dune": "dune_saves.py", "monkeymart": "monkeymart_saves.py", - "tombofthemask": "tombofthemask_saves.py"} + "tombofthemask": "tombofthemask_saves.py", "uno": "uno_saves.py"} def dk_frames(v): @@ -456,6 +460,10 @@ def dk_frames(v): # progress at 268); after the reinstall, from the copy, twice "tombofthemask": (lambda v: len(v) == 296 and v[:4] == b"TOTM" and v[288] & 1 and struct.unpack_from("= 10, lambda v: v[4] == 0 and struct.unpack_from("= 1), # best kept, shake off # Air Hockey and the easy computer chosen; then the normal computer and Tanks, the tallies kept diff --git a/tools/release.py b/tools/release.py index f054f180..0b1c27c2 100644 --- a/tools/release.py +++ b/tools/release.py @@ -48,6 +48,7 @@ "dune": ("Dune", "roll down the dunes and fly over the line, with the shop's 26 balls and 7 landscapes"), "monkeymart": ("Monkey Mart", "run a grocery mart as a monkey, both marts: crops, animals, machines and workers"), "tombofthemask": ("Tomb of the Mask", "dash through the tomb: the endless arcade climb and all 200 stages, with the 16 masks"), + "uno": ("UNO", "the card game against one to three bots, with the official rules and scoring to 500"), } VARIANTS = [ ("NumPlay-Invisible.nwa", "The same app, hidden: a blank icon with no name"),