diff --git a/Makefile b/Makefile index c59933fd..dc8e38ce 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ APP_SPACE = 2031616 # Upsilon, the N0110 and N0115's custom software, gives .nwa apps less RAM: NumPlay-Upsilon.nwa has # the games that fit it (NumPlay draws their text there: see launcher/src/compat.c) UPSILON_RAM = 107674 -UPSILON_GAMES = crossyroad blockblast portal tetris chess flappybird pacman snake connectfour solitaire g2048 minesweeper \ +UPSILON_GAMES = crossyroad blockblast portal tetris dinorun chess flappybird pacman snake connectfour solitaire g2048 minesweeper \ breakout numvisuals # NumPlay and its discreet versions: the same app, with another name and icon diff --git a/README.md b/README.md index 789e9a23..67eeae81 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

NumPlay

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

@@ -47,16 +47,17 @@ All of these come in one app, **NumPlay.nwa**. Block Blast
Block Blast
Portal Returns
Portal Returns
Tetris
Tetris
- Chess
Chess
+ Chrome Dino
Chrome Dino
+ Chess
Chess
Flappy Bird
Flappy Bird
Pac-Man
Pac-Man
Snake
Snake
Connect Four
Connect Four
- Solitaire
Solitaire
+ Solitaire
Solitaire
2048
2048
Minesweeper
Minesweeper
Block Breaker
Block Breaker
@@ -107,7 +108,7 @@ You need your calculator, its USB cable, and a computer with **Google Chrome** o | --- | --- | --- | | N0120 | Epsilon 23.2 or newer (the calculator's own software) | Everything | | N0110, N0115 | Epsilon 23.2 or newer | Everything. These models are about 2.5 times slower, so the big games show fewer frames per second, but they keep their speed. | -| N0110, N0115 | Upsilon | NumPlay-Upsilon.nwa: 14 games (see Other versions) | +| N0110, N0115 | Upsilon | NumPlay-Upsilon.nwa: 15 games (see Other versions) | | Any | Custom software built from Epsilon 25.2's source | Everything. These builds can give apps less memory (133 KB): on one that gives even less, a game that needs more says how much on its card. | To see your calculator's model and software, open **Settings**, then **About**. To update the software, go to [my.numworks.com](https://my.numworks.com) with the calculator plugged in. @@ -123,7 +124,7 @@ To see your calculator's model and software, open **Settings**, then **About**. | [NumPlay-Matrices.nwa](https://github.com/Mason363/NumPlay/releases/latest/download/NumPlay-Matrices.nwa) | The same app, in disguise: it looks like a math app called **Matrices**. | | [NumPlay-French.nwa](https://github.com/Mason363/NumPlay/releases/latest/download/NumPlay-French.nwa) | NumPlay en français : tous les jeux, en français. | | [NumPlay-Chinese.nwa](https://github.com/Mason363/NumPlay/releases/latest/download/NumPlay-Chinese.nwa) | NumPlay 中文版:所有游戏,简体中文。 | -| [NumPlay-Upsilon.nwa](https://github.com/Mason363/NumPlay/releases/latest/download/NumPlay-Upsilon.nwa) | For calculators running **Upsilon**: the 14 games that fit in the memory it gives apps. | +| [NumPlay-Upsilon.nwa](https://github.com/Mason363/NumPlay/releases/latest/download/NumPlay-Upsilon.nwa) | For calculators running **Upsilon**: the 15 games that fit in the memory it gives apps. | The home screen with NumPlay-Invisible: the spot after Settings looks empty The home screen with NumPlay-Matrices among the calculator's own apps @@ -199,6 +200,7 @@ NumPlay is a fan project, made out of love for these games and for the NumWorks - **[Gabriele Cirulli](https://github.com/gabrielecirulli/2048)** made 2048. - **Block Blast!** by [Hungry Studio](https://www.hungrystudio.com) inspired NumPlay's Block Blast, written in C for NumPlay with no code or art from it. Its blocks, scoring and colours follow measurements of the app by the players behind [blockblast-Self-Improvement-loop](https://github.com/Kucze3205/blockblast-Self-Improvement-loop). - Snake, Solitaire and Block Breaker look and play like **Google's games** in Search. They are drawn from scratch; nothing is taken from Google. +- **Chrome's dinosaur game** by Google inspired Chrome Dino, which ports its rules, numbers and pictures from [Chromium's source](https://chromium.googlesource.com/chromium/src/+/main/components/neterror/resources/dino_game/) (BSD license). **[LilyTheCatgirl](https://github.com/LilyTheCuteCatgirl)** brought it to NumPlay. Not affiliated with Google. - **[gd3ds](https://github.com/AleFunky/gd3ds)** by AleFunky and friends, whose research into Geometry Dash made NumDash possible. Clubstep, Deadlocked and Dash also rely on [gdsolver](https://github.com/gdsolver/gdsolver)'s measurements of Geometry Dash 2.2, level data from [gmdkit](https://github.com/UHDanke/gmdkit) by HDanke and checks against [GDRWeb](https://github.com/iliasHDZ/GDRWeb) by IliasHDZ. - **Balatro** by LocalThunk inspired NumPlay's Balatro, which uses art adapted from the game and the **m6x11** font by Daniel Linssen. - **Minecraft** by Mojang inspired NumBlocks, which follows Minecraft 1.8.8 exactly: it makes its worlds the way Minecraft 1.8.8 does and uses its blocks, items, recipes, textures, font and screens. @@ -222,7 +224,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!), Mojang or Microsoft (Minecraft), Maddy Makes Games (Celeste), Team Cherry (Hollow Knight), Toby Fox (Undertale), 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, Doodle Champion Island Games), STUDIO4°C or Microsoft (Minesweeper). Tetris is a trademark of The Tetris Company, Minecraft of Mojang Synergies AB, Pac-Man of Bandai Namco and Connect Four of Hasbro. +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!), Mojang or Microsoft (Minecraft), Maddy Makes Games (Celeste), Team Cherry (Hollow Knight), Toby Fox (Undertale), 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). Tetris is a trademark of The Tetris Company, Chrome of Google LLC, Minecraft of Mojang Synergies AB, Pac-Man of Bandai Namco and Connect Four of Hasbro. ## Gallery @@ -244,30 +246,31 @@ NumPlay is licensed under the [GNU General Public License v3.0](LICENSE). Copyri Tetris on a real NumWorks calculator: pieces drop and a line clears
Tetris + Chrome Dino: jumping over cacti, then night falls and a pterodactyl flies by
Chrome Dino Chess: the opening moves of a game against a bot
Chess - Flappy Bird: flapping through the pipes
Flappy Bird + Flappy Bird: flapping through the pipes
Flappy Bird Pac-Man: eating dots in the maze
Pac-Man Snake: the snake eats apples and grows
Snake - Connect Four: winning a round against the computer
Connect Four + Connect Four: winning a round against the computer
Connect Four Solitaire: moving cards between the columns
Solitaire 2048: sliding and merging tiles up to 128
2048 - Minesweeper: opening the field, flagging the mines and winning
Minesweeper + Minesweeper: opening the field, flagging the mines and winning
Minesweeper Block Breaker: the ball breaks bricks
Block Breaker NumVisuals: moving backgrounds, from northern lights to a lava lamp
NumVisuals - NumPowder: water floods the sand, then lava pours in and boils it
NumPowder + NumPowder: water floods the sand, then lava pours in and boils it
NumPowder The Final Earth 2: building a house in a city on a floating island
The Final Earth 2 Hollow Knight: fighting the False Knight and Hornet
Hollow Knight - Celeste on a real NumWorks calculator: Madeline climbs the first level
Celeste + Celeste on a real NumWorks calculator: Madeline climbs the first level
Celeste Champion Island: climbing, artistic swimming and the marathon
Champion Island Undertale: Flowey's friendliness pellets, Toriel's tile puzzle and her fire
Undertale diff --git a/_config.yml b/_config.yml index 837d6a04..27bd231f 100644 --- a/_config.yml +++ b/_config.yml @@ -1,4 +1,4 @@ title: NumPlay -description: "18 games for the NumWorks calculator in one app: NumDash, Crossy Road, NumBlocks, NumDrive, Balatro, Buckshot Roulette, Block Blast, Portal Returns, Tetris, Chess, Flappy Bird, Pac-Man, Snake and more, plus NumVisuals." +description: "19 games for the NumWorks calculator in one app: NumDash, Crossy Road, NumBlocks, NumDrive, Balatro, Buckshot Roulette, Block Blast, Portal Returns, Tetris, Chrome Dino, Chess, Flappy Bird, Pac-Man, Snake and more, plus NumVisuals." theme: jekyll-theme-cayman exclude: [games, launcher, tools, tests, build, LICENSES, Makefile, .github] diff --git a/docs/media/gallery/dinorun.gif b/docs/media/gallery/dinorun.gif new file mode 100644 index 00000000..1e93b1d8 Binary files /dev/null and b/docs/media/gallery/dinorun.gif differ diff --git a/docs/media/launcher.gif b/docs/media/launcher.gif index fc3512c5..0833adca 100644 Binary files a/docs/media/launcher.gif and b/docs/media/launcher.gif differ diff --git a/docs/media/thumb_dinorun.png b/docs/media/thumb_dinorun.png new file mode 100644 index 00000000..f27d4e87 Binary files /dev/null and b/docs/media/thumb_dinorun.png differ diff --git a/docs/play.md b/docs/play.md index 56d6adfc..fe008a94 100644 --- a/docs/play.md +++ b/docs/play.md @@ -305,6 +305,21 @@ Name the fallen human, then walk and talk your way through the Ruins, Snowdin, W
+## Chrome Dino + +Chrome Dino: the T-rex jumps over a low pterodactyl + +| Key | What it does | +| --- | --- | +| **OK** or **Up** (or 8) | Jump: hold it to jump higher | +| **Down** (or 2) | Duck, or in the air, come down faster | +| **EXE** | After a crash, play again at once | +| **Back** | Pause menu (with Quit game) | + +Chrome's game when you're offline, as Chrome plays it. Run as far as you can: jump the cacti and the low pterodactyls, duck under the ones flying at head height. You go faster and faster, and every 700 points night falls for a while. Your best score is kept. + +
+ ## Chess Chess against a bot diff --git a/games/dinorun/.gitignore b/games/dinorun/.gitignore new file mode 100644 index 00000000..ea1472ec --- /dev/null +++ b/games/dinorun/.gitignore @@ -0,0 +1 @@ +output/ diff --git a/games/dinorun/LICENSE b/games/dinorun/LICENSE new file mode 100644 index 00000000..6e8fe91d --- /dev/null +++ b/games/dinorun/LICENSE @@ -0,0 +1,32 @@ +Chrome Dino ports the rules, numbers and pictures of Chrome's dinosaur game, the T-Rex Runner of +Chromium (https://chromium.googlesource.com/chromium/src/+/main/components/neterror/resources/dino_game/): +its game code and its sprite sheet, components/neterror/resources/images/default_100_percent/offline/ +100-offline-sprite.png, which src/sprites.h is made from. They come under Chromium's license: + +Copyright 2015 The Chromium Authors + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google LLC nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/games/dinorun/Makefile b/games/dinorun/Makefile new file mode 100644 index 00000000..bec00c40 --- /dev/null +++ b/games/dinorun/Makefile @@ -0,0 +1,49 @@ +# Chrome Dino: output/dinorun.nwa on its own, output/module.o for NumPlay (see ../../tools/npmodule.py). +# +# make build | check | host-test | sprites SHEET=100-offline-sprite.png (Chromium's, see tools/sprites.py) +Q ?= @ +CC = arm-none-eabi-gcc +NWLINK ?= npx --yes -- nwlink@1.0.0 +OUT = output +SRC = src/main.c +HEADERS = ../common/epsilon_app.h ../common/epsilon_files.h ../common/np_lz.h ../common/np_text.h src/sprites.h +FLAGS = -std=c11 $(shell $(NWLINK) eadk-cflags-device) -Os -Wall -Wextra -Wno-unused-parameter \ + -fno-math-errno -fno-tree-loop-distribute-patterns -flto -fno-fat-lto-objects \ + -fwhole-program -fvisibility=internal +LINK = -Wl,--relocatable -nostartfiles --specs=nano.specs -Wl,-e,main \ + -Wl,-u,eadk_app_name -Wl,-u,eadk_app_icon -Wl,-u,eadk_api_level \ + -Wl,--gc-sections -flinker-output=nolto-rel + +.PHONY: build check clean module sim-module host-test sprites +build: $(OUT)/dinorun.nwa +$(OUT): + $(Q) mkdir -p $(OUT) +$(OUT)/icon.o: src/icon.png | $(OUT) + $(Q) $(NWLINK) png-icon-o $< $@ +$(OUT)/dinorun.nwa: $(SRC) $(HEADERS) $(OUT)/icon.o + $(Q) $(CC) $(FLAGS) $(LINK) $(SRC) $(OUT)/icon.o -o $@ + $(Q) arm-none-eabi-strip --strip-unneeded $@ +check: $(OUT)/dinorun.nwa + $(Q) $(NWLINK) nwa-bin $< $(OUT)/dinorun.bin + @echo "Chrome Dino installs as $$(wc -c < $(OUT)/dinorun.bin) bytes" +clean: + $(Q) rm -rf $(OUT) + +module: $(OUT)/module.o +$(OUT)/module.o: $(SRC) $(HEADERS) | $(OUT) + $(Q) $(CC) $(FLAGS) -Wl,--relocatable -nostartfiles -nostdlib -Wl,-e,main \ + -flinker-output=nolto-rel $(SRC) -o $@ +sim-module: $(OUT)/sim-module.o +$(OUT)/sim-module.o: $(SRC) $(HEADERS) | $(OUT) + $(Q) mkdir -p $(OUT)/sim + $(Q) cc -std=c11 -O2 -fPIC $(shell $(NWLINK) eadk-cflags-simulator) \ + -Dmain=np_dinorun_main -c $(SRC) -o $(OUT)/sim/main.o + $(Q) ld -r $(OUT)/sim/main.o -exported_symbol _np_dinorun_main -o $@ + +host-test: | $(OUT) + cc -std=c11 -O1 -g -Wall -Wextra -Wno-unused-parameter -fsanitize=address,undefined -fno-omit-frame-pointer \ + -DHOST -DPLATFORM_DEVICE=0 -Itests tests/test_dino.c -lm -o $(OUT)/host-test + $(OUT)/host-test + +sprites: + python3 tools/sprites.py $(SHEET) diff --git a/games/dinorun/README.md b/games/dinorun/README.md new file mode 100644 index 00000000..5f71e339 --- /dev/null +++ b/games/dinorun/README.md @@ -0,0 +1,35 @@ +# Chrome Dino + +Chrome's dinosaur game, the one you get when you're offline, on the NumWorks calculator. It plays and looks the way Chrome plays it on a screen as wide as the calculator's. + +Chrome Dino: the T-rex jumps over a low pterodactyl + +- **Chrome's own game**: its rules, numbers and pictures come from Chrome's source, run at Chrome's 60 frames a second. +- **Jump the cacti**, small and large, alone or in groups of up to three. Hold the jump key for a higher jump, or let go early for a short one. +- **Pterodactyls** from a fast enough speed on, at three heights: jump the low ones, duck under the ones flying at head height, and let the high ones fly over you. +- **Faster and faster**, up to Chrome's top speed. The score flashes at every 100 points. +- **Night** falls every 700 points: the screen fades to dark, with the moon in its phases and the stars, then day comes back. +- **Game over** with Chrome's crash, its GAME OVER and its restart button. Your best score is saved and shown as HI. + +The T-rex waiting to start, with the keys under it Chrome Dino at night, under a crescent moon GAME OVER, with the high score + +## Controls + +| Key | Action | +| --- | --- | +| OK or Up (or 8) | Start, jump (hold it to jump higher) | +| Down (or 2) | Duck; in the air, come down faster | +| OK, after a crash | Play again | +| EXE, after a crash | Play again at once | +| Back | Pause: Resume, Restart, Quit game | +| Home | Quit | + +## Credits + +Chrome's dinosaur game is **Google**'s (2014). This version ports it from [Chromium's source](https://chromium.googlesource.com/chromium/src/+/main/components/neterror/resources/dino_game/), its code and its pictures, under Chromium's BSD license (see [LICENSE](LICENSE)). Not affiliated with Google. Chrome is a trademark of Google LLC. + +**[LilyTheCatgirl](https://github.com/LilyTheCuteCatgirl)** brought the game to NumPlay, after her version for the calculator's Python in [Lily's Small NumWorks Games](https://github.com/LilyTheCuteCatgirl/lilys-small-numworks-games). Mason Chen made it follow Chrome's own code. + +It comes inside [NumPlay](https://github.com/Mason363/NumPlay), or on its own as `ChromeDino.nwa` from the [latest release](https://github.com/Mason363/NumPlay/releases/latest). + +Build: `make` (needs `arm-none-eabi-gcc` and Node.js for nwlink). Tests: `make host-test`. The pictures: `make sprites SHEET=100-offline-sprite.png`, from Chromium's sprite sheet (see `tools/sprites.py`). diff --git a/games/dinorun/docs/gameover.png b/games/dinorun/docs/gameover.png new file mode 100644 index 00000000..0e74c028 Binary files /dev/null and b/games/dinorun/docs/gameover.png differ diff --git a/games/dinorun/docs/night.png b/games/dinorun/docs/night.png new file mode 100644 index 00000000..f3b5a409 Binary files /dev/null and b/games/dinorun/docs/night.png differ diff --git a/games/dinorun/docs/shot.png b/games/dinorun/docs/shot.png new file mode 100644 index 00000000..5fb3e1dc Binary files /dev/null and b/games/dinorun/docs/shot.png differ diff --git a/games/dinorun/docs/start.png b/games/dinorun/docs/start.png new file mode 100644 index 00000000..5135048f Binary files /dev/null and b/games/dinorun/docs/start.png differ diff --git a/games/dinorun/lang/fr.txt b/games/dinorun/lang/fr.txt new file mode 100644 index 00000000..8a3bcedc --- /dev/null +++ b/games/dinorun/lang/fr.txt @@ -0,0 +1,15 @@ +# Chrome Dino in French: the English text as in the source, a tab, the translation (see tools/lang.py). +Paused Pause +Resume Reprendre +Restart Recommencer +Quit game Quitter +Quit game? Quitter ? +Yes Oui +No Non +Up/Down: choose OK: confirm Back: resume Haut/Bas : choisir OK : valider Retour : reprendre +Left/Right: choose OK: confirm Gauche/Droite : choisir OK : valider +OK: play again Back: quit OK : rejouer Retour : quitter +OK: start Back: quit OK : commencer Retour : quitter +OK or Up: jump Down: duck Back: pause OK ou Haut : sauter Bas : se baisser Retour : pause +Chrome's dinosaur game, not affiliated with Google Le jeu du dinosaure de Chrome, sans lien avec Google +Made by LilyTheCatgirl and Mason Chen for NumPlay Fait par LilyTheCatgirl et Mason Chen pour NumPlay diff --git a/games/dinorun/lang/zh.txt b/games/dinorun/lang/zh.txt new file mode 100644 index 00000000..a47d63ce --- /dev/null +++ b/games/dinorun/lang/zh.txt @@ -0,0 +1,15 @@ +# Chrome Dino in Chinese: the English text as in the source, a tab, the translation (see tools/lang.py). +Paused 暂停 +Resume 继续 +Restart 重新开始 +Quit game 退出游戏 +Quit game? 退出游戏? +Yes 是 +No 否 +Up/Down: choose OK: confirm Back: resume 上/下:选择 OK:确定 返回:继续 +Left/Right: choose OK: confirm 左/右:选择 OK:确定 +OK: play again Back: quit OK:再来一局 返回:退出 +OK: start Back: quit OK:开始 返回:退出 +OK or Up: jump Down: duck Back: pause OK或上:跳 下:蹲下 返回:暂停 +Chrome's dinosaur game, not affiliated with Google Chrome 的小恐龙游戏,与 Google 无关 +Made by LilyTheCatgirl and Mason Chen for NumPlay 由 LilyTheCatgirl 和 Mason Chen 为 NumPlay 制作 diff --git a/games/dinorun/src/icon.png b/games/dinorun/src/icon.png new file mode 100644 index 00000000..3ff7feea Binary files /dev/null and b/games/dinorun/src/icon.png differ diff --git a/games/dinorun/src/main.c b/games/dinorun/src/main.c new file mode 100644 index 00000000..6326e09b --- /dev/null +++ b/games/dinorun/src/main.c @@ -0,0 +1,995 @@ +/* Chrome Dino for NumWorks and NumPlay: Chrome's dinosaur game, the T-Rex Runner of + * Chromium's components/neterror/resources/dino_game (BSD license, see ../LICENSE), as + * Chrome plays it on a screen 320 pixels wide. Its rules, numbers and pictures are + * Chrome's, run at Chrome's 60 frames a second (with catch-up where drawing takes + * longer); the names below are the ones of Chrome's classes (Runner, Trex, Horizon, + * Obstacle, Cloud, NightMode, DistanceMeter, GameOverPanel). + * + * The screen is drawn in bands of 16 rows, in grey levels, and only the 32 x 16 + * tiles that changed since the last frame are sent to the display. + * + * First brought to NumPlay by LilyTheCatgirl. */ +#include +#include +#include +#include "../../common/epsilon_app.h" +#include "../../common/epsilon_files.h" +#include "../../common/np_lz.h" +#include "../../common/np_text.h" +#include "sprites.h" + +#ifdef __ELF__ +const char eadk_app_name[] __attribute__((section(".rodata.eadk_app_name"))) = "Chrome Dino"; +const uint32_t eadk_api_level __attribute__((section(".rodata.eadk_api_level"))) = 0; +#endif + +/* ---------------------------------------------------------------- the screen */ +#define W 320 +#define H 240 +#define BAND 16 +#define TILE_W 32 +#define TILES_X (W / TILE_W) +#define BANDS (H / BAND) +#define CANVAS_Y 40 /* Chrome's canvas, 320 x 150, then the keys under it */ +#define CANVAS_H 150 +#define HINT_Y 197 +#define CREDIT_Y 213 + +/* ---------------------------------------------------------------- Chrome's numbers */ +#define FPS 60 +#define TICK_MS (1000.0 / FPS) +#define MAX_CATCHUP 8 /* ticks a frame at most, after a slow one */ +#define WIDTH 320 /* Runner.dimensions */ +#define DEFAULT_WIDTH 600 /* DEFAULT_DIMENSIONS.width */ +/* Runner's defaultBaseConfig and normalModeConfig */ +#define ACCELERATION 0.001 +#define BOTTOM_PAD 10 +#define CLEAR_TIME 3000 +#define CLOUD_FREQUENCY 0.5 +#define GAMEOVER_CLEAR_TIME 1200 +#define GAP_COEFFICIENT 0.6 +#define INVERT_DISTANCE 700 +#define INVERT_FADE_DURATION 12000 +#define MAX_BLINK_COUNT 3 +#define MAX_CLOUDS 6 +#define MAX_OBSTACLE_LENGTH 3 +#define MAX_OBSTACLE_DUPLICATION 2 +#define MAX_SPEED 13 +#define MOBILE_SPEED_COEFFICIENT 1.2 +#define SPEED 6 +#define MAX_GAP_COEFFICIENT 1.5 +#define INTRO_MS 400 /* the canvas's CSS intro: .4s ease-out */ +#define INVERT_MS 1500 /* the page's CSS filter transition */ +/* Trex: defaultTrexConfig and normalJumpConfig */ +#define TREX_W 44 +#define TREX_H 47 +#define TREX_W_DUCK 59 +#define DROP_VELOCITY -5 +#define GRAVITY 0.6 +#define MAX_JUMP_HEIGHT 30 +#define MIN_JUMP_HEIGHT 30 +#define INITIAL_JUMP_VELOCITY -10 +#define SPEED_DROP_COEFFICIENT 3 +#define START_X_POS 50 +#define INTRO_DURATION 1500 +#define BLINK_TIMING 7000 +#define GROUND_Y (CANVAS_H - TREX_H - BOTTOM_PAD) +/* the keyboard's repeat, as on a computer: a held key presses again after 500 ms, 30 times a second */ +#define REPEAT_DELAY 500 +#define REPEAT_MS 33 + +#define KEY(k) (1ull << (k)) +#define JUMP_KEYS (KEY(eadk_key_ok) | KEY(eadk_key_up) | KEY(eadk_key_eight)) +#define DUCK_KEYS (KEY(eadk_key_down) | KEY(eadk_key_two)) +#define RESTART_KEYS KEY(eadk_key_exe) +#define BACK_KEYS KEY(eadk_key_back) +#define QUIT_KEYS (KEY(eadk_key_home) | KEY(eadk_key_on_off)) +#define SAVE_NAME "dinorun.sav" + +typedef struct { int16_t x, y, w, h; } box; /* CollisionBox */ + +/* ---------------------------------------------------------------- random numbers */ +static uint32_t rng = 0xA319F4B5u; +static double rnd(void) { /* Math.random() */ + rng ^= rng << 13; + rng ^= rng >> 17; + rng ^= rng << 5; + return (rng >> 8) * (1.0 / 16777216.0); +} +static int random_num(int lo, int hi) { return (int)(rnd() * (hi - lo + 1)) + lo; } /* getRandomNum */ +static int ifloor(double v) { + int i = (int)v; + return v < i ? i - 1 : i; +} +static int iceil(double v) { + int i = (int)v; + return v > i ? i + 1 : i; +} +static int iround(double v) { return ifloor(v + 0.5); } /* Math.round */ +/* a CSS cubic-bezier(x1, y1, x2, y2) at t (0..1) */ +static float bezier(float t, float x1, float y1, float x2, float y2) { + float lo = 0, hi = 1; + for (int k = 0; k < 20; k++) { + float u = (lo + hi) / 2, x = 3 * u * (1 - u) * (1 - u) * x1 + 3 * u * u * (1 - u) * x2 + u * u * u; + if (x < t) lo = u; + else hi = u; + } + float u = (lo + hi) / 2; + return 3 * u * (1 - u) * (1 - u) * y1 + 3 * u * u * (1 - u) * y2 + u * u * u; +} + +/* ---------------------------------------------------------------- the obstacles' types */ +typedef struct { + uint8_t region; + int16_t width, height; + int16_t ypos[3]; + uint8_t nypos; + float multiple_speed, min_speed, speed_offset; + int16_t min_gap; + uint8_t frames; /* numFrames, at frame_ms */ + float frame_ms; + uint8_t nboxes; + box boxes[5]; +} obstacle_type; +enum { CACTUS_SMALL, CACTUS_LARGE, PTERODACTYL, TYPES }; +static const obstacle_type TYPE[TYPES] = { + {R_CACTUS_SMALL, 17, 35, {105}, 1, 4, 0, 0, 120, 0, 0, 3, {{0, 7, 5, 27}, {4, 0, 6, 34}, {10, 4, 7, 14}}}, + {R_CACTUS_LARGE, 25, 50, {90}, 1, 7, 0, 0, 120, 0, 0, 3, {{0, 12, 7, 38}, {8, 0, 7, 49}, {13, 10, 10, 38}}}, + {R_PTERODACTYL, 46, 40, {100, 75, 50}, 3, 999, 8.5f, 0.8f, 150, 2, 1000.f / 6, 5, + {{15, 15, 16, 5}, {18, 21, 24, 6}, {2, 14, 4, 3}, {6, 10, 4, 7}, {10, 8, 6, 9}}}, +}; +static const box TREX_BOXES_DUCKING[1] = {{1, 18, 55, 25}}; +static const box TREX_BOXES_RUNNING[6] = {{22, 0, 17, 16}, {1, 18, 30, 9}, {10, 35, 14, 8}, + {1, 24, 29, 5}, {5, 30, 21, 4}, {9, 34, 15, 4}}; + +/* ---------------------------------------------------------------- the game's state */ +typedef struct { + const obstacle_type *type; + int size, x, y, width, gap, frame; + float speed_offset, timer; + bool following_created, remove; + box boxes[5]; +} obstacle; +#define MAX_OBSTACLES 8 + +enum { WAITING, RUNNING, CRASHED, JUMPING, DUCKING }; /* Trex's Status */ +static struct { + int x, y, x_initial, status, frame, jump_count, blink_count, shown; + float timer, ms_per_frame, blink_delay, anim_time; + double jump_velocity; + bool jumping, ducking, speed_drop, reached_min_height, playing_intro; +} trex; + +static struct { + bool playing, activated, crashed, playing_intro, inverted, invert_trigger, achievement; + double distance_ran, current_speed; + float running_time, invert_timer, intro_time, crash_time; + uint32_t highest_score; + /* Horizon */ + obstacle obstacles[MAX_OBSTACLES]; + int obstacle_count; + int history[MAX_OBSTACLE_DUPLICATION], history_count; + struct { int x, y, gap; } clouds[MAX_CLOUDS]; + int cloud_count; + int line_x[2], line_src[2]; /* HorizonLine: the two 600 wide stretches */ + /* NightMode */ + float moon_x, opacity; + struct { float x; int y, sy; } stars[2]; + int phase; + bool draw_stars; + /* DistanceMeter */ + int max_units, digit_count; + uint32_t max_score; + char digits[8]; + float flash_timer; + int flash_iterations; + bool painted; + /* GameOverPanel */ + float panel_timer; + int panel_frame; + /* the page's colours inverted (night): from, to, ms into the change */ + float inv_from, inv_to, inv_t; +} g; + +/* the calculator's own: the pause menu, and leaving */ +enum { MENU_NONE, MENU_PAUSE, MENU_QUIT }; +static int menu, menu_sel; +static bool leave; + +/* ---------------------------------------------------------------- saves */ +/* "DINO", version 2, 3 bytes of nothing, the highest score Chrome keeps (the distance run, rounded up) */ +static void save_best(void) { + uint8_t d[12] = {'D', 'I', 'N', 'O', 2}; + uint32_t v = g.highest_score; + for (int i = 0; i < 4; i++) d[8 + i] = (uint8_t)(v >> (8 * i)); + ef_write(SAVE_NAME, d, sizeof d); +} +static void load_best(void) { + uint32_t len = 0; + const uint8_t *s = ef_read(SAVE_NAME, &len); + if (!s || len < 8 || s[0] != 'D' || s[1] != 'I' || s[2] != 'N' || s[3] != 'O') return; + if (len == 12 && s[4] == 2) { + uint32_t v = (uint32_t)s[8] | (uint32_t)s[9] << 8 | (uint32_t)s[10] << 16 | (uint32_t)s[11] << 24; + if (v < 40000000u) g.highest_score = v; + } else if (len == 8) { + /* the first version kept the score as shown: the distance is 40 times it */ + uint32_t v = (uint32_t)s[4] | (uint32_t)s[5] << 8 | (uint32_t)s[6] << 16 | (uint32_t)s[7] << 24; + if (v < 1000000u) g.highest_score = v * 40; + } +} + +/* ---------------------------------------------------------------- Trex */ +static const int16_t FRAMES_WAITING[2] = {44, 0}, FRAMES_RUNNING[2] = {88, 132}, FRAMES_DUCKING[2] = {264, 323}; +static const int16_t *trex_frames(int *n) { + switch (trex.status) { + case WAITING: *n = 2; return FRAMES_WAITING; + case RUNNING: *n = 2; return FRAMES_RUNNING; + case DUCKING: *n = 2; return FRAMES_DUCKING; + default: *n = 1; return 0; /* CRASHED 220, JUMPING 0 */ + } +} +static int trex_frame_x(void) { + int n; + const int16_t *f = trex_frames(&n); + return f ? f[trex.frame] : trex.status == CRASHED ? 220 : 0; +} +static void trex_set_status(int status) { + trex.status = status; + trex.frame = 0; + trex.ms_per_frame = status == WAITING ? 1000.f / 3 : status == RUNNING ? 1000.f / 12 : status == DUCKING ? 1000.f / 8 + : 1000.f / 60; + if (status == WAITING) trex.anim_time = 0, trex.blink_delay = (float)iceil(rnd() * BLINK_TIMING); +} +static void trex_set_duck(bool on); +/* Trex.update(deltaTime, status) */ +static void trex_update(float dt, int status) { + trex.timer += dt; + if (status >= 0) trex_set_status(status); + if (trex.playing_intro && trex.x < START_X_POS) { + trex.x += iround(((double)START_X_POS / INTRO_DURATION) * dt); + trex.x_initial = trex.x; + } + if (trex.status == WAITING) { + /* blink(): drawn again once the delay is over, and the delay starts again after the blink */ + trex.anim_time += dt; + if (trex.anim_time >= trex.blink_delay) { + trex.shown = trex_frame_x(); + if (trex.frame == 1) { + trex.blink_delay = (float)iceil(rnd() * BLINK_TIMING); + trex.anim_time = 0; + trex.blink_count++; + } + } + } else { + trex.shown = trex_frame_x(); + } + int n; + trex_frames(&n); + if (trex.timer >= trex.ms_per_frame) { + trex.frame = trex.frame == n - 1 ? 0 : trex.frame + 1; + trex.timer = 0; + } + if (trex.speed_drop && trex.y == GROUND_Y) { + trex.speed_drop = false; + trex_set_duck(true); + } +} +static void trex_reset(void) { + trex.x = trex.x_initial; + trex.y = GROUND_Y; + trex.jump_velocity = 0; + trex.jumping = false; + trex.ducking = false; + trex_update(0, RUNNING); + trex.speed_drop = false; + trex.jump_count = 0; +} +static void trex_start_jump(double speed) { + if (!trex.jumping) { + trex_update(0, JUMPING); + trex.jump_velocity = INITIAL_JUMP_VELOCITY - speed / 10; + trex.jumping = true; + trex.reached_min_height = false; + trex.speed_drop = false; + } +} +static void trex_end_jump(void) { + if (trex.reached_min_height && trex.jump_velocity < DROP_VELOCITY) trex.jump_velocity = DROP_VELOCITY; +} +static void trex_update_jump(void) { + /* (one frame of Chrome's each tick: framesElapsed is 1) */ + if (trex.speed_drop) trex.y += iround(trex.jump_velocity * SPEED_DROP_COEFFICIENT); + else trex.y += iround(trex.jump_velocity); + trex.jump_velocity += GRAVITY; + if (trex.y < GROUND_Y - MIN_JUMP_HEIGHT || trex.speed_drop) trex.reached_min_height = true; + if (trex.y < MAX_JUMP_HEIGHT || trex.speed_drop) trex_end_jump(); + if (trex.y > GROUND_Y) { + trex_reset(); + trex.jump_count++; + } +} +static void trex_set_duck(bool on) { + if (on && trex.status != DUCKING) { + trex_update(0, DUCKING); + trex.ducking = true; + } else if (trex.status == DUCKING) { + trex_update(0, RUNNING); + trex.ducking = false; + } +} + +/* ---------------------------------------------------------------- Horizon: obstacles */ +static void obstacle_init(obstacle *o, const obstacle_type *t, double speed) { + o->type = t; + o->size = random_num(1, MAX_OBSTACLE_LENGTH); + o->x = WIDTH + t->width; + o->frame = 0; + o->timer = 0; + o->following_created = false; + o->remove = false; + for (int i = 0; i < t->nboxes; i++) o->boxes[i] = t->boxes[i]; + if (o->size > 1 && t->multiple_speed > speed) o->size = 1; + o->width = t->width * o->size; + o->y = t->nypos > 1 ? t->ypos[random_num(0, t->nypos - 1)] : t->ypos[0]; + if (o->size > 1) { + o->boxes[1].w = (int16_t)(o->width - o->boxes[0].w - o->boxes[2].w); + o->boxes[2].x = (int16_t)(o->width - o->boxes[2].w); + } + o->speed_offset = t->speed_offset ? (rnd() > 0.5 ? t->speed_offset : -t->speed_offset) : 0; + /* getGap */ + int min_gap = iround(o->width * speed + t->min_gap * GAP_COEFFICIENT); + int max_gap = iround(min_gap * MAX_GAP_COEFFICIENT); + o->gap = random_num(min_gap, max_gap); +} +static bool duplicate_obstacle(int type) { + int dup = 0; + for (int i = 0; i < g.history_count; i++) dup = g.history[i] == type ? dup + 1 : 0; + return dup >= MAX_OBSTACLE_DUPLICATION; +} +static void add_new_obstacle(double speed) { + for (;;) { /* (Chrome calls itself again until a type fits) */ + int type = random_num(0, TYPES - 1); + if (duplicate_obstacle(type) || speed < TYPE[type].min_speed) continue; + if (g.obstacle_count < MAX_OBSTACLES) obstacle_init(&g.obstacles[g.obstacle_count++], &TYPE[type], speed); + for (int i = MAX_OBSTACLE_DUPLICATION - 1; i > 0; i--) g.history[i] = g.history[i - 1]; + g.history[0] = type; + if (g.history_count < MAX_OBSTACLE_DUPLICATION) g.history_count++; + return; + } +} +static void update_obstacles(float dt, double speed) { + int removed = 0; + for (int i = 0; i < g.obstacle_count; i++) { + obstacle *o = &g.obstacles[i]; + if (!o->remove) { + double s = speed + o->speed_offset; + o->x -= ifloor(s * FPS / 1000 * dt); + if (o->type->frames) { + o->timer += dt; + if (o->timer >= o->type->frame_ms) { + o->frame = o->frame == o->type->frames - 1 ? 0 : o->frame + 1; + o->timer = 0; + } + } + if (o->x + o->width <= 0) o->remove = true; + } + if (o->remove) removed++; /* (Chrome takes as many off the front, the first being the oldest) */ + } + for (int i = removed; i < g.obstacle_count; i++) g.obstacles[i - removed] = g.obstacles[i]; + g.obstacle_count -= removed; + if (g.obstacle_count > 0) { + obstacle *last = &g.obstacles[g.obstacle_count - 1]; + if (!last->following_created && last->x + last->width > 0 && last->x + last->width + last->gap < WIDTH) { + add_new_obstacle(speed); + last->following_created = true; + } + } else { + add_new_obstacle(speed); + } +} + +/* ---------------------------------------------------------------- Horizon: clouds, ground, night */ +static void add_cloud(void) { + if (g.cloud_count >= MAX_CLOUDS) return; + g.clouds[g.cloud_count].x = WIDTH; + g.clouds[g.cloud_count].gap = random_num(100, 400); + g.clouds[g.cloud_count].y = random_num(30, 71); + g.cloud_count++; +} +static void update_clouds(float dt, double speed) { + double el_speed = 0.2 / 1000 * dt * speed; + int n = g.cloud_count; + if (!n) { + add_cloud(); + return; + } + for (int i = n - 1; i >= 0; i--) g.clouds[i].x -= iceil(el_speed); + if (n < MAX_CLOUDS && WIDTH - g.clouds[n - 1].x > g.clouds[n - 1].gap && CLOUD_FREQUENCY > rnd()) add_cloud(); + int kept = 0; /* (then the ones gone off the left) */ + for (int i = 0; i < g.cloud_count; i++) + if (g.clouds[i].x + 46 > 0) g.clouds[kept++] = g.clouds[i]; + g.cloud_count = kept; +} +static void update_lines(float dt, double speed) { + int inc = ifloor(speed * FPS / 1000 * dt); + int a = g.line_x[0] <= 0 ? 0 : 1, b = !a; + g.line_x[a] -= inc; + g.line_x[b] = g.line_x[a] + 600; + if (g.line_x[a] <= -600) { + g.line_x[a] += 1200; + g.line_x[b] = g.line_x[a] - 600; + g.line_src[a] = rnd() > 0.5 ? 600 : 0; + } +} +/* NightMode: its container is the canvas's height, 150, as Chrome gives it */ +#define NIGHT_W 150 +static void place_stars(void) { + int seg = NIGHT_W / 2; + for (int i = 0; i < 2; i++) { + g.stars[i].x = (float)random_num(seg * i, seg * (i + 1)); + g.stars[i].y = random_num(0, 70); + g.stars[i].sy = 9 * i; + } +} +static float night_x(float x, float speed) { return x < -20 ? NIGHT_W : x - speed; } +static void update_night(bool activated) { + if (activated && g.opacity == 0) { + g.phase++; + if (g.phase >= 7) g.phase = 0; + } + if (activated && (g.opacity < 1 || g.opacity == 0)) g.opacity += 0.035f; + else if (g.opacity > 0) g.opacity -= 0.035f; + if (g.opacity > 0) { + g.moon_x = night_x(g.moon_x, 0.25f); + if (g.draw_stars) + for (int i = 0; i < 2; i++) g.stars[i].x = night_x(g.stars[i].x, 0.3f); + } else { + g.opacity = 0; + place_stars(); + } + g.draw_stars = true; +} +static void reset_night(void) { + g.phase = 0; + g.opacity = 0; + update_night(false); +} +static void horizon_update(float dt, double speed, bool update_obstacles_too, bool night) { + update_lines(dt, speed); + update_night(night); + update_clouds(dt, speed); + if (update_obstacles_too) update_obstacles(dt, speed); +} +static void horizon_reset(void) { + g.obstacle_count = 0; + g.line_x[0] = 0, g.line_x[1] = 600; + reset_night(); +} + +/* ---------------------------------------------------------------- DistanceMeter */ +#define ACHIEVEMENT_DISTANCE 100 +#define FLASH_DURATION 250 +#define FLASH_ITERATIONS 3 +static uint32_t actual_distance(uint32_t d) { return d ? (uint32_t)iround(d * 0.025) : 0; } +static void meter_update(float dt, uint32_t distance) { + g.painted = true; + if (!g.achievement) { + distance = actual_distance(distance); + if (distance > g.max_score && g.max_units == 5) g.max_units = 6, g.max_score = 999999; + if (distance > 0 && distance % ACHIEVEMENT_DISTANCE == 0) g.achievement = true, g.flash_timer = 0; + for (int i = g.max_units - 1; i >= 0; i--, distance /= 10) g.digits[i] = (char)('0' + distance % 10); + g.digit_count = g.max_units; + } else if (g.flash_iterations <= FLASH_ITERATIONS) { + g.flash_timer += dt; + if (g.flash_timer < FLASH_DURATION) g.painted = false; + else if (g.flash_timer > FLASH_DURATION * 2) g.flash_timer = 0, g.flash_iterations++; + } else { + g.achievement = false, g.flash_iterations = 0, g.flash_timer = 0; + } +} +static void meter_reset(void) { + g.achievement = false; + meter_update(0, 0); +} + +/* ---------------------------------------------------------------- Runner */ +static void set_speed(double s) { /* setSpeed(): slower on a screen narrower than Chrome's */ + double mobile = s * WIDTH / DEFAULT_WIDTH * MOBILE_SPEED_COEFFICIENT; + g.current_speed = mobile > s ? s : mobile; +} +/* how far the page is inverted (0 day, 1 night): its CSS filter's transition, 1.5s cubic-bezier(0.65, 0.05, 0.36, 1) */ +static float night_e(void) { + float t = g.inv_t >= INVERT_MS ? 1 : g.inv_t / INVERT_MS; + return g.inv_from + (g.inv_to - g.inv_from) * bezier(t, 0.65f, 0.05f, 0.36f, 1); +} +static void invert(bool reset) { + bool to = reset ? false : g.invert_trigger; + if (reset) g.invert_timer = 0; + if (to != g.inverted) { + /* the CSS transition goes on from where the colours are */ + g.inv_from = night_e(); + g.inv_to = to ? 1 : 0; + g.inv_t = 0; + } + g.inverted = to; +} +static void runner_init(void) { + g.current_speed = SPEED; + set_speed(g.current_speed); + g.max_units = 5; + g.max_score = 99999; + meter_reset(); + g.line_x[0] = 0, g.line_x[1] = 600; + g.line_src[0] = 0, g.line_src[1] = 600; + g.cloud_count = 0; + add_cloud(); + reset_night(); + g.inv_t = INVERT_MS; + trex.y = GROUND_Y; + trex.shown = 0; + trex_update(0, WAITING); +} +static bool check_collision(const obstacle *o) { + box t = {(int16_t)(trex.x + 1), (int16_t)(trex.y + 1), TREX_W - 2, TREX_H - 2}; + box b = {(int16_t)(o->x + 1), (int16_t)(o->y + 1), (int16_t)(o->type->width * o->size - 2), (int16_t)(o->type->height - 2)}; + if (!(t.x < b.x + b.w && t.x + t.w > b.x && t.y < b.y + b.h && t.h + t.y > b.y)) return false; + const box *tb = trex.ducking ? TREX_BOXES_DUCKING : TREX_BOXES_RUNNING; + int tn = trex.ducking ? 1 : 6; + for (int i = 0; i < tn; i++) + for (int j = 0; j < o->type->nboxes; j++) { + box p = {(int16_t)(tb[i].x + t.x), (int16_t)(tb[i].y + t.y), tb[i].w, tb[i].h}; + box q = {(int16_t)(o->boxes[j].x + b.x), (int16_t)(o->boxes[j].y + b.y), o->boxes[j].w, o->boxes[j].h}; + if (p.x < q.x + q.w && p.x + p.w > q.x && p.y < q.y + q.h && p.h + p.y > q.y) return true; + } + return false; +} +static void game_over(void) { + g.playing = false; + g.crashed = true; + g.achievement = false; + trex_update(100, CRASHED); + if (trex.ducking) trex.x++; /* (Trex.draw moves a crashed ducking T-rex on by a pixel) */ + g.panel_timer = 0, g.panel_frame = 0; + if (g.distance_ran > g.highest_score) { + g.highest_score = (uint32_t)iceil(g.distance_ran); + save_best(); + } + g.crash_time = 0; +} +static void restart(void) { + g.running_time = 0; + g.playing = true; + g.crashed = false; + g.distance_ran = 0; + set_speed(SPEED); + meter_reset(); + horizon_reset(); + trex_reset(); + invert(true); + g.panel_frame = 0, g.panel_timer = 0; +} +static void play_intro(void) { + if (!g.activated && !g.crashed) { + g.playing_intro = true; + trex.playing_intro = true; + g.intro_time = 0; + g.playing = true; + g.activated = true; + } else if (g.crashed) { + restart(); + } +} +/* Runner.update(), one frame of Chrome's */ +static void tick(void) { + const float dt = (float)TICK_MS; + if (g.playing) { + if (trex.jumping) trex_update_jump(); + g.running_time += dt; + bool has_obstacles = g.running_time > CLEAR_TIME; + if (trex.jump_count == 1 && !g.playing_intro) play_intro(); + if (g.playing_intro) horizon_update(0, g.current_speed, has_obstacles, false); + else if (!g.crashed) horizon_update(g.activated ? dt : 0, g.current_speed, has_obstacles, g.inverted); + bool collision = has_obstacles && g.obstacle_count && check_collision(&g.obstacles[0]); + if (!collision) { + g.distance_ran += g.current_speed * dt / TICK_MS; + if (g.current_speed < MAX_SPEED) g.current_speed += ACCELERATION; + } else { + game_over(); + } + meter_update(dt, (uint32_t)iceil(g.distance_ran)); + /* night: every 700, for 12 seconds */ + if (g.invert_timer > INVERT_FADE_DURATION) { + g.invert_timer = 0; + g.invert_trigger = false; + invert(false); + } else if (g.invert_timer) { + g.invert_timer += dt; + } else { + uint32_t actual = actual_distance((uint32_t)iceil(g.distance_ran)); + if (actual > 0) { + g.invert_trigger = !(actual % INVERT_DISTANCE); + if (g.invert_trigger && g.invert_timer == 0) { + g.invert_timer += dt; + invert(false); + } + } + } + /* the canvas widens once (CSS intro), then the game starts */ + if (g.playing_intro) { + g.intro_time += dt; + if (g.intro_time >= INTRO_MS) { + g.running_time = 0; + g.playing_intro = false; + trex.playing_intro = false; + } + } + } + if (g.playing || (!g.activated && trex.blink_count < MAX_BLINK_COUNT)) trex_update(dt, -1); + if (g.crashed) { + g.crash_time += dt; + /* GameOverPanel: the logo for 875 ms, then the restart button's 7 other frames over 875 */ + g.panel_timer += dt; + if (g.panel_frame == 0 && g.panel_timer > 875) g.panel_timer = 0, g.panel_frame = 1; + else if (g.panel_frame > 0 && g.panel_frame < 8 && g.panel_timer >= 875.f / 8) g.panel_frame++, g.panel_timer = 0; + } + if (g.inv_t < INVERT_MS) g.inv_t += dt; +} + +/* ---------------------------------------------------------------- keys (Runner.onKeyDown and onKeyUp) */ +static void key_down_jump(void) { + if (g.crashed || menu) return; + if (!g.playing && !g.activated) g.playing = true; /* the first press starts the game */ + if (!g.playing) return; + if (!trex.jumping && !trex.ducking) trex_start_jump(g.current_speed); +} +static void key_down_duck(void) { + if (!g.playing || g.crashed || menu) return; + if (trex.jumping) trex.speed_drop = true, trex.jump_velocity = 1; /* setSpeedDrop */ + else if (!trex.ducking) trex_set_duck(true); +} +static void key_up_jump(void) { + if (g.playing) trex_end_jump(); + else if (g.crashed && g.crash_time >= GAMEOVER_CLEAR_TIME) restart(); +} +static void key_up_duck(void) { + trex.speed_drop = false; + trex_set_duck(false); +} + +/* ---------------------------------------------------------------- drawing: grey levels in a band */ +static uint8_t band[W * BAND] __attribute__((aligned(4))); +static int band_y; +static uint8_t sheet[SHEET_BYTES]; +static uint8_t tone[4]; /* the sheet's tones on screen this frame (0 unused) */ +static uint8_t bg, ink, soft; +static int clip_x0, clip_x1, clip_y0, clip_y1; /* what may be drawn on (the canvas, or the screen) */ + +static uint8_t inv(float e, int v) { return (uint8_t)(v + (255 - 2 * v) * e + 0.5f); } +static void clip_screen(void) { clip_x0 = 0, clip_x1 = W, clip_y0 = 0, clip_y1 = H; } +static void fill(int x, int y, int w, int h, uint8_t c) { + int x0 = x < clip_x0 ? clip_x0 : x, x1 = x + w > clip_x1 ? clip_x1 : x + w; + int y0 = y < clip_y0 ? clip_y0 : y, y1 = y + h > clip_y1 ? clip_y1 : y + h; + if (y0 < band_y) y0 = band_y; + if (y1 > band_y + BAND) y1 = band_y + BAND; + for (int r = y0; r < y1; r++) + for (int c2 = x0; c2 < x1; c2++) band[(r - band_y) * W + c2] = c; +} +/* a w x h part of a region at (sx, sy) of it, drawn at (dx, dy), at alpha a (0..256) */ +static void blit(int region, int sx, int sy, int w, int h, int dx, int dy, int a) { + if (a <= 0) return; + const dino_region *rg = ®IONS[region]; + int x0 = dx < clip_x0 ? clip_x0 : dx, x1 = dx + w > clip_x1 ? clip_x1 : dx + w; + int y0 = dy < clip_y0 ? clip_y0 : dy, y1 = dy + h > clip_y1 ? clip_y1 : dy + h; + if (y0 < band_y) y0 = band_y; + if (y1 > band_y + BAND) y1 = band_y + BAND; + for (int y = y0; y < y1; y++) { + const uint8_t *src = sheet + rg->off + (sy + y - dy) * rg->stride; + uint8_t *d = band + (y - band_y) * W; + for (int x = x0; x < x1; x++) { + int u = sx + x - dx, t = src[u >> 2] >> (2 * (u & 3)) & 3; + if (!t) continue; + if (a >= 256) d[x] = tone[t]; + else d[x] = (uint8_t)(d[x] + ((tone[t] - d[x]) * a >> 8)); + } + } +} +static bool band_meets(int y, int h) { return y < band_y + BAND && y + h > band_y; } + +/* ---- text: capitals 5 x 7 (lowercase drawn as capitals), columns of 7 bits, top bit first */ +static const uint8_t FONT[59][5] = { + {0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x5F,0x00,0x00},{0x00,0x07,0x00,0x07,0x00},{0x14,0x7F,0x14,0x7F,0x14}, + {0x24,0x2A,0x7F,0x2A,0x12},{0x23,0x13,0x08,0x64,0x62},{0x36,0x49,0x55,0x22,0x50},{0x00,0x05,0x03,0x00,0x00}, + {0x00,0x1C,0x22,0x41,0x00},{0x00,0x41,0x22,0x1C,0x00},{0x14,0x08,0x3E,0x08,0x14},{0x08,0x08,0x3E,0x08,0x08}, + {0x00,0x50,0x30,0x00,0x00},{0x08,0x08,0x08,0x08,0x08},{0x00,0x60,0x60,0x00,0x00},{0x20,0x10,0x08,0x04,0x02}, + {0x3E,0x51,0x49,0x45,0x3E},{0x00,0x42,0x7F,0x40,0x00},{0x42,0x61,0x51,0x49,0x46},{0x21,0x41,0x45,0x4B,0x31}, + {0x18,0x14,0x12,0x7F,0x10},{0x27,0x45,0x45,0x45,0x39},{0x3C,0x4A,0x49,0x49,0x30},{0x01,0x71,0x09,0x05,0x03}, + {0x36,0x49,0x49,0x49,0x36},{0x06,0x49,0x49,0x29,0x1E},{0x00,0x36,0x36,0x00,0x00},{0x00,0x56,0x36,0x00,0x00}, + {0x08,0x14,0x22,0x41,0x00},{0x14,0x14,0x14,0x14,0x14},{0x00,0x41,0x22,0x14,0x08},{0x02,0x01,0x51,0x09,0x06}, + {0x32,0x49,0x79,0x41,0x3E},{0x7E,0x11,0x11,0x11,0x7E},{0x7F,0x49,0x49,0x49,0x36},{0x3E,0x41,0x41,0x41,0x22}, + {0x7F,0x41,0x41,0x22,0x1C},{0x7F,0x49,0x49,0x49,0x41},{0x7F,0x09,0x09,0x09,0x01},{0x3E,0x41,0x49,0x49,0x7A}, + {0x7F,0x08,0x08,0x08,0x7F},{0x00,0x41,0x7F,0x41,0x00},{0x20,0x40,0x41,0x3F,0x01},{0x7F,0x08,0x14,0x22,0x41}, + {0x7F,0x40,0x40,0x40,0x40},{0x7F,0x02,0x0C,0x02,0x7F},{0x7F,0x04,0x08,0x10,0x7F},{0x3E,0x41,0x41,0x41,0x3E}, + {0x7F,0x09,0x09,0x09,0x06},{0x3E,0x41,0x51,0x21,0x5E},{0x7F,0x09,0x19,0x29,0x46},{0x46,0x49,0x49,0x49,0x31}, + {0x01,0x01,0x7F,0x01,0x01},{0x3F,0x40,0x40,0x40,0x3F},{0x1F,0x20,0x40,0x20,0x1F},{0x3F,0x40,0x38,0x40,0x3F}, + {0x63,0x14,0x08,0x14,0x63},{0x07,0x08,0x70,0x08,0x07},{0x61,0x51,0x49,0x45,0x43}, +}; +static int glyph_of(char ch) { + if (ch >= 'a' && ch <= 'z') ch = (char)(ch - 32); + return ch < 32 || ch > 'Z' ? '?' - 32 : ch - 32; +} +static uint8_t pen; +static void glyph(char ch, int x, int y, int k) { + const uint8_t *gl = FONT[glyph_of(ch)]; + for (int i = 0; i < 5; i++) + for (int j = 0, b = gl[i]; b; j++, b >>= 1) + if (b & 1) fill(x + i * k, y + j * k, k, k, pen); +} +#if NP_TEXT_EXTRA +/* Other languages: an accented letter is the font's capital with np_accent() over it (a cedilla + under it); a Chinese one comes from the 12-pixel font, 12 pixels tall up to size 2, then 24. */ +static void pen_px(int x, int y, void *ctx) { fill(x, y, 1, 1, pen); } +static int xk(int k) { return (7 * k + 6) / 12; } +static int letter(const char **s, int x, int y, int k, bool draw) { + uint32_t cp = np_utf8(s); + char b = (char)cp, b2 = 0; + int acc = 0; + if (cp >= 0x80 && (acc = np_latin(cp, &b, &b2), !b)) { + if (draw) np_xdraw(cp, x, y + (7 * k - 12 * xk(k)) / 2, xk(k), pen_px, 0); + return np_xadvance(cp, xk(k)); + } + if (draw) { + glyph(b, x, y, k); + np_accent(acc, x, acc == NP_ACC_CEDIL ? y + 7 * k : y - 2 * k - 1, k, pen_px, 0); + if (b2) glyph(b2, x + 6 * k, y, k); + } + return (b2 ? 12 : 6) * k; +} +static int tw(const char *s, int k) { + int w = -k; + while (*s) w += letter(&s, 0, 0, k, false); + return w; +} +static void text(const char *s, int x, int y, int k, uint8_t c) { + if (!band_meets(y - 3 * k - 3, 11 * k + 6)) return; + pen = c; + while (*s) x += letter(&s, x, y, k, true); +} +#else +static int tw(const char *s, int k) { + int n = 0; + while (s[n]) n++; + return n * 6 * k - k; +} +static void text(const char *s, int x, int y, int k, uint8_t c) { + if (!band_meets(y, 7 * k)) return; + pen = c; + for (; *s; s++, x += 6 * k) glyph(*s, x, y, k); +} +#endif +static void ctext(const char *s, int cx, int y, int k, uint8_t c) { text(s, cx - tw(s, k) / 2, y, k, c); } + +/* ---------------------------------------------------------------- a frame */ +static int container_w(void) { /* the canvas's container, 44 wide until the intro widens it (ease-out) */ + if (!g.activated) return TREX_W; + if (!g.playing_intro) return WIDTH; + float t = g.intro_time / INTRO_MS; + return TREX_W + (int)((WIDTH - TREX_W) * bezier(t > 1 ? 1 : t, 0, 0, 0.58f, 1)); /* ease-out */ +} +static void draw_canvas(int cw) { + clip_x0 = 0, clip_x1 = cw, clip_y0 = CANVAS_Y, clip_y1 = CANVAS_Y + CANVAS_H; + if (!band_meets(CANVAS_Y, CANVAS_H)) return; + const int Y = CANVAS_Y; + /* HorizonLine */ + for (int k = 0; k < 2; k++) blit(R_HORIZON, g.line_src[k], 0, 600, 12, g.line_x[k], Y + 127, 256); + /* NightMode */ + if (g.opacity > 0) { + int a = g.opacity >= 1 ? 256 : (int)(g.opacity * 256); + static const uint8_t PHASES[7] = {140, 120, 100, 60, 40, 20, 0}; + if (g.draw_stars) + for (int i = 0; i < 2; i++) blit(R_STAR, 0, g.stars[i].sy, 9, 9, iround(g.stars[i].x), Y + g.stars[i].y, a); + blit(R_MOON, PHASES[g.phase], 0, g.phase == 3 ? 40 : 20, 40, iround(g.moon_x), Y + 30, a); + } + /* Cloud */ + for (int i = 0; i < g.cloud_count; i++) blit(R_CLOUD, 0, 0, 46, 14, g.clouds[i].x, Y + g.clouds[i].y, 256); + /* Obstacle */ + for (int i = 0; i < g.obstacle_count; i++) { + const obstacle *o = &g.obstacles[i]; + int w = o->type->width * o->size, sx = (int)(o->type->width * o->size * (0.5 * (o->size - 1))); + sx += o->type->width * o->frame; + blit(o->type->region, sx, 0, w, o->type->height, o->x, Y + o->y, 256); + } + /* DistanceMeter (from the first frame on, as Chrome draws it when it starts) */ + int mx = WIDTH - 11 * (5 + 1); + if (g.painted) + for (int i = 0; i < g.digit_count; i++) blit(R_TEXT, 10 * (g.digits[i] - '0'), 0, 10, 13, mx + i * 11, Y + 5, 256); + if (g.highest_score) { + uint32_t v = actual_distance(g.highest_score); + int hx = mx - g.max_units * 2 * 10; + blit(R_TEXT, 100, 0, 10, 13, hx, Y + 5, 204); /* H */ + blit(R_TEXT, 110, 0, 10, 13, hx + 11, Y + 5, 204); /* I */ + for (int i = g.max_units - 1; i >= 0; i--, v /= 10) + blit(R_TEXT, 10 * (int)(v % 10), 0, 10, 13, hx + (3 + i) * 11, Y + 5, 204); + } + /* Trex */ + if (trex.status == CRASHED) blit(R_TREX_CRASH, 0, 0, TREX_W, TREX_H, trex.x, Y + trex.y, 256); + else if (trex.ducking) blit(R_TREX_DUCK, trex.shown - 264, 0, TREX_W_DUCK, TREX_H, trex.x, Y + trex.y, 256); + else blit(R_TREX, trex.shown, 0, TREX_W, TREX_H, trex.x, Y + trex.y, 256); + /* GameOverPanel: its frames drawn over one another, as Chrome does */ + if (g.crashed) { + blit(R_TEXT, 0, 13, 191, 11, iround(WIDTH / 2.0 - 191 / 2.0), Y + iround((CANVAS_H - 25) / 3.0), 256); + for (int f = 0; f <= g.panel_frame && f < 8; f++) blit(R_RESTART, 36 * f, 0, 36, 32, WIDTH / 2 - 16, Y + CANVAS_H / 2, 256); + } +} +static void button(int x, int y, int w, const char *s, bool sel) { + fill(x, y, w, 15, sel ? ink : bg); + if (!sel) fill(x, y, w, 1, soft), fill(x, y + 14, w, 1, soft), fill(x, y, 1, 15, soft), fill(x + w - 1, y, 1, 15, soft); + ctext(s, x + w / 2, y + 4, 1, sel ? bg : ink); +} +static void draw_menu(void) { + clip_screen(); + const int x = 70, y = CANVAS_Y + 20, w = 180, h = 108; + if (!band_meets(y, h)) return; + fill(x, y, w, h, bg); + fill(x, y, w, 2, ink), fill(x, y + h - 2, w, 2, ink), fill(x, y, 2, h, ink), fill(x + w - 2, y, 2, h, ink); + if (menu == MENU_PAUSE) { + ctext(T("Paused"), 160, y + 12, 2, ink); + static const char *const ITEMS[3] = {T("Resume"), T("Restart"), T("Quit game")}; + for (int i = 0; i < 3; i++) button(x + 30, y + 40 + i * 21, w - 60, ITEMS[i], menu_sel == i); + } else { + ctext(T("Quit game?"), 160, y + 26, 2, ink); + button(x + 24, y + 62, 56, T("Yes"), menu_sel == 1); + button(x + w - 80, y + 62, 56, T("No"), menu_sel == 0); + } +} +static void draw_hints(void) { + clip_screen(); + const char *h = menu == MENU_PAUSE ? T("Up/Down: choose OK: confirm Back: resume") + : menu ? T("Left/Right: choose OK: confirm") + : g.crashed ? T("OK: play again Back: quit") + : !g.activated && !g.playing ? T("OK: start Back: quit") + : T("OK or Up: jump Down: duck Back: pause"); + ctext(h, 160, HINT_Y, 1, soft); + if (!g.activated && !g.playing) { + ctext(T("Chrome's dinosaur game, not affiliated with Google"), 160, CREDIT_Y, 1, soft); + ctext(T("Made by LilyTheCatgirl and Mason Chen for NumPlay"), 160, CREDIT_Y + 12, 1, soft); + } +} + +static uint32_t hashes[BANDS][TILES_X]; +static bool hashes_valid; +static uint16_t upload[W * BAND]; +static uint16_t rgb(uint8_t v) { return (uint16_t)((v >> 3) << 11 | (v >> 2) << 5 | (v >> 3)); } +static uint32_t tile_hash(int x) { + uint32_t h = 2166136261u; + for (int y = 0; y < BAND; y++) { + const uint32_t *p = (const uint32_t *)(const void *)(band + y * W + x); + for (int i = 0; i < TILE_W / 4; i++) h = (h ^ p[i]) * 16777619u; + } + return h; +} +static void send_band(int b) { + int run = -1; + for (int c = 0; c <= TILES_X; c++) { + bool changed = false; + if (c < TILES_X) { + uint32_t h = tile_hash(c * TILE_W); + changed = !hashes_valid || hashes[b][c] != h; + hashes[b][c] = h; + } + if (changed && run < 0) run = c; + if (!changed && run >= 0) { + int x = run * TILE_W, w = (c - run) * TILE_W; + for (int y = 0; y < BAND; y++) + for (int i = 0; i < w; i++) upload[y * w + i] = rgb(band[y * W + x + i]); + eadk_display_push_rect((eadk_rect_t){(uint16_t)x, (uint16_t)(b * BAND), (uint16_t)w, BAND}, upload); + run = -1; + } + } +} +static void frame(void) { + float e = night_e(); + bg = inv(e, 255); + tone[1] = inv(e, 83), tone[2] = inv(e, 218), tone[3] = inv(e, 255); + ink = tone[1]; + soft = inv(e, 140); + int cw = container_w(); + for (int b = 0; b < BANDS; b++) { + band_y = b * BAND; + for (int i = 0; i < W * BAND; i++) band[i] = bg; + draw_canvas(cw); + if (menu) draw_menu(); + draw_hints(); + send_band(b); + } + hashes_valid = true; +} + +/* ---------------------------------------------------------------- keys and the loop */ +typedef struct { bool held; float held_ms, repeat_at; } key_state; +static key_state jump_key, duck_key; +/* a key's presses this frame, the keyboard's repeat included */ +static int presses(key_state *k, bool down, float dt) { + int n = 0; + if (down && !k->held) n = 1, k->held_ms = 0, k->repeat_at = REPEAT_DELAY; + else if (down) { + k->held_ms += dt; + while (k->held_ms >= k->repeat_at) n++, k->repeat_at += REPEAT_MS; + } + return n; +} +static void menu_input(uint64_t hit) { + if (menu == MENU_PAUSE) { + if (hit & BACK_KEYS) menu = MENU_NONE; + else if (hit & KEY(eadk_key_up)) menu_sel = (menu_sel + 2) % 3; + else if (hit & KEY(eadk_key_down)) menu_sel = (menu_sel + 1) % 3; + else if (hit & (KEY(eadk_key_ok) | KEY(eadk_key_exe))) { + if (menu_sel == 0) menu = MENU_NONE; + else if (menu_sel == 1) menu = MENU_NONE, restart(); + else menu = MENU_QUIT, menu_sel = 0; + } + } else { + if (hit & BACK_KEYS) menu = MENU_PAUSE, menu_sel = 2; + else if (hit & (KEY(eadk_key_left) | KEY(eadk_key_right))) menu_sel = !menu_sel; + else if (hit & (KEY(eadk_key_ok) | KEY(eadk_key_exe))) { + if (menu_sel) leave = true; + else menu = MENU_PAUSE, menu_sel = 2; + } + } +} +/* the keys of a frame: dt ms since the last */ +static void input(uint64_t keys, uint64_t hit, uint64_t released, float dt) { + if (keys & QUIT_KEYS) { + leave = true; + return; + } + if (menu) { + menu_input(hit); + jump_key.held = (keys & JUMP_KEYS) != 0, duck_key.held = (keys & DUCK_KEYS) != 0; + return; + } + if (hit & BACK_KEYS) { + if (g.playing && !g.crashed) menu = MENU_PAUSE, menu_sel = 0; + else leave = true; + return; + } + bool j = (keys & JUMP_KEYS) != 0, d = (keys & DUCK_KEYS) != 0; + for (int n = presses(&jump_key, j, dt); n > 0; n--) key_down_jump(); + for (int n = presses(&duck_key, d, dt); n > 0; n--) key_down_duck(); + if (!j && jump_key.held) key_up_jump(); + if (!d && duck_key.held) key_up_duck(); + jump_key.held = j, duck_key.held = d; + if (g.crashed && (released & RESTART_KEYS)) restart(); +} + +int main(void) { + np_app_begin(); + np_lz_unpack(SHEET_LZ, sizeof SHEET_LZ, sheet, sizeof sheet); + rng ^= (uint32_t)eadk_timing_millis() * 2654435761u; + if (!rng) rng = 1; + load_best(); + runner_init(); + uint64_t held = eadk_keyboard_scan(); + uint32_t last = (uint32_t)eadk_timing_millis(); + double owed = 0; + while (!leave) { + uint32_t now = (uint32_t)eadk_timing_millis(); + float dt = (float)(now - last); + last = now; + uint64_t keys = eadk_keyboard_scan(), hit = keys & ~held, released = held & ~keys; + held = keys; + input(keys, hit, released, dt); + if (leave) break; + if (!menu) { + owed += dt; + int n = 0; + while (owed >= TICK_MS && n < MAX_CATCHUP) tick(), owed -= TICK_MS, n++; + if (owed >= TICK_MS) owed = 0; /* (a long stall: the game waits instead of rushing) */ + } else { + owed = 0; + } + frame(); + uint32_t spent = (uint32_t)eadk_timing_millis() - now; + if (spent < 16) eadk_timing_msleep(16 - spent); + } + return np_app_end(); +} diff --git a/games/dinorun/src/sprites.h b/games/dinorun/src/sprites.h new file mode 100644 index 00000000..26e9d87b --- /dev/null +++ b/games/dinorun/src/sprites.h @@ -0,0 +1,244 @@ +/* Generated by tools/sprites.py from Chromium's 100-offline-sprite.png (BSD license, see ../LICENSE). */ +#ifndef DINO_SPRITES_H +#define DINO_SPRITES_H +#include + +/* a region of the sheet: 2 bits a pixel (0 clear, 1 ink, 2 light grey, 3 white), the lowest first */ +typedef struct { uint16_t off, w, stride; uint8_t h; } dino_region; +enum { R_TREX, R_TREX_CRASH, R_TREX_DUCK, R_CACTUS_SMALL, R_CACTUS_LARGE, R_PTERODACTYL, R_CLOUD, R_HORIZON, R_MOON, R_STAR, R_TEXT, R_RESTART, R_COUNT }; +#define SHEET_BYTES 16603 +static const dino_region REGIONS[R_COUNT] = { + {0, 176, 44, 47}, {2068, 44, 11, 47}, {2585, 118, 30, 47}, {3995, 102, 26, 35}, {4905, 150, 38, 50}, {6805, 92, 23, 40}, {7725, 46, 12, 14}, {7893, 1200, 300, 12}, {11493, 160, 40, 40}, {13093, 9, 3, 18}, {13147, 191, 48, 24}, {14299, 288, 72, 32}, +}; +static const uint8_t SHEET_LZ[5490] = { + 45,0,0,16,240,255,0,0,15,8,0,10,240,96,10,240,9,0,10,240,10,240,0,16, + 255,85,0,0,128,10,240,9,0,10,240,10,240,0,16,10,240,0,16,95,198,9,0,85, + 245,8,0,10,240,10,240,245,85,24,10,96,9,0,10,80,245,85,10,240,10,160,9,0, + 6,10,80,245,85,10,240,32,160,9,0,10,240,10,240,0,0,16,10,240,0,16,10,240, + 0,16,10,240,0,16,10,240,0,0,16,10,240,0,16,10,240,0,16,10,240,0,16,10, + 240,0,0,16,10,240,0,16,10,176,239,1,8,0,10,240,10,240,248,10,240,0,0,10, + 224,240,15,0,0,240,68,10,0,97,0,240,10,240,10,240,10,240,240,10,240,190,10,0, + 95,15,0,192,255,8,0,255,193,255,10,240,10,240,10,240,10,240,10,96,255,87,128,10, + 16,192,0,10,240,10,240,10,240,10,240,10,32,245,3,255,95,184,0,185,0,10,240,10, + 240,10,240,10,240,108,0,0,10,16,85,255,8,0,85,245,10,240,72,0,0,10,240,9, + 0,85,10,240,9,0,85,10,224,0,0,32,186,32,7,0,0,0,10,240,0,16,10,240, + 0,16,8,10,240,0,16,10,144,0,10,240,10,240,7,0,0,0,8,10,240,0,16,10, + 192,240,7,0,0,0,10,240,0,16,4,10,240,0,16,15,10,240,0,0,10,192,99,64, + 10,240,128,0,16,10,240,0,16,10,240,0,16,10,208,99,32,245,128,10,240,0,0,10, + 240,9,0,10,240,10,240,9,0,0,2,99,32,255,7,0,0,0,10,240,0,16,10,240, + 0,16,56,10,240,0,16,10,128,255,85,245,10,240,10,16,7,95,213,63,8,0,10,16, + 1,0,7,0,0,0,33,255,32,160,10,128,43,240,187,0,15,39,3,0,32,206,10,176, + 255,255,3,8,0,10,0,95,245,112,21,128,10,240,10,16,43,240,0,240,245,127,19,8, + 0,32,10,176,0,96,255,65,112,32,160,10,128,0,96,0,43,128,120,0,24,64,10,176, + 5,0,0,80,21,128,10,240,240,10,16,5,0,0,80,21,96,255,15,255,255,196,7,0, + 0,0,240,10,160,5,0,0,80,255,255,44,4,0,0,0,240,255,32,144,255,10,112,5, + 0,134,0,80,255,255,4,0,0,240,0,128,219,1,85,208,6,0,0,16,9,0,10,64, + 80,9,0,85,5,108,8,0,10,0,245,95,10,96,117,93,10,240,0,32,160,53,0,10, + 240,10,240,0,16,10,240,0,16,10,16,236,9,48,10,64,80,0,0,0,85,85,5,86, + 5,0,80,0,0,0,85,10,96,84,10,240,181,5,52,32,85,11,0,80,5,10,112,85, + 13,0,8,0,85,5,10,208,0,32,9,0,10,240,66,10,208,0,7,0,0,0,10,128, + 8,0,0,33,64,74,8,0,0,10,144,0,8,0,42,48,0,10,144,225,80,10,0,6, + 0,0,16,10,128,0,85,5,92,10,240,10,0,0,80,0,0,64,85,10,112,121,5,10, + 240,0,0,85,0,80,5,6,0,2,0,16,85,10,112,0,240,0,240,0,240,0,240,0, + 240,0,0,240,0,240,0,240,0,240,0,240,0,240,0,240,0,240,0,0,240,0,240,0, + 240,0,240,0,240,0,240,0,240,0,240,0,0,240,0,240,0,240,0,240,0,240,0,240, + 0,240,0,240,192,0,64,21,164,232,8,41,53,0,80,29,96,252,255,226,0,0,195,29, + 96,12,0,65,4,95,255,15,237,240,7,0,255,63,29,48,215,255,3,230,8,32,15,255, + 0,16,29,96,255,127,85,111,85,85,213,255,29,80,255,95,14,0,191,245,15,95,85,255, + 255,8,0,85,123,253,127,4,0,213,255,87,213,38,32,7,85,255,95,4,0,29,128,32, + 9,5,16,29,80,158,3,0,85,85,15,95,4,0,0,48,125,128,3,0,35,48,0,32, + 24,16,29,240,0,80,24,16,15,25,255,4,0,0,112,253,127,4,0,0,112,29,240,206, + 0,144,15,240,95,5,0,0,96,61,252,36,90,112,0,0,15,29,240,0,128,15,212,6, + 0,128,7,61,192,127,5,0,0,96,29,240,5,0,0,96,171,15,0,90,112,213,109,1, + 0,90,112,245,71,255,255,15,29,240,9,0,0,32,245,29,16,118,90,64,213,127,4,0, + 253,3,0,90,64,139,245,95,4,0,255,201,17,7,0,29,240,95,244,4,0,29,16,192, + 8,16,85,213,195,255,174,88,16,0,0,240,90,32,245,214,17,15,220,14,0,29,16,245, + 127,253,29,144,253,95,125,255,29,96,215,127,85,253,127,84,0,58,0,48,240,85,0,213, + 95,255,9,0,0,32,45,252,29,0,61,124,240,71,0,134,0,29,16,243,245,15,10,0, + 0,16,252,87,125,213,93,3,29,144,15,240,255,29,112,192,29,16,193,252,140,98,49,7, + 115,16,9,0,0,32,192,255,99,127,61,175,2,29,96,54,16,255,255,9,0,96,0,32, + 29,32,0,112,29,32,0,144,124,213,56,80,0,0,0,89,0,5,0,0,128,29,240,0, + 144,116,112,0,0,161,240,4,33,0,128,29,240,0,96,192,12,16,63,106,3,0,252,3, + 0,240,38,0,192,15,8,0,173,63,3,0,112,53,12,0,213,8,0,87,214,21,0,92, + 13,16,128,92,4,0,112,85,26,178,1,85,21,0,87,53,16,112,8,96,25,80,245,48, + 16,0,192,16,16,0,92,213,48,255,0,112,85,195,0,207,85,13,109,3,25,0,12,220, + 25,0,112,115,16,0,250,25,0,220,25,0,115,195,245,85,205,253,13,25,0,55,87,95, + 213,0,92,239,125,85,115,3,25,0,87,3,112,255,85,95,125,213,85,125,53,0,183,87, + 245,213,25,64,95,13,25,240,95,171,213,48,25,32,48,25,0,195,25,80,12,114,25,48, + 220,25,32,112,0,87,115,115,25,64,13,55,25,48,87,95,25,16,8,80,25,16,22,0, + 72,2,0,16,64,25,240,87,16,64,25,240,87,16,64,134,25,112,85,85,25,240,8,32, + 25,240,8,80,205,118,25,240,95,85,48,32,13,87,85,25,16,161,85,22,0,2,0,19, + 0,25,16,213,216,0,85,223,3,252,85,125,245,25,0,53,87,150,25,0,127,85,25,48, + 53,25,0,121,1,85,209,85,25,16,16,0,20,1,85,68,16,95,213,85,15,25,0,63, + 25,16,205,103,0,3,16,0,91,87,3,52,0,3,87,46,0,92,7,0,16,25,16,42, + 0,84,1,25,16,112,21,32,168,1,25,48,35,253,0,102,0,16,16,25,16,63,21,0, + 98,32,128,25,16,145,1,25,0,16,0,25,16,16,0,168,1,0,73,127,8,16,25,0, + 240,7,2,38,0,85,25,32,145,0,246,1,39,0,25,48,0,25,0,56,0,213,32,16, + 32,25,240,82,48,25,240,16,16,15,16,32,8,0,0,25,224,16,48,8,96,25,240,8, + 80,25,240,8,80,25,240,64,8,80,25,240,8,80,25,240,8,80,25,240,240,94,3,51, + 192,255,72,3,150,19,252,15,16,128,9,32,0,11,80,30,32,32,48,0,80,24,64,73, + 16,75,64,94,48,88,96,48,0,80,24,48,0,87,6,32,92,31,32,41,112,75,1,43, + 16,213,5,16,15,4,0,24,80,0,37,240,37,64,237,19,0,0,24,48,37,240,37,64, + 137,48,196,24,64,37,144,63,37,240,24,48,37,144,192,213,6,37,224,192,15,24,16,0, + 0,37,112,28,4,37,160,133,192,130,0,53,24,16,0,0,37,240,37,80,112,0,63,20, + 24,16,0,0,37,240,37,80,215,4,4,0,24,0,116,0,0,37,0,63,19,0,85,13, + 60,37,32,57,240,186,17,37,240,87,85,195,20,0,12,0,35,215,115,37,16,26,0,37, + 0,192,108,32,37,64,85,252,71,5,87,27,0,195,202,0,205,7,0,46,37,0,87,53, + 252,37,0,112,32,32,4,32,38,139,3,3,92,212,3,29,0,53,37,128,166,3,83,213, + 92,98,3,37,80,205,39,0,53,37,16,9,95,12,32,37,48,245,9,0,37,240,37,240, + 37,240,132,37,240,37,64,60,32,0,37,240,12,0,37,160,215,195,92,213,123,37,24,16, + 37,240,37,64,245,85,113,95,105,4,37,240,37,240,85,3,3,37,240,58,7,0,127,37, + 160,213,192,13,118,0,14,0,90,37,192,85,37,144,213,63,17,18,85,14,0,34,37,160, + 195,18,0,37,144,55,34,85,14,0,37,160,148,172,1,37,208,112,37,240,3,57,0,37, + 144,192,199,112,53,204,37,240,228,4,37,160,112,115,122,107,6,85,14,0,13,87,245,87, + 217,0,254,105,8,85,253,85,3,192,127,85,125,245,37,128,92,125,245,213,252,227,48,192, + 82,0,20,0,4,0,122,18,76,0,20,0,253,85,67,213,95,37,80,120,2,62,0,37, + 32,53,37,32,33,53,37,48,233,16,43,16,37,96,85,4,32,137,1,88,29,16,143,1, + 37,16,213,192,56,32,195,37,128,169,53,23,16,137,1,192,4,0,253,36,3,112,0,18, + 16,23,0,43,0,3,0,37,80,227,18,149,41,63,0,116,74,0,37,0,245,128,21,85, + 213,63,17,0,36,37,80,62,16,255,67,3,177,4,95,37,64,156,0,1,192,37,0,46, + 19,37,80,24,16,52,0,37,0,12,64,132,37,32,102,9,213,10,16,37,240,12,64,37, + 48,192,35,85,213,4,0,189,96,24,16,0,37,240,37,32,66,128,2,112,203,48,24,16, + 37,240,37,96,192,210,16,56,24,48,37,240,37,96,0,87,245,226,3,24,32,12,37,240, + 37,112,124,53,6,0,24,32,37,240,37,112,0,90,21,24,48,37,240,37,240,0,0,37, + 240,37,240,37,240,0,37,240,12,0,37,240,37,240,0,0,229,4,5,0,12,0,21,3, + 37,32,12,37,32,48,17,0,37,96,24,0,157,204,247,2,3,0,48,184,2,32,2,124, + 23,85,53,55,37,32,220,30,0,37,80,24,0,187,0,119,113,2,0,0,220,102,1,3, + 87,0,112,87,75,32,243,75,0,192,37,64,220,62,0,24,0,0,204,255,237,22,48,255, + 187,255,15,68,0,252,255,63,5,0,93,74,151,0,112,34,4,192,37,0,62,16,0,0, + 16,209,192,6,48,106,0,0,0,220,3,0,243,255,255,127,3,192,0,240,63,112,195,73, + 15,62,16,24,48,0,75,0,0,0,12,3,0,111,0,0,48,0,0,16,192,0,0,16, + 203,192,0,0,32,12,3,0,0,176,240,0,204,0,240,0,0,240,0,0,240,0,0,95, + 15,48,4,0,0,240,22,240,0,32,95,245,4,0,0,240,186,22,112,240,55,128,255,95, + 85,13,80,240,240,8,80,0,0,22,128,55,112,0,240,85,255,145,85,13,80,22,240,0, + 16,240,8,80,0,0,95,151,85,255,85,69,64,240,22,240,0,16,95,153,85,8,64,33, + 16,85,245,2,0,9,0,0,120,22,240,22,32,10,0,85,255,255,255,25,32,141,95,19, + 0,85,85,37,32,22,240,22,32,95,0,7,0,0,0,57,16,33,16,0,16,22,240,0, + 32,22,16,120,64,0,25,32,7,0,15,255,255,255,9,0,171,85,245,10,48,95,10,0, + 85,7,0,15,135,255,255,255,9,0,22,32,48,0,60,144,95,96,7,0,0,0,83,208, + 22,128,11,48,245,255,9,0,0,34,64,68,32,22,240,22,128,56,48,11,64,22,240,9, + 0,65,85,22,128,56,48,69,32,79,48,22,240,255,8,0,0,22,16,193,0,7,0,0, + 32,211,0,11,64,22,240,0,32,0,5,114,0,160,22,240,0,32,18,130,0,144,22,240, + 0,32,16,22,240,0,32,22,240,0,32,95,113,240,0,16,22,240,102,0,32,240,0,0, + 240,0,0,240,0,0,240,230,0,32,168,2,4,0,0,48,170,10,42,124,5,0,0,32, + 128,2,0,160,0,0,64,245,168,14,0,0,0,64,8,0,0,128,177,42,7,0,0,16, + 11,16,160,170,8,0,0,255,0,160,10,0,0,32,0,128,213,0,0,16,40,3,0,0, + 37,32,160,170,201,10,9,0,0,0,32,21,80,0,0,160,2,115,0,8,6,0,0,48, + 2,40,10,12,96,31,8,10,0,160,170,0,64,55,64,0,240,0,0,240,0,240,0,240, + 0,240,0,240,0,240,0,240,0,240,0,0,240,0,240,0,240,0,240,0,240,0,240,0, + 240,0,240,0,0,240,0,240,0,240,0,240,0,240,0,240,0,240,0,240,0,0,240,0, + 240,0,240,0,240,0,240,0,240,0,240,0,240,0,0,240,0,240,0,240,0,240,0,240, + 0,240,0,240,0,240,24,0,240,0,240,0,192,80,85,4,0,0,240,0,240,56,0,240, + 0,240,0,0,64,85,1,5,96,0,240,0,0,240,0,240,0,240,0,240,0,240,0,240, + 0,240,0,240,56,0,240,0,240,0,112,20,64,1,5,0,0,240,16,0,240,0,240,0, + 240,123,1,0,79,16,5,48,0,240,0,0,240,0,240,0,240,0,240,0,240,0,240,0, + 240,0,240,120,0,240,0,240,0,96,64,5,0,5,6,0,240,0,240,0,240,0,240,0, + 240,0,0,21,0,1,20,5,96,0,240,0,240,0,240,0,240,0,240,0,240,64,0,240, + 0,240,0,240,0,240,0,240,0,96,80,2,32,160,0,240,0,240,0,240,0,240,123,1, + 1,2,16,80,0,8,32,0,240,0,80,201,8,0,240,0,240,0,240,0,240,64,0,240, + 0,240,0,240,0,240,0,240,0,224,21,213,0,6,6,0,85,5,6,48,0,240,0,240, + 0,240,0,144,2,9,33,21,5,32,0,240,0,80,30,0,0,240,0,240,0,0,240,0, + 240,0,240,0,240,0,240,0,240,0,240,0,240,30,0,64,20,0,80,0,0,240,0,240, + 0,240,0,0,240,0,240,0,240,0,240,0,240,0,240,0,240,0,240,192,0,240,0,240, + 0,240,0,240,0,240,0,80,80,85,1,21,5,0,0,240,0,240,0,240,0,240,0,240, + 0,240,230,0,96,64,5,4,0,0,240,0,64,21,36,4,0,0,176,80,216,243,0,160, + 84,31,240,0,240,136,0,240,105,240,19,0,5,4,0,0,240,43,240,0,192,149,240,0, + 192,149,240,0,240,0,240,105,96,80,5,180,4,0,0,192,4,3,0,80,0,0,240,80, + 109,0,0,48,64,0,0,48,16,0,0,224,89,20,3,0,0,48,16,0,0,0,84,3, + 0,198,0,112,80,1,4,0,0,112,28,144,4,20,146,86,0,5,8,0,0,48,80,10, + 112,0,96,4,182,3,0,80,0,0,240,80,0,0,48,64,45,0,0,48,16,0,0,224, + 20,3,0,0,48,203,16,0,0,0,84,3,0,0,112,80,1,88,4,0,0,112,28,144, + 4,20,86,0,5,8,0,176,0,240,0,240,0,240,0,80,64,0,0,192,64,217,0,0, + 240,0,80,80,0,0,240,64,0,6,0,16,64,5,4,0,0,240,0,240,0,240,0,240, + 154,78,0,0,0,192,64,0,0,240,0,80,80,109,0,0,240,64,0,0,16,64,5,4, + 0,52,0,240,68,14,170,4,192,170,10,6,0,4,192,104,0,0,108,14,4,0,170,4, + 112,170,10,6,0,122,4,112,128,4,16,0,0,160,170,39,0,162,4,64,170,21,0,129, + 14,30,16,170,4,16,128,142,4,96,0,0,160,5,16,39,64,10,0,128,212,38,0,7, + 16,160,4,16,128,4,16,0,160,26,4,16,0,10,32,170,42,4,16,10,16,37,0,222, + 0,0,0,0,160,170,4,0,0,168,26,4,16,160,4,16,0,42,3,0,39,240,28,0, + 108,4,0,39,48,0,168,3,0,160,42,4,16,16,118,48,5,0,4,0,0,16,10,114, + 0,4,0,5,0,54,4,0,0,168,4,0,160,42,8,0,39,240,96,33,0,4,0,5, + 0,4,0,39,32,170,42,8,0,3,170,170,4,32,39,0,4,0,0,32,33,0,4,0, + 4,20,16,4,0,168,4,0,27,0,4,0,8,0,4,0,16,11,0,4,0,0,32,39, + 160,128,4,0,27,0,4,0,0,8,0,4,0,11,0,4,0,0,32,27,0,4,0,20, + 16,172,4,0,39,0,160,42,7,0,160,124,16,160,50,24,0,2,4,16,0,32,10,128, + 4,16,114,16,206,4,0,0,168,10,39,240,0,48,10,128,136,4,16,33,0,4,16,0, + 39,240,4,0,0,32,10,113,128,4,16,33,0,4,16,0,168,10,6,32,153,170,39,16, + 23,0,170,2,3,0,0,64,128,34,3,0,170,33,0,4,16,67,16,10,6,0,39,240, + 34,39,240,10,6,0,39,240,39,240,10,6,0,39,240,34,39,240,10,6,0,39,240,39, + 240,10,6,0,39,240,34,39,240,10,6,0,39,240,39,240,10,6,0,39,240,34,39,240, + 10,6,0,39,240,39,240,10,6,0,39,240,34,39,240,10,6,0,39,240,39,240,10,6, + 0,39,240,24,39,224,7,242,0,48,10,128,4,16,39,16,4,0,49,0,47,242,4,0, + 0,32,10,128,4,16,33,0,98,4,16,0,39,240,4,0,0,32,10,128,4,16,156,33, + 0,4,16,0,168,10,247,242,0,48,0,17,128,4,16,20,16,4,0,128,4,0,27,0, + 4,0,0,8,0,4,0,11,0,4,0,0,32,27,0,4,0,20,16,4,4,0,39,32, + 42,8,0,13,32,24,0,39,240,4,32,45,168,4,0,160,42,8,0,160,33,0,4,0, + 40,5,0,4,0,0,16,10,39,16,10,73,16,4,0,27,0,168,4,0,160,42,8,0, + 39,240,33,0,176,4,0,5,0,4,0,39,32,0,42,3,0,0,129,170,5,16,39,16, + 4,0,0,16,33,16,4,0,0,45,168,4,16,0,168,3,0,0,49,32,39,64,243,10, + 0,4,0,0,16,0,0,160,170,116,4,0,39,48,160,4,16,0,160,10,4,16,150,74, + 16,160,170,10,0,128,37,0,7,16,160,90,4,16,128,4,16,0,160,4,16,0,34,64, + 242,4,32,170,26,16,0,0,10,0,0,170,210,4,16,128,4,96,0,0,160,34,0,0, + 160,77,170,4,112,170,10,6,0,4,112,128,4,16,54,0,0,160,170,4,192,170,10,6, + 0,4,192,25,128,2,80,85,0,170,170,5,16,11,0,2,96,95,130,32,0,128,0,20, + 16,170,26,80,253,130,17,32,80,5,0,80,0,84,251,85,64,110,10,85,80,85,5,80, + 255,85,80,85,21,84,5,64,85,207,5,21,80,65,19,0,0,224,84,85,245,0,1,0, + 85,27,0,80,85,80,85,27,5,84,4,0,21,85,4,16,47,240,0,32,251,20,84,47, + 0,21,80,1,64,5,139,80,85,54,8,84,59,8,42,0,4,32,1,211,80,0,0,240, + 22,48,84,4,0,84,84,126,95,0,21,0,0,64,5,85,47,240,252,0,96,22,32,80, + 85,1,85,5,85,254,47,0,21,85,85,0,80,5,85,225,85,9,32,24,0,0,240,47, + 64,0,85,5,187,21,84,13,0,85,85,0,105,0,5,49,64,47,240,0,64,47,32,84, + 5,8,0,32,0,187,0,21,14,0,21,0,1,9,64,80,181,0,0,240,85,22,16,85, + 5,38,0,85,191,85,81,1,21,21,80,47,48,64,118,9,16,80,0,0,240,84,80,0, + 47,0,229,0,38,16,84,4,0,37,0,21,0,1,111,80,1,64,5,9,0,80,0,0, + 240,253,80,171,48,65,85,5,0,84,64,254,127,17,0,21,0,84,85,64,85,49,0,127, + 241,0,32,47,240,21,0,47,240,0,240,0,0,240,0,240,0,240,0,240,0,208,141,0, + 5,32,4,0,10,137,0,21,5,0,85,17,32,0,0,197,1,5,0,128,20,144,114,1, + 47,240,26,48,0,0,47,240,47,0,80,110,19,16,0,80,81,6,16,85,81,20,16,0, + 3,16,0,32,41,48,5,80,0,16,11,0,8,96,5,0,0,68,48,3,16,0,32,20, + 48,5,80,0,16,11,0,8,96,96,5,0,47,240,5,128,0,16,11,0,21,80,10,16, + 0,8,0,5,0,47,48,164,0,5,0,0,0,20,64,164,32,174,20,48,21,80,21,20, + 64,85,5,16,21,33,4,5,32,3,0,0,32,26,48,80,41,48,3,0,128,38,16,8, + 0,10,16,47,240,0,0,26,48,47,192,80,193,1,10,16,26,48,5,80,0,64,14,48, + 64,85,24,4,0,9,32,5,0,84,1,127,49,26,48,5,48,152,107,48,0,0,89,48, + 0,4,3,0,20,64,21,195,80,21,47,240,26,48,0,0,47,240,21,80,88,5,16,0, + 240,0,48,192,255,0,48,3,8,240,176,8,240,47,0,0,240,0,48,112,85,0,48,13, + 152,8,240,8,240,47,0,252,255,4,0,0,32,63,36,3,0,0,112,92,31,0,0,16, + 53,8,240,8,240,54,47,0,87,85,63,64,255,213,8,32,166,0,146,0,0,63,23,0, + 0,48,213,8,240,8,240,0,239,0,240,87,85,222,48,240,95,85,101,213,8,0,192,22, + 0,0,0,213,0,35,96,144,8,240,8,240,214,0,5,0,15,70,0,8,80,112,196,22, + 0,0,16,3,35,96,8,240,8,240,0,0,197,95,6,0,61,70,0,8,80,71,240,87, + 85,149,93,6,0,85,8,16,85,36,16,8,32,117,173,85,0,0,213,87,3,0,85,0, + 0,213,90,198,48,245,70,0,192,93,8,48,112,6,0,132,0,16,71,144,253,6,0,8, + 32,9,48,8,16,245,33,85,0,0,8,16,3,16,71,0,240,5,0,52,0,218,63,0, + 125,7,0,213,3,71,128,117,119,105,95,7,0,40,16,245,8,80,85,253,3,0,210,8, + 32,245,3,0,8,32,213,27,16,213,0,185,220,5,0,255,33,92,117,85,0,0,15,214, + 71,128,245,255,2,0,95,44,16,245,127,70,8,80,255,95,8,96,64,0,8,32,127,8, + 16,211,0,220,78,16,71,64,87,8,16,112,85,223,245,213,255,255,87,71,0,213,255,217, + 95,37,0,44,16,253,127,8,80,255,95,214,8,80,127,213,8,64,255,8,16,0,92,223, + 95,245,255,255,255,167,1,87,213,124,127,0,71,32,253,213,255,255,95,71,0,191,85,255, + 87,245,255,255,44,16,255,109,95,8,80,255,87,8,80,95,213,8,64,253,127,8,16,0, + 87,127,253,213,95,238,167,17,87,117,125,47,0,53,0,112,79,213,255,87,85,98,0,71, + 16,95,16,1,44,44,32,8,80,127,253,8,80,245,138,0,8,32,184,61,0,8,0,62, + 0,95,85,127,215,18,87,63,245,127,85,95,245,63,143,0,71,0,65,85,71,48,8,0, + 35,0,44,0,8,96,125,8,80,225,245,8,80,3,0,8,0,62,0,87,85,117,222,71, + 32,213,87,85,125,8,0,112,85,65,127,125,0,71,32,8,16,35,0,8,0,87,8,80, + 21,93,8,80,117,8,80,213,8,32,62,32,71,48,9,85,8,48,71,0,85,71,80,8, + 0,35,0,8,240,12,8,240,71,112,85,85,8,48,71,240,8,240,8,240,4,134,96,71, + 0,95,8,32,71,240,8,240,8,240,71,0,6,103,81,85,93,1,0,71,240,44,128,8, + 240,8,96,206,62,0,253,213,95,62,0,8,0,117,85,196,8,16,71,32,213,71,240,8, + 240,8,144,0,95,247,85,245,255,63,34,95,85,245,247,137,87,8,0,135,2,255,101,2, + 71,16,7,0,255,65,255,44,16,6,0,8,240,8,160,61,21,245,14,19,58,8,16,253, + 8,16,112,85,245,25,0,23,19,130,80,32,127,44,16,8,240,8,208,62,16,71,48,124, + 55,85,85,125,8,16,112,85,0,48,71,0,66,80,16,95,44,16,8,240,8,208,54,16, + 245,52,0,0,86,5,71,32,55,148,4,0,0,16,44,0,0,48,8,240,68,8,112,199, + 20,125,190,20,8,0,21,0,213,135,4,224,22,0,0,16,71,240,8,240,8,112,0,192, + 95,111,85,93,85,253,71,16,87,213,154,0,240,71,240,44,128,8,240,8,96,0,0,127, + 85,147,95,213,54,4,8,0,245,30,17,71,240,213,152,8,240,8,240,55,16,87,213,30, + 1,8,0,93,11,85,221,8,0,192,22,0,0,0,89,18,0,48,112,44,112,8,240,8, + 96,55,0,87,87,63,69,0,34,8,0,127,53,4,0,0,27,48,53,7,64,35,0,136, + 8,240,8,240,55,0,252,6,55,177,1,8,48,252,4,201,1,33,21,92,31,0,0,16, + 124,119,8,240,8,96,8,55,0,0,240,0,48,112,31,0,0,16,12,120,8,240,144,8, + 96,47,0,0,240,0,48,192,149,0,0,16,3,0,8,240,8,240, +}; +#endif diff --git a/games/dinorun/tests/eadk.h b/games/dinorun/tests/eadk.h new file mode 100644 index 00000000..57ef7707 --- /dev/null +++ b/games/dinorun/tests/eadk.h @@ -0,0 +1,14 @@ +#ifndef DINO_FAKE_EADK_H +#define DINO_FAKE_EADK_H +#include +typedef struct {uint16_t x,y,width,height;} eadk_rect_t; +/* Test-only names; the real NumWorks SDK supplies these. */ +enum {eadk_key_left=0,eadk_key_up=1,eadk_key_down=2,eadk_key_right=3, + eadk_key_ok=4,eadk_key_back=5,eadk_key_home=6,eadk_key_on_off=8, + eadk_key_shift=12,eadk_key_backspace=17,eadk_key_eight=31, + eadk_key_two=43,eadk_key_exe=52}; +uint64_t eadk_keyboard_scan(void); +uint32_t eadk_timing_millis(void); +void eadk_timing_msleep(uint32_t ms); +void eadk_display_push_rect(eadk_rect_t rect,const uint16_t *px); +#endif diff --git a/games/dinorun/tests/test_dino.c b/games/dinorun/tests/test_dino.c new file mode 100644 index 00000000..7d6936f0 --- /dev/null +++ b/games/dinorun/tests/test_dino.c @@ -0,0 +1,334 @@ +/* Host tests of Chrome Dino, on the game's own source: Chrome's numbers (the jump, the + * speed, the obstacles' rules, the collision boxes, night, the score's flashes), the + * calculator's keys and pause menu, and the drawing (sending only the tiles that changed + * leaves the screen as a full redraw). Then long games with random keys and a player + * that jumps, looking for anything out of bounds (built with the address and undefined + * behaviour sanitizers). + * + * make host-test */ +#include +#include +#include +#include +#include +#define main dino_app_main +#include "../src/main.c" +#undef main + +static uint16_t screen[W * H]; +static uint64_t pixels_sent; +uint64_t eadk_keyboard_scan(void) { return 0; } +uint32_t eadk_timing_millis(void) { return 0; } +void eadk_timing_msleep(uint32_t ms) { (void)ms; } +void eadk_display_push_rect(eadk_rect_t r, const uint16_t *px) { + assert(r.width > 0 && r.height > 0 && r.x + r.width <= W && r.y + r.height <= H); + pixels_sent += (uint64_t)r.width * r.height; + for (int y = 0; y < r.height; y++) + for (int x = 0; x < r.width; x++) screen[(y + r.y) * W + x + r.x] = px[y * r.width + x]; +} + +/* ---- driving the game as main() does, a frame being one tick */ +static uint64_t keys_now; +static void step(uint64_t keys) { + uint64_t hit = keys & ~keys_now, released = keys_now & ~keys; + keys_now = keys; + input(keys, hit, released, (float)TICK_MS); + if (!menu) tick(); +} +static void fresh(void) { + memset(&g, 0, sizeof g); + memset(&trex, 0, sizeof trex); + memset(&jump_key, 0, sizeof jump_key), memset(&duck_key, 0, sizeof duck_key); + menu = MENU_NONE, leave = false, keys_now = 0; + rng = 0x12345678u; + runner_init(); +} +static uint16_t screen_copy[W * H]; +static void check_dirty_equals_full(void) { + frame(); + memcpy(screen_copy, screen, sizeof screen); + hashes_valid = false; + frame(); + assert(!memcmp(screen_copy, screen, sizeof screen)); +} + +/* Chrome's Trex.updateJump, written again from offline.ts for the jump's heights */ +static int chrome_apex(double speed, int hold_ticks) { + double vel = INITIAL_JUMP_VELOCITY - speed / 10; + int y = GROUND_Y, apex = y; + bool min_reached = false; + for (int t = 0; t < 200; t++) { + if (t == hold_ticks && min_reached && vel < DROP_VELOCITY) vel = DROP_VELOCITY; /* key up */ + y += (int)floor(vel + 0.5); + vel += GRAVITY; + if (y < GROUND_Y - MIN_JUMP_HEIGHT) min_reached = true; + if (y < MAX_JUMP_HEIGHT && min_reached && vel < DROP_VELOCITY) vel = DROP_VELOCITY; + if (y > GROUND_Y) break; + if (y < apex) apex = y; + } + return apex; +} + +static void test_start_and_intro(void) { + fresh(); + assert(!g.playing && trex.x == 0 && trex.y == GROUND_Y && container_w() == TREX_W); + frame(); + /* only the T-rex shows, in a window of its width */ + bool dark_left = false, dark_right = false; + for (int y = CANVAS_Y; y < CANVAS_Y + CANVAS_H; y++) + for (int x = 0; x < W; x++) + if (screen[y * W + x] == rgb(83)) *(x < TREX_W ? &dark_left : &dark_right) = true; + assert(dark_left && !dark_right); + assert(fabs(g.current_speed - 3.84) < 1e-9); /* 6 x 320 / 600 x 1.2: Chrome's speed on a narrow screen */ + step(KEY(eadk_key_ok)); + assert(g.playing && trex.jumping && !g.activated); + assert(fabs(g.current_speed - 3.841) < 1e-9); /* and 0.001 more each frame */ + int ticks = 0; + while (trex.jumping) step(KEY(eadk_key_ok)), ticks++; + assert(ticks > 20 && ticks < 60); + /* landing starts the intro: the canvas widens in 0.4 s and the T-rex walks in */ + assert(g.playing_intro && g.activated); + step(0); + int w0 = container_w(); + while (g.playing_intro) step(0); + assert(container_w() == WIDTH && w0 < WIDTH); + assert(trex.x >= 22 && trex.x <= 25); /* a pixel a frame for the intro's 0.4 s, as in Chrome (not its 50) */ + /* no obstacle for 3 seconds */ + int t = 0; + while (!g.obstacle_count && t < 400) step(0), t++; + assert(t == 180); /* the first obstacle once the game has run 3 s (180 frames) */ +} + +static void test_jump_heights(void) { + /* a held jump goes to Chrome's height; one let go early is lower */ + for (int hold = 1; hold <= 40; hold += 13) { + fresh(); + step(KEY(eadk_key_ok)); /* start: the first jump */ + while (trex.jumping) step(KEY(eadk_key_ok)); + for (int i = 0; i < 300; i++) g.obstacle_count = 0, g.running_time = 0, step(0); /* (no obstacles) */ + double speed = g.current_speed; + assert(!trex.jumping && !g.crashed); + step(KEY(eadk_key_up)); + int apex = trex.y, t = 1; + while (trex.jumping) { + g.obstacle_count = 0; + step(t < hold ? KEY(eadk_key_up) : 0); + if (trex.y < apex) apex = trex.y; + t++; + } + int want = chrome_apex(speed, hold); + if (abs(apex - want) > 1) printf("hold %d: apex %d, Chrome's %d\n", hold, apex, want); + assert(abs(apex - want) <= 1); + } +} + +static void test_speed_and_spawns(void) { + fresh(); + step(KEY(eadk_key_ok)); + bool ptero_slow = false, ptero_seen = false, small_group_slow = false, large_group_slow = false; + int run = 0, last = -1; + double prev = g.current_speed; + for (int i = 0; i < 60 * 300; i++) { + step(0); + assert(g.current_speed >= prev && g.current_speed <= MAX_SPEED + ACCELERATION); + prev = g.current_speed; + const obstacle *o = g.obstacle_count ? &g.obstacles[g.obstacle_count - 1] : 0; + if (o && o->x == WIDTH + o->type->width && !o->following_created) { /* (one spawned this frame) */ + int type = (int)(o->type - TYPE); + if (type == PTERODACTYL && g.current_speed < 8.5) ptero_slow = true; + if (type == PTERODACTYL) ptero_seen = true; + if (type == CACTUS_SMALL && o->size > 1 && g.current_speed < 4) small_group_slow = true; + if (type == CACTUS_LARGE && o->size > 1 && g.current_speed < 7) large_group_slow = true; + run = type == last ? run + 1 : 1; + last = type; + assert(run <= MAX_OBSTACLE_DUPLICATION); + } + assert(g.obstacle_count <= MAX_OBSTACLES); + for (int k = 0; k < g.obstacle_count; k++) g.obstacles[k].y = -500; /* (out of the way: they only go by) */ + assert(!g.crashed); + } + printf("spawns: pterodactyl before 8.5 %d, seen %d, groups too early %d %d\n", ptero_slow, ptero_seen, small_group_slow, + large_group_slow); + assert(!ptero_slow && ptero_seen && !small_group_slow && !large_group_slow); + assert(fabs(g.current_speed - MAX_SPEED) < 0.01); +} + +static void place(int type, int y) { + obstacle_init(&g.obstacles[0], &TYPE[type], g.current_speed); + g.obstacles[0].size = 1, g.obstacles[0].width = TYPE[type].width; + for (int i = 0; i < TYPE[type].nboxes; i++) g.obstacles[0].boxes[i] = TYPE[type].boxes[i]; + g.obstacles[0].x = trex.x + 10; + g.obstacles[0].y = y; + g.obstacle_count = 1; + if (g.running_time <= CLEAR_TIME) g.running_time = CLEAR_TIME + 1; /* (obstacles count from 3 s on) */ +} +static void test_collisions(void) { + fresh(); + step(KEY(eadk_key_ok)); + while (trex.jumping) step(0); + for (int i = 0; i < 60; i++) step(0); + /* the pterodactyl's three heights: the low one is jumped, the middle one ducked under, the high one flies over */ + place(PTERODACTYL, 100); + assert(check_collision(&g.obstacles[0])); + trex_set_duck(true); + assert(check_collision(&g.obstacles[0])); + trex_set_duck(false); + place(PTERODACTYL, 75); + assert(check_collision(&g.obstacles[0])); + trex_set_duck(true); + assert(!check_collision(&g.obstacles[0])); + trex_set_duck(false); + place(PTERODACTYL, 50); + assert(!check_collision(&g.obstacles[0])); + /* cacti are hit standing and ducking */ + place(CACTUS_SMALL, 105); + assert(check_collision(&g.obstacles[0])); + trex_set_duck(true); + assert(check_collision(&g.obstacles[0])); + trex_set_duck(false); + /* ...but not from high above */ + trex.y = 20; + assert(!check_collision(&g.obstacles[0])); + trex.y = GROUND_Y; +} + +static void test_night_and_flash(void) { + fresh(); + step(KEY(eadk_key_ok)); + bool flashed = false; + int night_ticks = 0; + for (int i = 0; i < 60 * 200 && !g.crashed; i++) { + g.obstacle_count = 0, g.running_time = 0; /* (no obstacles) */ + step(0); + if (g.achievement && !g.painted) flashed = true; + uint32_t score = actual_distance((uint32_t)iceil(g.distance_ran)); + if (g.inverted) night_ticks++; + if (score >= 700 && score < 710) assert(g.inverted); + if (score > 1300) break; + } + assert(flashed); + /* 12 seconds of night, the moon shown */ + assert(night_ticks >= 12000 / TICK_MS - 2 && night_ticks <= 12000 / TICK_MS + 3); +} + +static void test_game_over_and_restart(void) { + fresh(); + step(KEY(eadk_key_ok)); + while (trex.jumping) step(0); + for (int i = 0; i < 60; i++) step(0); + place(CACTUS_SMALL, 105); + step(0); + assert(g.crashed && !g.playing && trex.status == CRASHED); + assert(g.highest_score == (uint32_t)iceil(g.distance_ran)); + /* OK let go before 1.2 s: nothing; after: a new game */ + step(KEY(eadk_key_ok)), step(0); + assert(g.crashed); + for (int i = 0; i < 75; i++) step(0); + step(KEY(eadk_key_ok)), step(0); + assert(!g.crashed && g.playing && g.distance_ran < 10); + /* EXE (Chrome's Enter) at once */ + place(CACTUS_SMALL, 105); + step(0); + assert(g.crashed); + step(KEY(eadk_key_exe)), step(0); + assert(!g.crashed); + check_dirty_equals_full(); +} + +static void test_menu(void) { + fresh(); + step(KEY(eadk_key_ok)); + for (int i = 0; i < 100; i++) step(0); + double d = g.distance_ran; + step(KEY(eadk_key_back)); + assert(menu == MENU_PAUSE); + for (int i = 0; i < 50; i++) step(0); + assert(g.distance_ran == d); /* paused */ + step(KEY(eadk_key_ok)); /* Resume */ + assert(!menu); + step(KEY(eadk_key_back)), step(0); + step(KEY(eadk_key_down)), step(0), step(KEY(eadk_key_down)), step(0); /* Quit game */ + step(KEY(eadk_key_ok)), step(0); + assert(menu == MENU_QUIT && !leave); + frame(); + step(KEY(eadk_key_ok)), step(0); /* No (chosen first) */ + assert(menu == MENU_PAUSE && !leave); + step(KEY(eadk_key_ok)), step(0); + step(KEY(eadk_key_left)), step(0); + step(KEY(eadk_key_ok)); + assert(leave); + /* Home quits at once; Back quits from the start and from game over */ + fresh(); + step(KEY(eadk_key_home)); + assert(leave); + fresh(); + step(KEY(eadk_key_back)); + assert(leave); +} + +/* a player that jumps when a cactus or a low pterodactyl comes near, and ducks under a middle one */ +static uint64_t bot(void) { + if (!g.obstacle_count) return 0; + const obstacle *o = &g.obstacles[0]; + int d = o->x - (trex.x + TREX_W); + int reach = (int)(g.current_speed * 9) + 10; + if (d > reach || o->x + o->width < trex.x) return 0; + if (o->type == &TYPE[PTERODACTYL] && o->y == 75) return KEY(eadk_key_down); + if (o->type == &TYPE[PTERODACTYL] && o->y == 50) return 0; + return KEY(eadk_key_ok); +} +static void test_long_games(void) { + int best = 0; + for (int game = 0; game < 30; game++) { + fresh(); + rng = 0x9E3779B9u * (uint32_t)(game + 1); + step(KEY(eadk_key_ok)); + uint32_t r = (uint32_t)game * 7919u + 1; + for (int i = 0; i < 60 * 240; i++) { + r = r * 1103515245u + 12345u; + uint64_t k; + if (game < 10) { /* random keys */ + static const int KS[] = {eadk_key_ok, eadk_key_up, eadk_key_down, eadk_key_two, eadk_key_exe, eadk_key_eight, eadk_key_back}; + k = r >> 28 < 6 ? KEY(KS[(r >> 16) % 7]) : 0; + } else { + k = bot(); + if (g.crashed) k = (r >> 20) & 1 ? KEY(eadk_key_exe) : 0; + } + step(k); + if (leave) break; + if (menu && (r >> 24) % 3 == 0) step(KEY(eadk_key_back)); + assert(g.obstacle_count >= 0 && g.obstacle_count <= MAX_OBSTACLES && g.cloud_count <= MAX_CLOUDS); + assert(trex.y <= GROUND_Y && trex.y > -60); + if ((i & 255) == 0) frame(); + int s = (int)actual_distance(g.highest_score); + if (s > best) best = s; + } + check_dirty_equals_full(); + } + printf("long games: the jumping player's best %d\n", best); + assert(best > 700); /* (it saw a night) */ +} + +int main(void) { + np_lz_unpack(SHEET_LZ, sizeof SHEET_LZ, sheet, sizeof sheet); + test_start_and_intro(); + test_jump_heights(); + test_speed_and_spawns(); + test_collisions(); + test_night_and_flash(); + test_game_over_and_restart(); + test_menu(); + pixels_sent = 0; + fresh(); + hashes_valid = false; + frame(); + uint64_t full = pixels_sent; + pixels_sent = 0; + step(KEY(eadk_key_ok)); + frame(); + printf("drawing: %llu pixels for the first screen, %llu for the next frame\n", (unsigned long long)full, + (unsigned long long)pixels_sent); + test_long_games(); + printf("Chrome Dino: all tests passed\n"); + return 0; +} diff --git a/games/dinorun/tools/sprites.py b/games/dinorun/tools/sprites.py new file mode 100644 index 00000000..340ada13 --- /dev/null +++ b/games/dinorun/tools/sprites.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""Packs Chrome's own dinosaur game pictures into src/sprites.h. + +The source is Chromium's sprite sheet for the game at 1x, +components/neterror/resources/images/default_100_percent/offline/100-offline-sprite.png +(https://chromium.googlesource.com/chromium/src, BSD license: see ../LICENSE), at the +positions its offline_sprite_definitions.ts and the game's classes give. Its +pictures have four tones: clear, the ink (#535353), the light grey of the clouds +and the moon (#DADADA) and white; a pixel is 2 bits, rows a whole number of bytes. +The regions are packed together with games/common/np_lz.h's LZ77, which the game +unpacks into RAM when it starts. + +Usage: sprites.py 100-offline-sprite.png +""" +import os +import sys + +from PIL import Image + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT = os.path.join(HERE, "..", "src", "sprites.h") + +# name: x, y, width, height on the sheet (1x) +REGIONS = [ + ("TREX", 848, 2, 176, 47), # standing frames at 0 (jumping, waiting), 44 (waiting), 88 and 132 (running) + ("TREX_CRASH", 1068, 2, 44, 47), # the tRex frame at 220 + ("TREX_DUCK", 1112, 2, 118, 47), # the ducking frames at 264 and 323, 59 wide + ("CACTUS_SMALL", 228, 2, 102, 35), # one, two and three together: 17, 34 and 51 wide + ("CACTUS_LARGE", 332, 2, 150, 50), # 25, 50 and 75 wide + ("PTERODACTYL", 134, 2, 92, 40), # two frames, 46 wide + ("CLOUD", 86, 2, 46, 14), + ("HORIZON", 2, 52, 1200, 12), # a flat stretch of ground and a bumpy one, 600 wide + ("MOON", 484, 2, 160, 40), # its phases (night_mode.ts: PHASES) + ("STAR", 645, 2, 9, 18), # two stars + ("TEXT", 655, 2, 191, 24), # the figures and HI (10 x 13 each), then GAME OVER (191 x 11) at y 13 + ("RESTART", 2, 68, 288, 32), # the restart button's 8 frames, 36 apart +] + + +def tone(r, g, b, a): + if a < 128: + return 0 + v = (r + g + b) / 3 + return 1 if v < 150 else 2 if v < 236 else 3 + + +def lz_pack(data): + """games/common/np_lz.h's np_lz_pack, the same stream (and the same choices).""" + table = [0xFFFF] * 4096 + out, i, n = bytearray(), 0, len(data) + flag_at, items = 0, 8 + while i < n: + if items == 8: + flag_at = len(out) + out.append(0) + items = 0 + length = dist = 0 + if i + 3 <= n: + h = ((data[i] << 8) ^ (data[i + 1] << 4) ^ data[i + 2]) & 4095 + cand = table[h] + table[h] = i & 0xFFFF + if cand != 0xFFFF and cand < i and i - cand <= 4096: + m = min(n - i, 18) + while length < m and data[cand + length] == data[i + length]: + length += 1 + dist = i - cand + if length >= 3: + out += bytes(((dist - 1) & 255, ((dist - 1) >> 8) | ((length - 3) << 4))) + for k in range(1, length): + j = i + k + if j + 3 <= n: + table[((data[j] << 8) ^ (data[j + 1] << 4) ^ data[j + 2]) & 4095] = j & 0xFFFF + i += length + else: + out[flag_at] |= 1 << items + out.append(data[i]) + i += 1 + items += 1 + return bytes(out) + + +def lz_unpack(data): + out, i = bytearray(), 0 + while i < len(data): + flags = data[i] + i += 1 + for b in range(8): + if i >= len(data): + break + if flags >> b & 1: + out.append(data[i]) + i += 1 + else: + dist = (data[i] | (data[i + 1] & 15) << 8) + 1 + length = (data[i + 1] >> 4) + 3 + i += 2 + for _ in range(length): + out.append(out[-dist]) + return bytes(out) + + +def main(): + sheet = Image.open(sys.argv[1]).convert("RGBA") + assert sheet.size == (1233, 100), "the 1x sheet, 1233 x 100" + px = sheet.load() + blob, rows = bytearray(), [] + for name, x0, y0, w, h in REGIONS: + stride = (w + 3) // 4 + rows.append((name, len(blob), w, h, stride)) + for y in range(h): + line = bytearray(stride) + for x in range(w): + line[x >> 2] |= tone(*px[x0 + x, y0 + y]) << (2 * (x & 3)) + blob += line + packed = lz_pack(bytes(blob)) + assert lz_unpack(packed) == bytes(blob) + out = ["/* Generated by tools/sprites.py from Chromium's 100-offline-sprite.png (BSD license, see ../LICENSE). */", + "#ifndef DINO_SPRITES_H", "#define DINO_SPRITES_H", "#include ", "", + "/* a region of the sheet: 2 bits a pixel (0 clear, 1 ink, 2 light grey, 3 white), the lowest first */", + "typedef struct { uint16_t off, w, stride; uint8_t h; } dino_region;", + "enum { " + ", ".join("R_" + r[0] for r in rows) + ", R_COUNT };", + f"#define SHEET_BYTES {len(blob)}", + "static const dino_region REGIONS[R_COUNT] = {", + " " + ", ".join(f"{{{off}, {w}, {stride}, {h}}}" for name, off, w, h, stride in rows) + ",", "};", + f"static const uint8_t SHEET_LZ[{len(packed)}] = {{"] + for i in range(0, len(packed), 24): + out.append(" " + ",".join(str(b) for b in packed[i:i + 24]) + ",") + out += ["};", "#endif", ""] + open(OUT, "w").write("\n".join(out)) + print(f"sprites: {len(rows)} regions, {len(blob)} bytes unpacked, {len(packed)} packed") + + +if __name__ == "__main__": + main() diff --git a/games/games.json b/games/games.json index eb9f6c92..46baffab 100644 --- a/games/games.json +++ b/games/games.json @@ -192,6 +192,23 @@ ], "repo": "https://github.com/Tatone26/Numworks-games/tree/main/apps/tetris" }, + { + "id": "dinorun", + "title": "Chrome Dino", + "tagline": "Jump, duck, survive", + "credit": "Based on Chrome's dinosaur game by Google", + "top": "0xE5E5E5", + "bottom": "0x555555", + "accent": "0x71A050", + "records": [ + "dinorun.sav" + ], + "shots": [ + "docs/shot.png" + ], + "app": "ChromeDino.nwa", + "repo": "https://github.com/Mason363/NumPlay/tree/main/games/dinorun" + }, { "id": "chess", "title": "Chess", diff --git a/launcher/lang/fr.txt b/launcher/lang/fr.txt index 28fd6d58..d8624234 100644 --- a/launcher/lang/fr.txt +++ b/launcher/lang/fr.txt @@ -78,6 +78,8 @@ Portal Returns Portal Returns Think with portals Pense avec les portails Tetris Tetris Stack and clear lines Empile et efface les lignes +Chrome Dino Chrome Dino +Jump, duck, survive Saute, baisse-toi, survis Chess Échecs Bots, puzzles, 2 players Bots, problèmes, 2 joueurs Flappy Bird Flappy Bird @@ -108,6 +110,7 @@ By Tatone26, from All the Apps Par Tatone26, tiré d'All the Apps Inspired by Tatone26's All the Apps Inspiré d'All the Apps de Tatone26 Based on 2048 by Gabriele Cirulli D'après 2048 de Gabriele Cirulli Based on Block Blast! by Hungry Studio D'après Block Blast! de Hungry Studio +Based on Chrome's dinosaur game by Google D'après le jeu du dinosaure de Chrome, de Google # a game the RAM of the calculator's software is too small for Needs Il manque KB more memory Ko de mémoire diff --git a/launcher/lang/zh.txt b/launcher/lang/zh.txt index dfc156aa..b4447fa4 100644 --- a/launcher/lang/zh.txt +++ b/launcher/lang/zh.txt @@ -77,6 +77,8 @@ Portal Returns 传送门归来 Think with portals 用传送门思考 Tetris 俄罗斯方块 Stack and clear lines 堆叠并消除行 +Chrome Dino Chrome 小恐龙 +Jump, duck, survive 跳跃、蹲下、活下去 Chess 国际象棋 Bots, puzzles, 2 players 电脑对手、谜题、双人对战 Flappy Bird 笨鸟先飞 @@ -107,6 +109,7 @@ By Tatone26, from All the Apps 作者 Tatone26,出自 All the Apps Inspired by Tatone26's All the Apps 灵感来自 Tatone26 的 All the Apps Based on 2048 by Gabriele Cirulli 改编自 Gabriele Cirulli 的 2048 Based on Block Blast! by Hungry Studio 改编自 Hungry Studio 的 Block Blast! +Based on Chrome's dinosaur game by Google 改编自 Google Chrome 的小恐龙游戏 # a game the RAM of the calculator's software is too small for Needs 内存还差 KB more memory KB diff --git a/tests/test_games.py b/tests/test_games.py index f21de9a5..3a18a000 100644 --- a/tests/test_games.py +++ b/tests/test_games.py @@ -56,6 +56,10 @@ def every(t0, t1, step, key, d=80): (3500, "ok", 250)), 5500), "tetris": ("Tetris.nwa", "tetris.sav", presses((1200, "ok")) + every(2500, 7000, 500, "up"), 8000, presses((1200, "ok"), (2500, "ok")), 5000), + # start and run into the first cactus: a best; next time, start and hold OK (the keyboard's + # repeat jumps again on landing): further, a better best + "dinorun": ("ChromeDino.nwa", "dinorun.sav", presses((1500, "ok")), 11000, + presses((1500, "ok")) + [(1700, 10500, K["ok"])], 11000), # add-ons, the Counter (5th), count to 2; next time it opens there: one more "numvisuals": ("NumVisuals.nwa", "numvisuals.sav", presses((1500, "ok"), (2000, "right"), (2300, "right"), (2600, "right"), (2900, "right"), diff --git a/tests/test_upsilon.py b/tests/test_upsilon.py index d06fbfbd..1f50e330 100644 --- a/tests/test_upsilon.py +++ b/tests/test_upsilon.py @@ -31,10 +31,10 @@ def main(): st = os.path.join(a.out, "storage.bin") save = bytes(range(30)) write_storage(st, [("pi.py", b"\x01print(3.14159)\n\x00"), ("snake.sav", save)]) - # the home screen: Crossy Road, Block Blast, Portal Returns, Tetris, Chess...: Right four times, OK + # the home screen: Crossy Road, Block Blast, Portal Returns, Tetris, Chrome Dino, Chess...: Right five times, OK c = emu.Calculator(a.nwa, storage_file=st) c.enable_checks(reads=True) - c.keys = presses((1200, "right"), (1600, "right"), (2000, "right"), (2400, "right"), (3000, "ok")) + \ + c.keys = presses((1200, "right"), (1600, "right"), (2000, "right"), (2400, "right"), (2800, "right"), (3200, "ok")) + \ [(7400, 7800, emu.KEYS["home"])] menu = os.path.join(a.out, "chess.png") c.pending_shots = [(6900, menu)] diff --git a/tools/release.py b/tools/release.py index 9b2c73bd..07a8754a 100644 --- a/tools/release.py +++ b/tools/release.py @@ -46,7 +46,7 @@ ("NumPlay-French.nwa", "NumPlay en français : tous les jeux, en français"), ("NumPlay-Chinese.nwa", "NumPlay 中文版:所有游戏,简体中文"), ] -UPSILON = ("NumPlay-Upsilon.nwa", "For calculators running Upsilon (N0110, N0115): the 14 games that fit in the memory " +UPSILON = ("NumPlay-Upsilon.nwa", "For calculators running Upsilon (N0110, N0115): the 15 games that fit in the memory " "Upsilon gives apps. Install it with nwlink, see the README")