diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index dc29f093..03a0c171 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -80,6 +80,7 @@ jobs:
make -C games/sudoku host test
make -C games/uttt host test
make -C games/sokoban host test
+ make -C games/twoplayer host test
make -C games/flappydunk host test
make -C games/towerblock host test
make -C games/suika host test
diff --git a/README.md b/README.md
index f2fc2527..b460e28b 100644
--- a/README.md
+++ b/README.md
@@ -431,7 +431,7 @@ NumPlay is licensed under the [GNU General Public License v3.0](LICENSE). Copyri
 Level Devil |
-  2 Players |
+  2 Players |
 Cookie Clicker |
 Space Invaders |
diff --git a/docs/media/gallery/twoplayer.gif b/docs/media/gallery/twoplayer.gif
index 66580b14..8fb9cd14 100644
Binary files a/docs/media/gallery/twoplayer.gif and b/docs/media/gallery/twoplayer.gif differ
diff --git a/docs/play.md b/docs/play.md
index ae9d6b81..b1c076ba 100644
--- a/docs/play.md
+++ b/docs/play.md
@@ -816,16 +816,16 @@ Google's brick breaker: seven bricks across in blue, red, yellow and green, wall
## 2 Players
-
+
| Key | Player 1 | Player 2 |
| --- | --- | --- |
| **Move** (in Pong, up and down) | Arrows | **×** up, **Ans** down, **3** left, **−** right |
-| **Fire** (Tanks) | **shift** | **EXE** |
+| **Fire, draw, pull** | **OK** | **EXE** |
-On the menu, **Up / Down** pick a game and **Left / Right** who player 2 is: a friend, or the computer (easy, normal, hard). Against the computer, **OK** and **EXE** fire too. **Back** pauses (Resume, Restart, Other games, Quit game).
+On the menu, **Up / Down** pick a game and **Left / Right** who player 2 is: a friend, or the computer (easy, normal, hard). Against the computer, **EXE** works as OK too. **Back** pauses (Resume, Restart, Other games, Quit game).
-Pong (first to 11), Air Hockey (first to 7), Tanks in a new maze every round and Light Cycles (first to 5 rounds). Before each game, a map of the keyboard lights up each player's keys. The matches each side wins are kept for every game.
+Pong (first to 11), Air Hockey (first to 7), Tanks in a new maze every round, Light Cycles (first to 5 rounds), Quick Draw (draw first when the signal shows) and Tug of War (tap faster than the other side). Before each game, a map of the keyboard lights up each player's keys. The matches each side wins are kept for every game.
diff --git a/games/twoplayer/Makefile b/games/twoplayer/Makefile
index 2c6a58d2..c378a398 100644
--- a/games/twoplayer/Makefile
+++ b/games/twoplayer/Makefile
@@ -1,6 +1,7 @@
-# 2 Players — Pong, Air Hockey, Tanks, Light Cycles and Quick Draw, for two on one calculator.
+# 2 Players: Pong, Air Hockey, Tanks, Light Cycles, Quick Draw and Tug of War, for two on one calculator.
# make build output/twoplayer.nwa
# make check link the app like the calculator does (output/twoplayer.bin)
+# make test the tests on a computer (tests/play.c)
#
# In NumPlay the game draws with the launcher's strips, shapes and fonts and saves through its
# file functions (module.o leaves them to the launcher); on its own it brings them along.
@@ -21,7 +22,7 @@ LDFLAGS = -Wl,--relocatable -nostartfiles --specs=nano.specs
LDFLAGS += -Wl,-e,main -Wl,-u,eadk_app_name -Wl,-u,eadk_app_icon -Wl,-u,eadk_api_level
LDFLAGS += -Wl,--gc-sections -flinker-output=nolto-rel
-.PHONY: build check clean
+.PHONY: build check clean host test
build: $(BUILD_DIR)/twoplayer.nwa
check: $(BUILD_DIR)/twoplayer.nwa
@@ -45,6 +46,18 @@ $(BUILD_DIR)/twoplayer.nwa: $(BUILD_DIR)/main.o $(BUILD_DIR)/gfx.o $(BUILD_DIR)/
$(Q) $(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@
$(Q) arm-none-eabi-strip --strip-unneeded $@
+# on a computer, with the sanitizers: the rules, bots against bots, random sessions, old saves (tests/play.c)
+HOST_FLAGS = -std=gnu11 -O1 -g -Wall -Wextra -Wno-unused-parameter -DPLATFORM_DEVICE=1 -DHOST -ffp-contract=off \
+ $(shell $(NWLINK) eadk-cflags-simulator) -fsanitize=address,undefined -fno-sanitize-recover=undefined
+host: output/play
+output/play: $(SRC) tests/play.c $(HEADERS)
+ $(Q) mkdir -p output
+ $(Q) cc $(HOST_FLAGS) tests/play.c ../../launcher/src/gfx.c ../../launcher/src/font_data.c \
+ ../../launcher/src/xfont.c -lm -o $@
+ @echo "HOST $@"
+test: output/play
+ output/play test
+
$(BUILD_DIR)/icon.o: src/icon.png | $(BUILD_DIR)
@echo "ICON $<"
$(Q) $(NWLINK) png-icon-o $< $@
diff --git a/games/twoplayer/README.md b/games/twoplayer/README.md
index 8e81d23e..37bd77ef 100644
--- a/games/twoplayer/README.md
+++ b/games/twoplayer/README.md
@@ -1,15 +1,17 @@
# 2 Players
-Pong and three more games for two people on one calculator, side by side: player 1 on the arrows, player 2 on the keys around +, both at once. No one to play with? Player 2 can be the computer, easy, normal or hard.
+Six games for two people on one calculator, side by side: player 1 on the arrows and OK, player 2 on the keys around + and EXE, both at once. No one to play with? Player 2 can be the computer, easy, normal or hard.
-
+
- **Pong**, the 1972 arcade game: black and white, the dashed net and the blocky scores. The paddle has eight segments: the two in the middle send the ball back straight, the others at steeper and steeper angles. The ball speeds up as a rally goes on, and the first to 11 wins.
- **Air Hockey**: a mallet each, in your own half, and a puck that slides and bounces off the rails and the goal posts. Hit it while you move to send it flying. First to 7.
- **Tanks**: a new maze every round, shells that bounce off the walls (five each at a time) and can hit the tank that fired them. First to 5 rounds.
- **Light Cycles**: never stop, never turn back, and make the other cycle crash into a wall or a trail. Two turns can wait in line, and the cycles speed up as the round goes on. First to 5 rounds.
+- **Quick Draw**: wait for DRAW!, then press your key before the other one. Press too soon and you lose the round. First to 5.
+- **Tug of War**: tap your key as fast as you can to pull the flag past your line. The rope slides back a little, and after 30 seconds the one ahead takes the round. First to 5.
- **Before each game**, a map of the calculator's keyboard lights up each player's keys in their colour, with the rules.
-- **The computer** plays every game: in Pong it works out where the ball will come and aims at the corners, in Air Hockey it gets behind the puck to shoot or guards its goal, its tank finds its way through the maze and plans shots off the walls, and its light cycle keeps the most room for itself and, on hard, cuts you off.
+- **The computer** plays at a player's pace: it reacts after a moment, misjudges a little and moves slower than you, more so on easy. In Pong it works out where the ball will come, in Air Hockey it gets behind the puck to shoot or guards its goal, its tank finds its way through the maze and looks for a shot, and its light cycle keeps the most room for itself.
- **The matches won** by each side are kept for every game, against a friend and against the computer.
## Controls
@@ -17,23 +19,25 @@ Pong and three more games for two people on one calculator, side by side: player
| | Player 1 | Player 2 |
| --- | --- | --- |
| Move (in Pong, up and down) | Arrows | × up, Ans down, 3 left, − right |
-| Fire (Tanks) | shift | EXE |
+| Fire, draw, pull | OK | EXE |
| Key | Action |
| --- | --- |
| Up, Down, OK | Pick a game, then OK to play |
| Left, Right | Player 2, or the computer: easy, normal, hard |
| Back | Pause: Resume, Restart, Other games, Quit game |
-| Home | Quit |
+| Home | Save and quit |
-Against the computer, OK and EXE fire too.
+Against the computer, EXE works as OK too.
## Credits
**Pong** is Atari's arcade game (1972, by Allan Alcorn), whose paddles and ball this one follows. Air hockey, tanks duelling in a maze in the spirit of Tank Trouble, and the light cycles of the film Tron (1982) are classics of two-player games. Everything here is written in C for NumPlay and drawn from shapes as it goes, with no code or art from any of them.
-Inside NumPlay it draws with the launcher's own shapes and fonts and saves through its file functions, so the four games take about 13 KB.
+**Quick Draw** and **Tug of War** are by [Bobydeluxe](https://github.com/Bobydeluxe), in the spirit of the short duels of 12 MiniBattles and GoBattle 2, with no code or art from them. Bobydeluxe also put player 1's key on OK and made the computer fairer.
+
+Inside NumPlay it draws with the launcher's own shapes and fonts and saves through its file functions, so the six games take about 15 KB.
It comes inside [NumPlay](https://github.com/Mason363/NumPlay), or on its own as `TwoPlayers.nwa` from the [latest release](https://github.com/Mason363/NumPlay/releases/latest).
-Build: `make` (needs `arm-none-eabi-gcc` and Node.js for nwlink). `tools/bot.py` plays it against the computer in the emulator for these pictures.
+Build: `make` (needs `arm-none-eabi-gcc` and Node.js for nwlink). `make test` plays every game on a computer, bots against bots. `tools/bot.py` plays it against the computer in the emulator.
diff --git a/games/twoplayer/docs/games.png b/games/twoplayer/docs/games.png
index 0c8c9b65..ad6c71ea 100644
Binary files a/games/twoplayer/docs/games.png and b/games/twoplayer/docs/games.png differ
diff --git a/games/twoplayer/lang/fr.txt b/games/twoplayer/lang/fr.txt
index 6bf2f85b..b9f4b40f 100644
--- a/games/twoplayer/lang/fr.txt
+++ b/games/twoplayer/lang/fr.txt
@@ -3,31 +3,64 @@ Pong Pong
Air Hockey Air hockey
Tanks Tanks
Light Cycles Motos de lumière
+Quick Draw Duel éclair
+Tug of War Tir à la corde
+Volleyball Volley
+Sumo Sumo
+Rocket Race Course de fusées
+Archery Tir à l'arc
+Dodgeball Balle aux prisonniers
+Football Football
+Meteor Run Pluie de météores
+Fishing Pêche
+Memory Duel Duel de mémoire
Return the ball with your paddle: its middle sends the ball back straight, its ends at a steep angle. The longer a rally lasts, the faster the ball. Renvoie la balle avec ta raquette : son milieu la renvoie tout droit, ses bouts en biais. Plus l'échange dure, plus la balle va vite.
Knock the puck into the other goal. Your mallet stays in your half; hit the puck on the move to send it flying. Envoie le palet dans l'autre but. Ton maillet reste dans ta moitié ; frappe le palet en bougeant pour qu'il file.
Drive through the maze and hit the other tank. Shells bounce off the walls: watch out for your own! Roule dans le labyrinthe et touche l'autre tank. Les obus rebondissent sur les murs : gare aux tiens !
Steer your light cycle and make the other one crash into a wall or a trail. It never stops and never turns back. Pilote ta moto de lumière et fais percuter l'autre contre un mur ou une traînée. Elle ne s'arrête et ne recule jamais.
+Wait for DRAW!, then press your key: the first one wins. Too soon loses the round. Attends FEU !, puis appuie sur ta touche : le premier gagne. Trop tôt, tu perds la manche.
+Tap your key fast to pull the rope. Pull the flag past your line. Appuie vite sur ta touche pour tirer la corde. Fais passer le drapeau derrière ta ligne.
+Jump with up or your key. Hit the ball over the net and don't let it land on your side. Saute avec haut ou ta touche. Renvoie la balle par-dessus le filet sans la laisser tomber de ton côté.
+Push the other one out of the ring. Your key dashes. The ring shrinks. Pousse l'autre hors du cercle. Ta touche fonce. Le cercle rétrécit.
+Tap up or your key to fly through the gaps. Don't touch the walls or the edges. Appuie sur haut ou ta touche pour passer dans les trous. Ne touche ni les murs ni les bords.
+Up and down aim. Hold your key to draw the bow, let go to shoot. Three hits win. Haut et bas visent. Maintiens ta touche pour bander l'arc, relâche pour tirer. Trois touches gagnent.
+Your key throws at the other one. Dodge their balls. Three hits win. Ta touche lance sur l'autre. Évite ses balles. Trois touches gagnent.
+Push the ball into the other goal. Near it, your key kicks it at the goal. Pousse le ballon dans l'autre but. Tout près, ta touche le tire vers le but.
+Dodge the falling rocks. Both lanes get the same ones. Last one standing wins. Évite les rochers qui tombent : les mêmes dans les deux couloirs. Le dernier debout gagne.
+Hold your key to lower the hook, let go to reel in. Land a fish first. Maintiens ta touche pour descendre l'hameçon, relâche pour remonter. Attrape un poisson le premier.
+Watch the arrows, then repeat them. First to finish without a mistake wins. Regarde les flèches, puis refais-les. Le premier à finir sans erreur gagne.
Computer IA
Player 2 Joueur 2
You Toi
Player 1 Joueur 1
+Back pause Retour pause
+DRAW! FEU !
+Wait... Attends...
+Your turn! À toi !
+Watch Regarde
Computer: easy IA : facile
Computer: normal IA : normale
Computer: hard IA : difficile
2 Players 2 joueurs
+OK play Back quit OK jouer Retour quitter
+arrows flèches
First to Premier à
Play Jouer
+Back games Retour jeux
Draw! Égalité !
Resume Reprendre
Restart Recommencer
Other games Autres jeux
Quit game Quitter
Paused Pause
+OK choose Back resume OK choisir Retour reprendre
Player 1 wins! Joueur 1 gagne !
Player 2 wins! Joueur 2 gagne !
You win! Gagné !
The computer wins L'IA gagne
Rematch Revanche
+OK choose Back games OK choisir Retour jeux
No Non
Yes Oui
Quit game? Quitter ?
+OK choose Back no OK choisir Retour non
diff --git a/games/twoplayer/lang/zh.txt b/games/twoplayer/lang/zh.txt
index 7f16e822..67658497 100644
--- a/games/twoplayer/lang/zh.txt
+++ b/games/twoplayer/lang/zh.txt
@@ -3,31 +3,64 @@ Pong 乒乓
Air Hockey 桌上冰球
Tanks 坦克大战
Light Cycles 光速摩托
+Quick Draw 快枪对决
+Tug of War 拔河
+Volleyball 排球
+Sumo 相扑
+Rocket Race 火箭穿越
+Archery 射箭
+Dodgeball 躲避球
+Football 足球
+Meteor Run 流星雨
+Fishing 钓鱼
+Memory Duel 箭头对决
Return the ball with your paddle: its middle sends the ball back straight, its ends at a steep angle. The longer a rally lasts, the faster the ball. 用球拍把球打回去:拍子中间把球直直打回,两端打出斜角。来回越久,球越快。
Knock the puck into the other goal. Your mallet stays in your half; hit the puck on the move to send it flying. 把冰球打进对方球门。你的球槌只能在自己半场;边移动边击球,球就会飞出去。
Drive through the maze and hit the other tank. Shells bounce off the walls: watch out for your own! 在迷宫里开坦克,击中对方。炮弹会在墙上反弹:小心别打到自己!
Steer your light cycle and make the other one crash into a wall or a trail. It never stops and never turns back. 驾驶你的光速摩托,让对方撞上墙壁或光轨。摩托从不停下,也不能掉头。
+Wait for DRAW!, then press your key: the first one wins. Too soon loses the round. 等到出现「拔枪!」再按你的键,先按的获胜。按太快就输了这一局。
+Tap your key fast to pull the rope. Pull the flag past your line. 快速连按你的键,把旗子拉过你的线。
+Jump with up or your key. Hit the ball over the net and don't let it land on your side. 用上或你的键跳起。把球打过网,别让它落在你这边。
+Push the other one out of the ring. Your key dashes. The ring shrinks. 把对方推出场地。你的键是冲撞。场地会变小。
+Tap up or your key to fly through the gaps. Don't touch the walls or the edges. 按上或你的键飞过缺口。别碰到墙或上下边缘。
+Up and down aim. Hold your key to draw the bow, let go to shoot. Three hits win. 上下调整角度。按住你的键拉弓,松开射箭。命中三次获胜。
+Your key throws at the other one. Dodge their balls. Three hits win. 你的键向对方投球。躲开对方的球。命中三次获胜。
+Push the ball into the other goal. Near it, your key kicks it at the goal. 把球推进对方球门。靠近时,你的键把球踢向球门。
+Dodge the falling rocks. Both lanes get the same ones. Last one standing wins. 躲开落下的石头。两边落下的石头相同。最后留下的获胜。
+Hold your key to lower the hook, let go to reel in. Land a fish first. 按住你的键放下鱼钩,松开收线。先钓到鱼的获胜。
+Watch the arrows, then repeat them. First to finish without a mistake wins. 看好箭头,再照着按一遍。先全部按对的获胜,按错就输。
Computer 电脑
Player 2 玩家 2
You 你
Player 1 玩家 1
+Back pause 返回 暂停
+DRAW! 拔枪!
+Wait... 等待…
+Your turn! 轮到你了!
+Watch 看好
Computer: easy 电脑:简单
Computer: normal 电脑:普通
Computer: hard 电脑:困难
2 Players 双人对战
+OK play Back quit OK 开始 返回 退出
+arrows 方向键
First to 先得
Play 开始
+Back games 返回 游戏
Draw! 平局!
Resume 继续
Restart 重新开始
Other games 其他游戏
Quit game 退出游戏
Paused 暂停
+OK choose Back resume OK 选择 返回 继续
Player 1 wins! 玩家 1 获胜!
Player 2 wins! 玩家 2 获胜!
You win! 你赢了!
The computer wins 电脑获胜
Rematch 再来一局
+OK choose Back games OK 选择 返回 游戏
No 否
Yes 是
Quit game? 退出游戏?
+OK choose Back no OK 选择 返回 否
diff --git a/games/twoplayer/src/main.c b/games/twoplayer/src/main.c
index 66cd8ec5..2a5d5310 100644
--- a/games/twoplayer/src/main.c
+++ b/games/twoplayer/src/main.c
@@ -1,15 +1,15 @@
-/* 2 Players: Pong and three more games for two people on one calculator.
+/* 2 Players: Pong and more games for two people on one calculator.
*
- * Player 1 holds the left of the keyboard (the arrows, and shift to fire), player 2 the right
- * (the keys around +, and EXE to fire), both at once; or player 2 is the computer, easy, normal
- * or hard. Pong plays as the 1972 arcade game does; then Air Hockey, Tanks in a maze with
- * shells that bounce off the walls, and Light Cycles. A match is first to a few points, and the
+ * Player 1 holds the left of the keyboard (the arrows, and OK to act), player 2 the right (the keys
+ * around +, and EXE to act), both at once; or player 2 is the computer, easy, normal or hard. Pong
+ * plays as the 1972 arcade game does; then Air Hockey, Tanks in a maze with shells that bounce off
+ * the walls, Light Cycles, and Bobydeluxe's party games. A match is first to a few points, and the
* matches each side has won are kept for every game.
*
* Inside NumPlay it draws with the launcher's strips, shapes and fonts (launcher/src/gfx.c) and
* saves through its file functions, like Block Blast; on its own (TwoPlayers.nwa) it brings them
* along. Positions are in 256ths of a pixel and time in milliseconds, but for the round things
- * of Air Hockey. */
+ * of Air Hockey and the party games, in pixels and seconds. */
#include
#include "../../../launcher/src/gfx.h"
#include "../../common/np_text.h"
@@ -33,17 +33,24 @@ void np_push(int x, int y, int w, int h, const color_t *px) {
#define INK HEX(0x161A33) /* the menus' night blue */
#define GOLD HEX(0xFFC93C)
-enum { PONG, HOCKEY, TANKS, CYCLES, NG };
-static const char *const NAME[NG] = {T("Pong"), T("Air Hockey"), T("Tanks"), T("Light Cycles")};
-static const uint8_t TARGET[NG] = {11, 7, 5, 5}; /* first to */
+/* (in the menu's order, and the saves': a new game goes last) */
+enum { PONG, HOCKEY, TANKS, CYCLES, QUICK, TUG, NG };
+static const char *const NAME[NG] = {T("Pong"), T("Air Hockey"), T("Tanks"), T("Light Cycles"), T("Quick Draw"),
+ T("Tug of War")};
+static const uint8_t TARGET[NG] = {11, 7, 5, 5, 5, 5}; /* first to */
+/* the keys each game uses, for its keyboard: left 1, up 2, down 4, right 8, the action 16 */
+static const uint8_t KEYS_USED[NG] = {6, 15, 31, 15, 16, 16};
static const char *const RULES[NG] = {
T("Return the ball with your paddle: its middle sends the ball back straight, its ends at a steep angle. The longer a rally lasts, the faster the ball."),
T("Knock the puck into the other goal. Your mallet stays in your half; hit the puck on the move to send it flying."),
T("Drive through the maze and hit the other tank. Shells bounce off the walls: watch out for your own!"),
T("Steer your light cycle and make the other one crash into a wall or a trail. It never stops and never turns back."),
+ T("Wait for DRAW!, then press your key: the first one wins. Too soon loses the round."),
+ T("Tap your key fast to pull the rope. Pull the flag past your line."),
};
-/* twoplayer.sav: the game last chosen, who player 2 is, and the matches won */
+/* twoplayer.sav: the game last chosen, who player 2 is, and the matches won (version 1 had four
+ games: a shorter file of the same layout, read as it is) */
static struct {
uint8_t magic, version, game, cpu; /* cpu: 0 a second player, 1 to 3 the computer, easy to hard */
uint16_t wins[NG][2][2]; /* [game][against the computer][player 1, player 2] */
@@ -54,12 +61,13 @@ static color_t strip[SCREEN_W * STRIP_H];
static color_t team[2], light[2], dark[2]; /* player 1 red, player 2 blue */
static uint32_t now, seed, phase_t; /* now: the game's clock, stopped while paused */
static int ms; /* this frame's milliseconds */
+static float dt; /* ...in seconds */
static int game, cpu, score[2], phase, winner; /* phase: 0 playing, 1 a point just won, 2 the match over */
static int pad[2], tap[2]; /* each player's keys held, and just pressed */
enum { B_LEFT = 1, B_UP = 2, B_DOWN = 4, B_RIGHT = 8, B_FIRE = 16 };
-/* the keys: left, up, down, right, fire */
+/* the keys: left, up, down, right, action */
static const uint8_t KEYS[2][5] = {
- {eadk_key_left, eadk_key_up, eadk_key_down, eadk_key_right, eadk_key_shift},
+ {eadk_key_left, eadk_key_up, eadk_key_down, eadk_key_right, eadk_key_ok},
{eadk_key_three, eadk_key_multiplication, eadk_key_ans, eadk_key_minus, eadk_key_exe}};
static uint32_t rnd(void) {
@@ -72,10 +80,12 @@ static int clampi(int v, int a, int b) { return v < a ? a : v > b ? b : v; }
static float clampf(float v, float a, float b) { return v < a ? a : v > b ? b : v; }
static float len(float x, float y) { return __builtin_sqrtf(x * x + y * y); }
static uint32_t ago(uint32_t t) { return now - t; }
+static bool due(uint32_t t) { return (int32_t)(now - t) >= 0; }
static bool down(uint64_t keys, int k) { return (uint32_t)(keys >> (k & 32)) >> (k & 31) & 1; }
static bool is_cpu(int p) { return p && cpu; }
static int dirx(int p) { return !!(pad[p] & B_RIGHT) - !!(pad[p] & B_LEFT); }
static int diry(int p) { return !!(pad[p] & B_DOWN) - !!(pad[p] & B_UP); }
+static int acted(int p) { return tap[p] >> 4 & 1; } /* the action key just pressed */
/* sine of a 64th of a turn, times 127 */
static const int8_t SINQ[17] = {0, 12, 25, 37, 49, 60, 71, 81, 90, 98, 106, 112, 117, 122, 125, 126, 127};
static int sin64(int a) {
@@ -83,6 +93,18 @@ static int sin64(int a) {
return a & 32 ? -v : v;
}
static int cos64(int a) { return sin64(a + 16); }
+/* The computer sees and thinks at a human's pace: it decides again every so often, not every
+ frame, and holds its keys in between. */
+static struct {
+ uint32_t next;
+ float x, y;
+} bot;
+static bool bot_due(void) {
+ static const uint16_t EVERY[4] = {0, 340, 240, 160};
+ if (!due(bot.next)) return false;
+ bot.next = now + EVERY[cpu] + rn(EVERY[cpu] / 2);
+ return true;
+}
static char *num(char *o, uint32_t v) { /* v in o, then the end of it */
char t[10];
@@ -130,19 +152,24 @@ static void pong_start(void) { /* the ball is served towards the one who lost th
int to = winner < 2 ? 1 - winner : rn(2);
hits = aim = 0;
bx = (160 - BS / 2) << 8, by = (70 + rn(100)) << 8, bvx = to ? 1 : -1, bvy = rn(9) - 4;
- serve_at = now + 900;
+ serve_at = now + 900, bot.y = 120 << 8;
if (!score[0] && !score[1]) py[0] = py[1] = (120 - PH / 2) << 8;
}
-/* the computer's paddle (player 2): to where the ball will come, off centre by its aim */
+/* the computer's paddle (player 2): to where it sees the ball will come, off centre by its aim
+ and by how much it misjudges, slower than a player's */
static int pong_cpu(void) {
- static const uint8_t SPEED[4] = {0, 32, 46, 64};
- int to = 120 << 8, v = SPEED[cpu];
- if (bvx > 0 && !serve_at && bx > (cpu == 1 ? 170 << 8 : cpu == 2 ? 100 << 8 : 0)) {
- int span = (PBOT - BS - PTOP) << 8, y = by - (PTOP << 8) + bvy * (((PX[1] - BS) << 8) - bx) / bvx;
- for (int i = 0; i < 8 && (y < 0 || y > span); i++) y = y < 0 ? -y : 2 * span - y;
- to = y + ((PTOP + BS / 2) << 8) - aim;
+ static const uint8_t SPEED[4] = {0, 20, 30, 40}, MISS[4] = {0, 20, 12, 7};
+ static const int16_t SEES[4] = {0, 190, 145, 90}; /* where the ball must be before it looks */
+ if (bot_due()) {
+ int to = 120 << 8;
+ if (bvx > 0 && !serve_at && bx > SEES[cpu] << 8) {
+ int span = (PBOT - BS - PTOP) << 8, y = by - (PTOP << 8) + bvy * (((PX[1] - BS) << 8) - bx) / bvx;
+ for (int i = 0; i < 8 && (y < 0 || y > span); i++) y = y < 0 ? -y : 2 * span - y;
+ to = y + ((PTOP + BS / 2) << 8) - aim;
+ }
+ bot.y = (float)(to + (rn(2 * MISS[cpu] + 1) - MISS[cpu]) * 256);
}
- return clampi((to - py[1] - (PH << 7)) / 32, -v, v);
+ return clampi(((int)bot.y - py[1] - (PH << 7)) / 40, -SPEED[cpu], SPEED[cpu]);
}
static void pong_step(void) {
for (int p = 0; p < 2; p++)
@@ -158,7 +185,10 @@ static void pong_step(void) {
bvx = p ? -s : s, bvy = SEG[clampi((y + BS - top) * 8 / (PH + BS), 0, 7)] * s / 10;
bx = (p ? PX[p] - BS : PX[p] + PW) << 8;
/* where the computer means to take the next one: off centre, by how much it misjudges */
- if (!p) aim = cpu == 3 ? (rn(2) ? 2816 : -2816) + rn(1536) - 768 : (rn(512) - 256) * (cpu == 1 ? 25 : 14);
+ if (!p) {
+ aim = rn(1536) - 768;
+ aim = cpu == 3 ? aim + (rn(2) ? 2816 : -2816) : aim / 3 * (cpu == 1 ? 25 : 14);
+ }
}
if (x < -BS - 10 || x > SCREEN_W + 10) {
point(x < 0);
@@ -176,6 +206,7 @@ static void pong_paint(void) {
gfx_fill(0, PTOP - 4, SCREEN_W, 4, WHITE);
gfx_fill(0, PBOT, SCREEN_W, 4, WHITE);
for (int y = PTOP + 4; y < PBOT; y += 16) gfx_fill(158, y, 4, 8, WHITE);
+ gfx_text_center(&np_font_small, 80, 227, T("Back pause"), HEX(0x6E6E6E), 256); /* (under the paddles and the ball) */
for (int p = 0; p < 2; p++) {
gfx_fill(PX[p], py[p] >> 8, PW, PH, WHITE);
for (int v = score[p], n = v > 9, d = n; d >= 0; d--, v /= 10)
@@ -201,6 +232,7 @@ static void hockey_start(void) { /* the puck to the one who was scored on */
int to = winner < 2 ? 1 - winner : rn(2);
for (int p = 0; p < 2; p++) mx[p] = p ? RX1 - 40 : RX0 + 40, my[p] = RYM, mvx[p] = mvy[p] = 0;
qx = to ? 220 : 100, qy = RYM, qvx = qvy = 0, qaim = (float)(rn(44) - 22); /* where the computer aims */
+ bot.x = RX1 - 40, bot.y = RYM;
}
/* the puck off something round at (x, y) moving at (vx, vy) */
static void knock(float x, float y, float vx, float vy, float r) {
@@ -211,23 +243,31 @@ static void knock(float x, float y, float vx, float vy, float r) {
float vn = (qvx - vx) * dx + (qvy - vy) * dy;
if (vn < 0) qvx -= 1.85f * vn * dx, qvy -= 1.85f * vn * dy;
}
+/* the computer's mallet (player 2): behind the puck to shoot it at the other goal, or guarding
+ its own, where it saw them last; its speed (tx, ty) */
+static void hockey_cpu(float *tx, float *ty) {
+ static const float V[4] = {0, 90, 130, 175};
+ static const uint8_t MISS[4] = {0, 18, 11, 6};
+ if (bot_due()) {
+ float x = RX1 - 36, y = RYM + (qy - RYM) / 2;
+ if (qx > 160 - QR && !phase) {
+ float ax = RX0 - qx, ay = RYM + qaim - qy, l = len(ax, ay);
+ ax /= l, ay /= l;
+ x = qx - ax * (MR + QR + 3), y = qy - ay * (MR + QR + 3);
+ if (mx[1] < qx + 2) x = qx + MR + QR + 10, y = qy + (qy > RYM ? -28 : 28); /* in front of it: go round */
+ else if (len(x - mx[1], y - my[1]) < 7) x = qx + ax * 24, y = qy + ay * 24; /* strike */
+ if (x > RX1 - MR || y < RY0 + MR || y > RY1 - MR) x = qx, y = qy; /* it can't get behind it: at it */
+ }
+ bot.x = x, bot.y = y + (float)(rn(2 * MISS[cpu] + 1) - MISS[cpu]);
+ }
+ float x = bot.x - mx[1], y = bot.y - my[1], l = len(x, y) + 0.01f, v = V[cpu] * (l < 16 ? l / 16 : 1) / l;
+ *tx = x * v, *ty = y * v;
+}
static void hockey_step(void) {
- static const float V[4] = {0, 120, 175, 235};
for (int p = 0; p < 2; p++) {
float tx, ty, k = clampf(ms * 0.014f, 0, 1);
- if (is_cpu(p)) { /* behind the puck to shoot it at the other goal, or guarding its own */
- tx = RX1 - 36, ty = RYM + (qy - RYM) / 2;
- if (qx > 160 - QR && !phase) {
- float ax = RX0 - qx, ay = RYM + qaim - qy, l = len(ax, ay);
- ax /= l, ay /= l;
- tx = qx - ax * (MR + QR + 3), ty = qy - ay * (MR + QR + 3);
- if (mx[1] < qx + 2) tx = qx + MR + QR + 10, ty = qy + (qy > RYM ? -28 : 28); /* in front of it: go round */
- else if (len(tx - mx[1], ty - my[1]) < 7) tx = qx + ax * 24, ty = qy + ay * 24; /* strike */
- }
- tx -= mx[1], ty -= my[1];
- float l = len(tx, ty) + 0.01f, v = V[cpu] * (l < 16 ? l / 16 : 1) / l;
- tx *= v, ty *= v;
- } else {
+ if (is_cpu(p)) hockey_cpu(&tx, &ty);
+ else {
int x = dirx(p), y = diry(p);
float s = x && y ? 175 : 245;
tx = x * s, ty = y * s;
@@ -239,6 +279,8 @@ static void hockey_step(void) {
if (phase) return;
for (int i = 0; i < ms; i++) {
qx += qvx / 1000, qy += qvy / 1000;
+ for (int k = 0; k < 4; k++) knock(k & 1 ? RX1 : RX0, k & 2 ? GY1 : GY0, 0, 0, 0); /* the goal posts */
+ for (int p = 0; p < 2; p++) knock(mx[p], my[p], mvx[p], mvy[p], MR);
/* the boards: back on their edge, and off them only when moving into them (a mallet can push the
puck into a board, and a slow puck flipped back and forth there stayed in it) */
if (qy < RY0 + QR) qy = RY0 + QR, qvy = qvy < 0 ? -qvy * 0.9f : qvy;
@@ -251,8 +293,10 @@ static void hockey_step(void) {
point(qx < 160);
return;
}
- for (int k = 0; k < 4; k++) knock(k & 1 ? RX1 : RX0, k & 2 ? GY1 : GY0, 0, 0, 0); /* the goal posts */
- for (int p = 0; p < 2; p++) knock(mx[p], my[p], mvx[p], mvy[p], MR);
+ for (int p = 0; p < 2; p++) { /* and a mallet pressing it into a corner gives way */
+ float dx = mx[p] - qx, dy = my[p] - qy, d = len(dx, dy) + .01f;
+ if (d < MR + QR) mx[p] = qx + dx / d * (MR + QR), my[p] = qy + dy / d * (MR + QR);
+ }
}
float s = len(qvx, qvy), f = 1 - 0.00035f * ms;
if (s * f > 560) f = 560 / s;
@@ -390,17 +434,17 @@ static bool shot_finds(int a, int b) {
}
return false;
}
-/* the computer's tank: a shot when there is one to take, else the way to the player, cell by
- cell through the maze (found breadth first from the player) */
+/* the computer's tank: a shot when it sees one, a little off, else the way to the player, cell
+ by cell through the maze (found breadth first from the player) */
static void tanks_cpu(int *v, int *turn) {
- static const int16_t RELOAD[4] = {0, 1500, 950, 550};
- static const int8_t BOUNCES[4] = {0, 0, 1, 3};
+ static const int16_t RELOAD[4] = {0, 2300, 1700, 1200}, LOOK[4] = {0, 700, 500, 350};
+ static const int8_t BOUNCES[4] = {0, 0, 0, 1};
int dir = ta[1] >> 8, want = dir;
- if (ago(ai_t) > 300) {
+ if (ago(ai_t) > (uint32_t)LOOK[cpu]) {
ai_t = now, ai_aim = -1;
for (int i = 0; i < 64 && ai_aim < 0; i++) { /* the nearest to where it faces first */
int a = (dir + (i & 1 ? 1 : -1) * ((i + 1) / 2)) & 63;
- if (shot_finds(a, BOUNCES[cpu])) ai_aim = (a + (cpu == 1 ? rn(5) - 2 : 0)) & 63;
+ if (shot_finds(a, BOUNCES[cpu])) ai_aim = (a + rn(9 - 2 * cpu) - (4 - cpu)) & 63;
}
}
if (ai_aim >= 0) {
@@ -422,10 +466,10 @@ static void tanks_cpu(int *v, int *turn) {
int d = (gx >> 8) * cos64(a) + (gy >> 8) * sin64(a);
if (d > best) best = d, want = a;
}
- *v = clampi(6 - (((want - dir + 32) & 63) - 32) * (((want - dir + 32) & 63) - 32), 0, 1) * 70;
+ *v = clampi(6 - (((want - dir + 32) & 63) - 32) * (((want - dir + 32) & 63) - 32), 0, 1) * (36 + 9 * cpu);
}
int d = ((want - dir + 32) & 63) - 32;
- *turn = d > 0 ? 8 : d < 0 ? -8 : 0;
+ *turn = d > 0 ? 4 + cpu : d < 0 ? -4 - cpu : 0;
}
static void tanks_step(void) {
for (int p = 0; p < 2; p++) {
@@ -434,7 +478,7 @@ static void tanks_step(void) {
if (is_cpu(p)) tanks_cpu(&v, &turn);
else {
v = diry(p) < 0 ? 70 : diry(p) * -50, turn = dirx(p) * 8;
- if (tap[p] & B_FIRE) fire(p);
+ if (acted(p)) fire(p);
}
ta[p] = (ta[p] + turn * ms) & 0x3FFF; /* 8 a ms: half a turn a second */
int x = tx_[p], y = ty_[p], dx = cos64(ta[p] >> 8) * v * ms / 496, dy = sin64(ta[p] >> 8) * v * ms / 496;
@@ -535,25 +579,19 @@ static int room(int x, int y, int cap) {
}
return n;
}
-/* the computer's way: the turn that leaves it the most room; hard, it counts the cells the
- player could take next as taken, and with room to spare it goes to cut the player off */
+/* the computer's way, at a human's pace: the turn that leaves it the most room and the longest
+ way ahead, without guessing the player's next move */
static int cycles_cpu(void) {
- static const int16_t CAP[4] = {0, 30, 300, 900};
+ static const int16_t CAP[4] = {0, 12, 70, 220};
+ if (!bot_due()) return hd[1];
int best = hd[1], bv = -(1 << 30);
- uint8_t *taken[3] = {0};
- for (int d = 0; d < 3 && cpu == 3; d++) {
- int x = hx[0] + DX[(hd[0] + d + 3) & 3], y = hy[0] + DY[(hd[0] + d + 3) & 3];
- if (free_at(x, y)) taken[d] = &cell[y][x], cell[y][x] = 3;
- }
for (int t = 0; t < 3; t++) {
- int d = (hd[1] + t + (t == 2)) & 3, x = hx[1] + DX[d], y = hy[1] + DY[d], r = room(x, y, CAP[cpu]);
- int v = r * 8 + (t ? 0 : 6) + rn(cpu == 1 ? 120 : 6), ex = x - hx[0] - 6 * DX[hd[0]], ey = y - hy[0] - 6 * DY[hd[0]];
- if (cpu == 3 && r >= CAP[3]) v -= (ex < 0 ? -ex : ex) + (ey < 0 ? -ey : ey);
+ int d = (hd[1] + t + (t == 2)) & 3, x = hx[1] + DX[d], y = hy[1] + DY[d], r = room(x, y, CAP[cpu]), a = 0;
+ while (a < 7 && free_at(x + a * DX[d], y + a * DY[d])) a++;
+ int v = r * 8 + a * 48 + (t ? 0 : 6) + rn(cpu == 1 ? 120 : 6);
if (!r) v = -(1 << 29);
if (v > bv) bv = v, best = d;
}
- for (int d = 0; d < 3; d++)
- if (taken[d]) *taken[d] = 0;
return best;
}
static void cycles_step(void) {
@@ -601,22 +639,94 @@ static void cycles_paint(void) {
}
}
-static void (*const START[NG])(void) = {pong_start, hockey_start, tanks_start, cycles_start};
-static void (*const STEP_[NG])(void) = {pong_step, hockey_step, tanks_step, cycles_step};
-static void (*const PAINT[NG])(void) = {pong_paint, hockey_paint, tanks_paint, cycles_paint};
+/* ------------------------------------------------------------ the party games
+ Bobydeluxe's: short rounds, a key each. */
+static struct {
+ float rope; /* Tug of War: -100 (player 1's side) to 100 */
+ uint32_t t0, at, next; /* the round's start, the signal (Quick Draw), the computer's next press */
+} pg;
+static void party(void) { /* a new round */
+ pg.rope = 0, pg.t0 = now, pg.next = now + 600;
+}
+static uint32_t round_ms(void) { return now - pg.t0; }
+static void arena(uint32_t bg) {
+ gfx_fill(0, 22, SCREEN_W, SCREEN_H - 22, INK);
+ box(6, 28, 308, 206, 12, gfx_rgb(bg));
+}
+static void person(int x, int feet, int p) { /* a player seen from the side, its key under it */
+ disc(x, feet, 9, HEX(0x232944));
+ box(x - 7, feet - 19, 14, 17, 5, team[p]);
+ disc(x, feet - 25, 7, light[p]);
+ disc(x + (p ? -3 : 3), feet - 26, 1, INK);
+ if (!is_cpu(p)) gfx_text_center(&np_font_body, clampi(x, 30, 290), 222, p ? "EXE" : "OK", light[p], 256);
+}
+
+/* Quick Draw: the first to press after the signal wins; before it, loses */
+static void quick_start(void) {
+ static const uint16_t REACT[4] = {0, 360, 280, 210};
+ party();
+ pg.at = now + 1500 + rn(2500);
+ if (cpu) pg.next = pg.at + REACT[cpu] + rn(REACT[cpu] / 2);
+}
+static void quick_step(void) {
+ if (phase) return;
+ if (cpu && due(pg.next)) tap[1] = B_FIRE;
+ int a = acted(0), b = acted(1);
+ if (a | b) point(a & b ? 2 : due(pg.at) ? b : a);
+}
+static void quick_paint(void) {
+ bool go = due(pg.at);
+ arena(go ? 0x3A2F1C : 0x262D52);
+ for (int p = 0; p < 2; p++) person(80 + 160 * p, 196, p);
+ say(go ? &np_font_title : &np_font_body, 160, 130, go ? T("DRAW!") : T("Wait..."), go ? GOLD : WHITE);
+}
+
+/* Tug of War: every tap pulls; the rope slides back to the middle a little */
+static void tug_step(void) {
+ static const uint16_t EVERY[4] = {0, 300, 200, 150};
+ if (phase) return;
+ if (cpu && due(pg.next)) tap[1] = B_FIRE, pg.next = now + EVERY[cpu] + rn(EVERY[cpu] / 5);
+ float r = pg.rope + (acted(1) - acted(0)) * 7.5f, d = 3 * dt;
+ pg.rope = r > d ? r - d : r < -d ? r + d : 0;
+ if (pg.rope < -67 || pg.rope > 67 || round_ms() > 30000) point(pg.rope < -1 ? 0 : pg.rope > 1 ? 1 : 2);
+}
+static void tug_paint(void) {
+ int x = 160 + (int)pg.rope;
+ char s[8], *e = num(s, round_ms() < 30000 ? (30999 - round_ms()) / 1000 : 0); /* the seconds left */
+ arena(0x262D52);
+ e[0] = 's', e[1] = 0;
+ say(&np_font_body, 160, 56, s, GOLD);
+ for (int p = 0; p < 2; p++) gfx_fill(p ? 227 : 91, 96, 2, 100, team[p]);
+ gfx_fill(x - 86, 150, 172, 4, HEX(0xC5CBE4));
+ for (int p = 0; p < 2; p++) person(x + (p ? 86 : -86), 186, p);
+ box(x - 6, 140, 12, 20, 3, GOLD);
+}
+
+static void (*const START[NG])(void) = {pong_start, hockey_start, tanks_start, cycles_start, quick_start, party};
+static void (*const STEP_[NG])(void) = {pong_step, hockey_step, tanks_step, cycles_step, quick_step, tug_step};
+static void (*const PAINT[NG])(void) = {pong_paint, hockey_paint, tanks_paint, cycles_paint, quick_paint, tug_paint};
/* ------------------------------------------------------------ screens */
enum { S_MENU, S_RULES, S_PLAY, S_PAUSE, S_ASK, S_OVER };
-static int screen, sel, back_to;
+static int screen, sel, back_to, top; /* top: the first game the menu shows */
+static uint32_t shown; /* when the screen came (the clock's time) */
static void save(void) {
sv.magic = 'P', sv.version = 1, sv.game = (uint8_t)game, sv.cpu = (uint8_t)cpu;
np_file_write(SAVE_NAME, &sv, sizeof sv);
}
static void new_match(void) {
- score[0] = score[1] = 0, phase = 0, winner = 2, screen = S_PLAY;
+ score[0] = score[1] = 0, phase = 0, winner = 2, screen = S_PLAY, bot.next = now + 450;
START[game]();
}
+static void play(void) { /* a frame of the match */
+ STEP_[game]();
+ if (phase == 1 && ago(phase_t) > 1500) {
+ phase = 0;
+ if (score[0] < TARGET[game] && score[1] < TARGET[game]) bot.next = now + 450, START[game]();
+ else phase = 2, screen = S_OVER, sel = 0, sv.wins[game][cpu > 0][score[1] > score[0]]++, save();
+ }
+}
static const char *opponent(void) {
static const char *const O[4] = {T("Player 2"), T("Computer: easy"), T("Computer: normal"), T("Computer: hard")};
return O[cpu];
@@ -630,7 +740,7 @@ static void tri(int x, int y, int d, color_t c) { /* a little arrow, pointing ri
}
}
/* The calculator's keyboard, the keys of this game in each player's colour: arrows on the keys
- that move, a dot on the one that fires. */
+ that move, a dot on the one that acts. */
static void keyboard(int x0, int y0) {
static const int8_t ARROW[4][2] = {{0, 9}, {13, 0}, {13, 18}, {26, 9}}, DIR[4] = {2, 3, 1, 0};
box(x0 - 4, y0 - 4, 132, 136, 10, HEX(0x0E1124));
@@ -644,8 +754,7 @@ static void keyboard(int x0, int y0) {
else x += (k - 30) % 6 * 24, y += 74 + (k - 30) / 6 * 14, w = 20, h = 10;
for (int q = 0; q < 2; q++)
for (int i = 0; i < 5; i++)
- if (KEYS[q][i] == k && (i == 4 ? game == TANKS : game != PONG || i == 1 || i == 2) && !is_cpu(q))
- p = q, b = i;
+ if (KEYS[q][i] == k && KEYS_USED[game] >> i & 1 && !is_cpu(q)) p = q, b = i;
box(x, y, w, h, 3, p < 0 ? HEX(0x3A4268) : team[p]);
if (p >= 0 && b == 4) disc(x + w / 2, y + h / 2, 2, WHITE);
else if (p >= 0) tri(x + w / 2, y + h / 2, DIR[b], WHITE);
@@ -656,19 +765,23 @@ static void button(int x, int y, int w, const char *s, bool on) {
box(x, y, w, 26, 13, on ? GOLD : HEX(0x3A4372));
gfx_text_center(&np_font_body, x + w / 2, y + 18, s, on ? INK : WHITE, 256);
}
-/* a card over the game: its title, then its buttons from by down. Over a game it is drawn in that game's
- look: Pong's black court and white walls with its square ball, the air hockey rink with a mallet, the tanks' grey maze, the light cycles' blue grid; over the menu, in the menu's */
-static void card(int y, int h, const char *title, const char *const *items, int n, int by) {
+static void keys_line(int y, const char *s) { say(&np_font_small, 160, y, s, HEX(0xD6DCF5)); }
+/* a card over the game: its title, then its buttons from by down, and its keys under it. Over
+ the first four games it is drawn in that game's look: Pong's black court and white walls with
+ its square ball, the air hockey rink with a mallet, the tanks' grey maze, the light cycles' blue
+ grid; over the menu and the party games, in the menu's */
+static void card(int y, int h, const char *title, const char *const *items, int n, int by, const char *keys) {
gfx_fill_alpha(0, 0, SCREEN_W, SCREEN_H, 0, 130);
- if (screen <= S_RULES || (screen == S_ASK && back_to == S_MENU)) {
+ keys_line(232, keys);
+ if (screen <= S_RULES || (screen == S_ASK && back_to == S_MENU) || game > CYCLES) {
box(60, y, 200, h, 14, HEX(0x262D52));
gfx_rrect_outline(60, y, 200, h, 14, 2, HEX(0x56619A), 256);
say(&np_font_title, 160, y + 30, title, WHITE);
for (int i = 0; i < n; i++) button(84, by + 32 * i, 152, items[i], sel == i);
return;
}
- static const uint32_t LOOK[NG][3] = {{0x000000, 0xFFFFFF, 0xFFFFFF}, {0xF4F6FA, 0x9AA3B8, 0x161A33},
- {0xE6E6EA, 0x3A3D4F, 0x3A3D4F}, {0x0B1230, 0x3E8BFF, 0xFFFFFF}};
+ static const uint32_t LOOK[4][3] = {{0x000000, 0xFFFFFF, 0xFFFFFF}, {0xF4F6FA, 0x9AA3B8, 0x161A33},
+ {0xE6E6EA, 0x3A3D4F, 0x3A3D4F}, {0x0B1230, 0x3E8BFF, 0xFFFFFF}};
color_t bg = gfx_rgb(LOOK[game][0]), edge = gfx_rgb(LOOK[game][1]), ink = gfx_rgb(LOOK[game][2]);
gfx_fill(57, y - 3, 206, h + 6, edge);
gfx_fill(60, y, 200, h, bg);
@@ -685,16 +798,18 @@ static void card(int y, int h, const char *title, const char *const *items, int
}
}
-/* the little picture of each game in the menu: x, y, w, h, roundness, colour (red, blue, white) */
+/* the little picture of each game in the menu: x, y, w, h, roundness, colour (red, blue, white, gold) */
static const int8_t ICON[][6] = {
{4, 6, 3, 10, 0, 0}, {19, 10, 3, 10, 0, 1}, {12, 11, 3, 3, 0, 2}, /* Pong */
{3, 8, 10, 10, 5, 0}, {14, 6, 10, 10, 5, 1}, {10, 16, 6, 6, 3, 2}, /* Air Hockey */
{0, 0, 0, 0, 0, 0}, /* (a tank) */
{4, 7, 12, 3, 0, 0}, {13, 7, 3, 12, 0, 0}, {10, 19, 12, 3, 0, 1}, {10, 12, 3, 9, 0, 1}, /* Light Cycles */
+ {3, 8, 10, 10, 5, 0}, {13, 8, 10, 10, 5, 1}, {11, 5, 4, 16, 2, 3}, /* Quick Draw */
+ {3, 12, 20, 3, 0, 2}, {11, 8, 5, 11, 2, 3}, /* Tug of War */
};
-static const uint8_t ICON_AT[NG + 1] = {0, 3, 6, 7, 11};
+static const uint8_t ICON_AT[NG + 1] = {0, 3, 6, 7, 11, 14, 16};
static void icon(int g, int x, int y) {
- const color_t C[3] = {team[0], team[1], WHITE};
+ const color_t C[4] = {team[0], team[1], WHITE, GOLD};
box(x, y, 26, 26, 6, INK);
if (g == TANKS) tank(x + 13, y + 14, 54, 0);
else
@@ -702,45 +817,55 @@ static void icon(int g, int x, int y) {
box(x + ICON[i][0], y + ICON[i][1], ICON[i][2], ICON[i][3], ICON[i][4], C[ICON[i][5]]);
}
+#define ROWS 5 /* the games the menu shows at once */
static void scene(void *ctx) {
- char s[16];
+ char s[24];
if (screen <= S_RULES || (screen == S_ASK && back_to == S_MENU)) {
gfx_vgrad(0, 0, 160, SCREEN_H, 0xB8323A, 0x3A0F24); /* the two players' colours */
gfx_vgrad(160, 0, 160, SCREEN_H, 0x2A5FD0, 0x0F1A44);
say(&np_font_title, 160, 28, T("2 Players"), WHITE);
- for (int g = 0; g < NG; g++) {
- int y = 46 + 39 * g, on = g == game;
- gfx_rrect(24, y, 272, 32, 10, on ? WHITE : INK, on ? 256 : 170);
- icon(g, 27, y + 3);
- gfx_text(&np_font_body, 62, y + 21, NAME[g], on ? INK : WHITE, 256);
+ for (int i = 0; i < ROWS; i++) {
+ int g = top + i, y = 38 + 33 * i, on = g == game;
+ gfx_rrect(24, y, 272, 30, 10, on ? WHITE : INK, on ? 256 : 170);
+ icon(g, 26, y + 2);
+ gfx_text(&np_font_body, 62, y + 20, NAME[g], on ? INK : WHITE, 256);
for (int p = 0; p < 2; p++) { /* the matches each side has won */
num(s, sv.wins[g][cpu > 0][p]);
- gfx_text_center(&np_font_body, 248 + 30 * p, y + 21, s, team[p], 256);
+ gfx_text_center(&np_font_body, 248 + 30 * p, y + 20, s, team[p], 256);
}
- gfx_text_center(&np_font_body, 263, y + 21, "-", on ? INK : WHITE, 256);
+ gfx_text_center(&np_font_body, 263, y + 20, "-", on ? INK : WHITE, 256);
}
+ if (top) tri(306, 44, 3, WHITE); /* more above, more below */
+ if (top < NG - ROWS) tri(306, 194, 1, WHITE);
+ keys_line(207, T("OK play Back quit"));
gfx_rrect(40, 212, 240, 24, 12, INK, 200);
tri(52, 224, 2, WHITE), tri(268, 224, 0, WHITE);
gfx_text_center(&np_font_body, 160, 229, opponent(), WHITE, 256);
if (screen == S_RULES) {
+ static const char *const KEY[2][5] = {{"", "", "", "", "OK"}, {"3", "x", "Ans", "-", "EXE"}};
gfx_fill_alpha(0, 0, SCREEN_W, SCREEN_H, 0, 140);
- box(8, 8, 304, 224, 14, HEX(0x262D52));
+ box(8, 8, 304, 232, 14, HEX(0x262D52));
say(&np_font_title, 160, 34, NAME[game], WHITE);
gfx_paragraph(&np_font_small, 160, 54, 284, 12, RULES[game], HEX(0xD6DCF5), 256);
+ keyboard(26, 100);
+ for (int p = 0; p < 2; p++) { /* who plays, and with which keys */
+ int y = 100 + 36 * p, u = KEYS_USED[game], x = 188;
+ box(166, y, 14, 14, 4, team[p]);
+ gfx_text(&np_font_body, 188, y + 12, p ? opponent() : who(0), WHITE, 256);
+ if (!p && u & 15) x = gfx_text(&np_font_small, x, y + 27, T("arrows"), light[p], 256) + 6;
+ for (int i = 0; i < 5; i++)
+ if (u >> i & 1 && !is_cpu(p)) x = gfx_text(&np_font_small, x, y + 27, KEY[p][i], i < 4 ? light[p] : GOLD, 256) + 6;
+ }
char *e = s;
for (const char *q = T("First to "); *q; q++) *e++ = *q;
num(e, TARGET[game]);
- gfx_text_center(&np_font_body, 231, 184, s, GOLD, 256);
- keyboard(26, 100);
- for (int p = 0; p < 2; p++) {
- box(166, 110 + 30 * p, 14, 14, 4, team[p]);
- gfx_text(&np_font_body, 188, 122 + 30 * p, p ? opponent() : who(0), WHITE, 256);
- }
- button(166, 196, 130, T("Play"), true);
+ gfx_text_center(&np_font_body, 231, 182, s, GOLD, 256);
+ button(166, 190, 130, T("Play"), true);
+ say(&np_font_small, 231, 233, T("Back games"), HEX(0xD6DCF5));
}
} else {
PAINT[game]();
- if (game != PONG) { /* the score bar: each side's name and points */
+ if (game != PONG) { /* the score bar: each side's name and points, and the pause key */
gfx_fill(0, 0, SCREEN_W, 22, INK);
for (int p = 0; p < 2; p++) {
box(p ? 214 : 4, 3, 102, 17, 8, team[p]);
@@ -749,6 +874,7 @@ static void scene(void *ctx) {
gfx_text_right(&np_font_body, p ? 308 : 98, 16, s, WHITE, 256);
}
}
+ if (game) gfx_text_center(&np_font_small, 160, 15, T("Back pause"), HEX(0xD6DCF5), 256);
if (phase == 1 && screen == S_PLAY) {
int t = (int)ago(phase_t), y = 120 - (t < 200 ? (200 - t) / 10 : 0);
gfx_rrect(70, y - 26, 180, 62, 14, INK, 200);
@@ -757,11 +883,11 @@ static void scene(void *ctx) {
}
if (screen == S_PAUSE) {
static const char *const ITEM[4] = {T("Resume"), T("Restart"), T("Other games"), T("Quit game")};
- card(40, 172, T("Paused"), ITEM, 4, 82);
+ card(40, 172, T("Paused"), ITEM, 4, 82, T("OK choose Back resume"));
} else if (screen == S_OVER) {
static const char *const WON[2][2] = {{T("Player 1 wins!"), T("Player 2 wins!")}, {T("You win!"), T("The computer wins")}};
static const char *const ITEM[2] = {T("Rematch"), T("Other games")};
- card(46, 158, WON[cpu > 0][score[1] > score[0]], ITEM, 2, 140);
+ card(46, 158, WON[cpu > 0][score[1] > score[0]], ITEM, 2, 140, T("OK choose Back games"));
for (int p = 0; p < 2; p++) {
num(s, (uint32_t)score[p]);
say(&np_font_title, 124 + 72 * p, 118, s, light[p]);
@@ -771,7 +897,7 @@ static void scene(void *ctx) {
}
if (screen == S_ASK) {
static const char *const ITEM[2] = {T("No"), T("Yes")};
- card(60, 120, T("Quit game?"), ITEM, 2, 104);
+ card(60, 120, T("Quit game?"), ITEM, 2, 104, T("OK choose Back no"));
}
}
@@ -786,15 +912,17 @@ int main(void) {
seed = eadk_random() ^ last;
if (!seed) seed = 1;
const uint8_t *d = np_file_read(SAVE_NAME, &n);
- if (d && n == sizeof sv && d[0] == 'P' && d[1] == 1)
+ if (d && n >= 4 && n <= sizeof sv && d[0] == 'P' && d[1] == 1) /* (a save with fewer games: the first ones) */
for (uint32_t i = 0; i < n; i++) ((uint8_t *)&sv)[i] = d[i];
game = sv.game % NG, cpu = sv.cpu & 3;
+ top = clampi(game - 2, 0, NG - ROWS);
uint64_t held = eadk_keyboard_scan(), stale = held; /* keys still down from the launcher wait to be let go */
for (bool quit = false; !quit;) {
uint32_t t = (uint32_t)eadk_timing_millis();
- ms = t - last < 40 ? (int)(t - last) : 40, last = t;
+ ms = t - last < 40 ? (int)(t - last) : 40, last = t, dt = ms * .001f;
if (screen == S_PLAY) now += (uint32_t)ms;
uint64_t k = eadk_keyboard_scan();
+ if (cpu) k |= (uint64_t)down(k, eadk_key_exe) << eadk_key_ok; /* alone, EXE is OK: one key, either */
stale &= k, k &= ~stale;
uint64_t hit = k & ~held;
held = k;
@@ -803,8 +931,9 @@ int main(void) {
pad[0] = pad[1] = tap[0] = tap[1] = 0;
for (int i = 0; i < 10; i++)
pad[i / 5] |= down(k, KEYS[i / 5][i % 5]) << i % 5, tap[i / 5] |= down(hit, KEYS[i / 5][i % 5]) << i % 5;
- if (cpu && ok) tap[0] |= B_FIRE; /* alone: OK and EXE fire too */
+ if (cpu) pad[1] = tap[1] = 0; /* (player 2's keys are the computer's) */
int dx = down(hit, eadk_key_right) - down(hit, eadk_key_left), dy = down(hit, eadk_key_down) - down(hit, eadk_key_up);
+ int was = screen;
switch (screen) {
case S_MENU:
case S_RULES:
@@ -812,8 +941,9 @@ int main(void) {
if (screen == S_RULES) {
if (ok) new_match();
else if (back) screen = S_MENU;
- } else if (dy) game = (game + dy + NG) % NG;
- else if (ok) screen = S_RULES, save();
+ } else if (dy) {
+ game = (game + dy + NG) % NG, top = clampi(top, game - ROWS + 1, game);
+ } else if (ok) screen = S_RULES, save();
else if (back) back_to = S_MENU, screen = S_ASK, sel = 0;
break;
case S_PLAY:
@@ -821,26 +951,23 @@ int main(void) {
screen = S_PAUSE, sel = 0;
break;
}
- STEP_[game]();
- if (phase == 1 && ago(phase_t) > 1500) {
- phase = 0;
- if (score[0] < TARGET[game] && score[1] < TARGET[game]) START[game]();
- else phase = 2, screen = S_OVER, sel = 0, sv.wins[game][cpu > 0][score[1] > score[0]]++, save();
- }
+ play();
break;
default: /* the cards: Paused, the match over, Quit game? */
sel = (sel + dy) & (screen == S_PAUSE ? 3 : 1);
+ if (screen == S_OVER && t - shown < 700) break; /* (a key still being mashed doesn't choose) */
if (screen == S_ASK) {
if (back || (ok && !sel)) screen = back_to;
else quit = ok;
- } else if (back && screen == S_PAUSE) {
- screen = S_PLAY;
+ } else if (back) {
+ screen = screen == S_PAUSE ? S_PLAY : S_MENU;
} else if (ok) { /* Resume, Restart, Other games, Quit game; Rematch, Other games */
int a = screen == S_PAUSE ? sel : sel ? 2 : 1;
if (a == 1) new_match();
else screen = a ? a == 2 ? S_MENU : S_ASK : S_PLAY, back_to = S_PAUSE, sel = 0;
}
}
+ if (screen != was) shown = t;
eadk_display_wait_for_vblank();
gfx_render(scene, 0, 0, SCREEN_H);
}
diff --git a/games/twoplayer/tests/play.c b/games/twoplayer/tests/play.c
new file mode 100644
index 00000000..8eef5914
--- /dev/null
+++ b/games/twoplayer/tests/play.c
@@ -0,0 +1,408 @@
+/* 2 Players on a computer, for tests and pictures (make host, make test).
+ *
+ * play test the rules of every game; bots against bots: player 1 played by the computer's
+ * own mind (at a player's speed) against the computer, at every level, and a
+ * quick or slow player in Quick Draw and Tug of War; random sessions through the
+ * menus and the games, quitting and opening again, that never break a rule; the
+ * old saves (four games) come back with their tallies, damaged ones reset
+ * play shot DIR pictures (DIR/NAME.ppm): the menu, each game's page, each game, the cards
+ *
+ * Built with -ffp-contract=off, and the game uses no libc randomness, sorting or unordered
+ * arguments: the same results on any computer. */
+#define NP_MODULE 1
+#define main tp_main
+#include "../src/main.c"
+#undef main
+#include
+#include
+#include
+
+static uint16_t fb[SCREEN_H][SCREEN_W];
+static uint8_t disk[1024];
+static uint32_t disk_len;
+static int writes, fails, unfinished;
+void np_push(int x, int y, int w, int h, const color_t *p) {
+ for (int j = 0; j < h; j++)
+ for (int i = 0; i < w; i++) fb[y + j][x + i] = p[j * w + i];
+}
+void *np_alloc(uint32_t size) { return malloc(size); }
+const uint8_t *np_file_read(const char *name, uint32_t *len) {
+ *len = disk_len;
+ return disk_len ? disk : 0;
+}
+bool np_file_write(const char *name, const void *d, uint32_t len) {
+ if (strcmp(name, "twoplayer.sav") || len > sizeof disk) return false;
+ memcpy(disk, d, disk_len = len), writes++;
+ return true;
+}
+#define CHECK(c, ...) do { if (!(c)) { printf("FAIL %s:%d: ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); fails++; } } while (0)
+
+/* ---------------------------------------------------------------- whole sessions, scripted */
+static uint32_t clock_ms, frames, fseed = 1;
+static int session_frames;
+static uint64_t (*script)(uint32_t frame);
+static uint32_t frnd(void) { return fseed = fseed * 1664525u + 1013904223u; }
+static uint64_t K(int k) { return (uint64_t)1 << k; }
+static uint64_t no_keys(uint32_t f) { return 0; }
+uint64_t eadk_keyboard_scan(void) {
+ clock_ms += 15 + frnd() % 30, frames++;
+ if (session_frames && frames >= (uint32_t)session_frames) return K(eadk_key_home);
+ if (script) return script(frames);
+ /* mostly play keys, sometimes the menus' and Back */
+ static const uint8_t KEY[] = {eadk_key_left, eadk_key_up, eadk_key_down, eadk_key_right, eadk_key_ok, eadk_key_exe,
+ eadk_key_three, eadk_key_multiplication, eadk_key_ans, eadk_key_minus, eadk_key_back};
+ uint64_t k = 0;
+ for (int i = 0; i < 3; i++) {
+ uint32_t a = frnd(), b = frnd(), c = frnd(); /* (one at a time: the same order on any compiler) */
+ if (a % 3 == 0) k |= K(KEY[b % (sizeof KEY - (c % 8 ? 1 : 0))]);
+ }
+ return k;
+}
+uint64_t eadk_timing_millis(void) { return clock_ms; }
+uint32_t eadk_random(void) { return frnd(); }
+bool eadk_display_wait_for_vblank(void) { return true; }
+void eadk_timing_msleep(uint32_t ms) { (void)ms; }
+
+/* the game's state is sane */
+static void sane(const char *where) {
+ CHECK(game >= 0 && game < NG && cpu >= 0 && cpu <= 3, "%s: game %d cpu %d", where, game, cpu);
+ CHECK(score[0] >= 0 && score[1] >= 0 && score[0] <= TARGET[game] && score[1] <= TARGET[game], "%s: score %d-%d in %d", where,
+ score[0], score[1], game);
+ CHECK(screen >= S_MENU && screen <= S_OVER && top >= 0 && top <= NG - ROWS && game >= top && game < top + ROWS, "%s: screen %d top %d",
+ where, screen, top);
+ CHECK(pg.rope >= -75 && pg.rope <= 75, "%s: rope %f", where, (double)pg.rope);
+}
+static void reset_all(void) { /* as a fresh start of the app */
+ memset(&sv, 0, sizeof sv), memset(&pg, 0, sizeof pg), memset(&bot, 0, sizeof bot);
+ screen = sel = back_to = top = game = cpu = 0, now = 0, phase = 0, score[0] = score[1] = 0;
+}
+
+/* ---------------------------------------------------------------- matches, frame by frame */
+static void begin(int g, int level, uint32_t s) {
+ reset_all();
+ game = g, cpu = level, seed = s ? s : 1, now = 1000, ms = 20, dt = ms * .001f, top = clampi(g - 2, 0, NG - ROWS);
+ new_match();
+}
+static void frame(int p1pad, int p1tap, int p2pad, int p2tap) { /* as main's loop in a match */
+ ms = 20, dt = .02f, now += 20;
+ pad[0] = p1pad, tap[0] = p1tap, pad[1] = p2pad, tap[1] = p2tap;
+ if (cpu) pad[1] = tap[1] = 0;
+ play();
+}
+
+/* Player 1 played by the computer's own mind at a level, at a player's speed: the court mirrored
+ so player 1 is player 2, the computer's thinking (with its own memory) run, its keys read. */
+static __typeof__(bot) mind;
+static int ai_aim1;
+static uint32_t ai_t1, ai_shot1;
+#define SWAP(a, b) do { __typeof__(a) t_ = a; a = b; b = t_; } while (0)
+static void think(int level, int *p, int *t) {
+ int keep = cpu;
+ __typeof__(bot) b = bot;
+ memcpy(&bot, &mind, sizeof bot), cpu = level, *p = *t = 0;
+ if (game == PONG) {
+ int y = py[1], x = bx, vx = bvx, a = aim; /* (a player aims at the middle) */
+ py[1] = py[0], bx = ((SCREEN_W - BS) << 8) - bx, bvx = -bvx, aim = 0;
+ int v = pong_cpu();
+ py[1] = y, bx = x, bvx = vx, aim = a;
+ *p = v > 3 ? B_DOWN : v < -3 ? B_UP : 0;
+ } else if (game == HOCKEY) {
+ float s[8] = {mx[0], my[0], mvx[0], mvy[0], mx[1], my[1], mvx[1], mvy[1]}, q[2] = {qx, qvx}, tx, ty;
+ mx[1] = SCREEN_W - s[0], my[1] = s[1], mvx[1] = -s[2], mvy[1] = s[3];
+ mx[0] = SCREEN_W - s[4], my[0] = s[5], mvx[0] = -s[6], mvy[0] = s[7], qx = SCREEN_W - qx, qvx = -qvx;
+ hockey_cpu(&tx, &ty);
+ mx[0] = s[0], my[0] = s[1], mvx[0] = s[2], mvy[0] = s[3], mx[1] = s[4], my[1] = s[5], mvx[1] = s[6], mvy[1] = s[7];
+ qx = q[0], qvx = q[1], tx = -tx;
+ *p = (tx > 40 ? B_RIGHT : tx < -40 ? B_LEFT : 0) | (ty > 40 ? B_DOWN : ty < -40 ? B_UP : 0);
+ } else if (game == TANKS) {
+ int v, turn;
+ SWAP(tx_[0], tx_[1]); SWAP(ty_[0], ty_[1]); SWAP(ta[0], ta[1]); SWAP(dead[0], dead[1]); SWAP(dead_t[0], dead_t[1]);
+ SWAP(ai_aim, ai_aim1); SWAP(ai_t, ai_t1); SWAP(ai_shot, ai_shot1);
+ for (int i = 0; i < SHELLS; i++) sh[i].own = sh[i].own < 0 ? -1 : 1 - sh[i].own;
+ tanks_cpu(&v, &turn);
+ SWAP(tx_[0], tx_[1]); SWAP(ty_[0], ty_[1]); SWAP(ta[0], ta[1]); SWAP(dead[0], dead[1]); SWAP(dead_t[0], dead_t[1]);
+ SWAP(ai_aim, ai_aim1); SWAP(ai_t, ai_t1); SWAP(ai_shot, ai_shot1);
+ for (int i = 0; i < SHELLS; i++) sh[i].own = sh[i].own < 0 ? -1 : 1 - sh[i].own;
+ *p = (v > 0 ? B_UP : 0) | (turn > 0 ? B_RIGHT : turn < 0 ? B_LEFT : 0);
+ } else if (game == CYCLES) {
+ static const uint8_t BIT[4] = {B_RIGHT, B_DOWN, B_LEFT, B_UP};
+ uint32_t next = bot.next;
+ SWAP(hx[0], hx[1]); SWAP(hy[0], hy[1]); SWAP(hd[0], hd[1]);
+ int d = cycles_cpu();
+ SWAP(hx[0], hx[1]); SWAP(hy[0], hy[1]); SWAP(hd[0], hd[1]);
+ int last = nturns[0] ? turns[0][nturns[0] - 1] : hd[0];
+ if (bot.next != next && d != last) *t = BIT[d]; /* (a turn when it decided one) */
+ }
+ memcpy(&mind, &bot, sizeof bot), bot = b, cpu = keep;
+}
+/* a match of player 1 (a mind at level l1, or for Quick Draw and Tug of War a player who reacts
+ in r ms, or taps every r ms) against the computer at l2; who won */
+static int match(int g, int l1, int r, int l2, uint32_t s) {
+ begin(g, l2, s);
+ memset(&mind, 0, sizeof mind), ai_aim1 = -1, ai_t1 = ai_shot1 = now;
+ uint32_t last_tap = 0;
+ for (int f = 0; f < 60000 && screen == S_PLAY; f++) { /* (20 minutes: two minds can pin the puck forever) */
+ int p = 0, t = 0;
+ if (g == QUICK) t = !phase && due(pg.at + r) ? B_FIRE : 0;
+ else if (g == TUG) {
+ if (!phase && now - last_tap >= (uint32_t)r) t = B_FIRE, last_tap = now;
+ } else think(l1, &p, &t);
+ frame(p | t, t, 0, 0);
+ sane("match");
+ }
+ if (screen != S_OVER) unfinished++, printf("unfinished: game %d %d-%d screen %d\n", g, score[0], score[1], screen);
+ return score[1] >= score[0];
+}
+
+/* ---------------------------------------------------------------- the rules */
+static void rules(void) {
+ /* Quick Draw: too soon loses, after the signal the first wins, together a draw */
+ begin(QUICK, 0, 7);
+ frame(B_FIRE, B_FIRE, 0, 0);
+ CHECK(phase == 1 && winner == 1 && score[1] == 1, "quick: too soon %d %d", phase, winner);
+ begin(QUICK, 0, 7), now = pg.at - 20;
+ frame(0, 0, B_FIRE, B_FIRE);
+ CHECK(winner == 1, "quick: player 2 first %d", winner);
+ begin(QUICK, 0, 7), now = pg.at - 20;
+ frame(B_FIRE, B_FIRE, B_FIRE, B_FIRE);
+ CHECK(winner == 2 && !score[0] && !score[1], "quick: together %d", winner);
+ begin(QUICK, 0, 7), now = pg.at - 25;
+ frame(0, 0, 0, 0);
+ CHECK(!phase, "quick: nothing yet");
+ begin(QUICK, 3, 7); /* the computer's keys are its own */
+ int f = 0;
+ while (!phase && f++ < 1000) frame(0, 0, B_FIRE, B_FIRE);
+ CHECK(winner == 1 && now >= pg.next, "quick: the computer drew at %u (%u)", now, pg.next);
+ /* Tug of War: a tap pulls 7.5, the rope slides back 3 a second, past 67 wins, 30 s ends it */
+ begin(TUG, 0, 7);
+ frame(B_FIRE, B_FIRE, 0, 0);
+ CHECK(pg.rope < -7.4f && pg.rope > -7.5f, "tug: one tap %f", (double)pg.rope);
+ frame(B_FIRE, B_FIRE, B_FIRE, B_FIRE);
+ CHECK(pg.rope < -7.3f && pg.rope > -7.5f, "tug: together %f", (double)pg.rope);
+ for (int i = 0; i < 50; i++) frame(0, 0, 0, 0);
+ CHECK(pg.rope < -4.3f && pg.rope > -4.5f, "tug: slides back %f", (double)pg.rope);
+ frame(B_FIRE, 0, 0, 0);
+ CHECK(pg.rope > -4.5f, "tug: holding doesn't pull");
+ for (int i = 0; i < 9 && !phase; i++) frame(B_FIRE, B_FIRE, 0, 0), frame(0, 0, 0, 0);
+ CHECK(phase == 1 && winner == 0, "tug: player 1 pulls it over %d %f", phase, (double)pg.rope);
+ begin(TUG, 0, 7);
+ frame(0, 0, B_FIRE, B_FIRE);
+ while (!phase) frame(0, 0, 0, 0);
+ CHECK(winner == 2 && round_ms() > 30000, "tug: time up, the rope back in the middle: a draw %d", winner);
+ begin(TUG, 0, 7);
+ while (!phase) frame(0, 0, 0, 0), frame(0, 0, B_FIRE, B_FIRE);
+ CHECK(winner == 1, "tug: player 2 wins");
+ /* a match: first to its points, the tally kept */
+ begin(TUG, 0, 7);
+ sv.wins[TUG][0][1] = 41;
+ for (int r = 0; r < 5; r++) {
+ while (!phase) frame(0, 0, 0, 0), frame(0, 0, B_FIRE, B_FIRE);
+ for (int i = 0; i < 80 && phase; i++) frame(0, 0, 0, 0);
+ }
+ CHECK(screen == S_OVER && score[1] == 5 && sv.wins[TUG][0][1] == 42, "tug: the match %d %d %d", screen, score[1], sv.wins[TUG][0][1]);
+ /* Pong: a ball past a paddle is the other's point; the serve goes to the one who lost it */
+ begin(PONG, 0, 7);
+ bx = 2 << 8, bvx = -40, serve_at = 0, by = 120 << 8, bvy = 0, py[0] = 20 << 8;
+ for (int i = 0; i < 20 && !phase; i++) frame(0, 0, 0, 0);
+ CHECK(phase == 1 && winner == 1, "pong: past player 1");
+ for (int i = 0; i < 80 && phase; i++) frame(0, 0, 0, 0);
+ CHECK(!phase && bvx < 0, "pong: served to player 1");
+ /* Light Cycles: into a wall */
+ begin(CYCLES, 0, 7);
+ for (int i = 0; i < 400 && !phase; i++) frame(0, 0, 0, 0);
+ CHECK(phase == 1 && winner == 2, "cycles: both into the walls together %d", winner);
+}
+
+/* ---------------------------------------------------------------- saves */
+static void saves(void) {
+ /* version 1, four games: its tallies, game and opponent come back, the new games at 0 */
+ uint8_t v1[36] = {'P', 1, 3, 2};
+ for (int i = 0; i < 16; i++) v1[4 + 2 * i] = (uint8_t)(10 + i), v1[5 + 2 * i] = (uint8_t)i;
+ reset_all(), memcpy(disk, v1, disk_len = 36), writes = 0;
+ session_frames = 3, frames = 0, script = no_keys, tp_main(), session_frames = 0;
+ CHECK(game == CYCLES && cpu == 2, "v1: game %d cpu %d", game, cpu);
+ for (int g = 0; g < NG; g++)
+ for (int i = 0; i < 4; i++) {
+ int want = g < 4 ? (10 + g * 4 + i) | (g * 4 + i) << 8 : 0;
+ CHECK(sv.wins[g][i / 2][i % 2] == want, "v1: game %d tally %d: %d", g, i, sv.wins[g][i / 2][i % 2]);
+ }
+ CHECK(writes && disk_len == sizeof sv && disk[0] == 'P' && disk[1] == 1 && !memcmp(disk + 4, v1 + 4, 32), "v1: saved again, the same");
+ /* the new length comes back as it is */
+ uint8_t full[sizeof sv];
+ memcpy(full, disk, sizeof full);
+ reset_all(), session_frames = 3, frames = 0, tp_main(), session_frames = 0;
+ CHECK(!memcmp(disk, full, sizeof full), "a save comes back the same");
+ /* damaged ones: another magic, a later version, too long: a fresh start */
+ for (int k = 0; k < 3; k++) {
+ memcpy(disk, full, disk_len = sizeof full);
+ if (k == 0) disk[0] = 'Q';
+ if (k == 1) disk[1] = 2;
+ if (k == 2) disk_len = sizeof full + 2;
+ reset_all(), sv.wins[0][0][0] = 0, session_frames = 3, frames = 0, tp_main(), session_frames = 0;
+ CHECK(sv.wins[0][0][0] == 0 && sv.wins[1][0][0] == 0, "damaged save %d read", k);
+ }
+ disk_len = 0, script = 0;
+}
+
+/* ---------------------------------------------------------------- random sessions */
+static void sessions(int n) {
+ disk_len = 0;
+ for (int s = 0; s < n; s++) {
+ reset_all();
+ session_frames = 300 + (int)(frnd() % 1500), frames = 0, script = 0;
+ tp_main();
+ sane("session");
+ CHECK(disk_len == sizeof sv && disk[0] == 'P', "session %d: saved", s);
+ }
+ session_frames = 0;
+}
+
+/* Back pauses, Quit game asks first, Home saves and quits */
+static uint64_t walk_script(uint32_t f) {
+ /* OK (its page), OK (play), Back (pause), down x3 (Quit game), OK, OK (No), Back (play), Back (pause), down x3, OK,
+ down (Yes), OK: out */
+ static const uint8_t STEPS[] = {eadk_key_ok, eadk_key_ok, eadk_key_back, eadk_key_down, eadk_key_down, eadk_key_down,
+ eadk_key_ok, eadk_key_ok, eadk_key_back, eadk_key_back, eadk_key_down, eadk_key_down,
+ eadk_key_down, eadk_key_ok, eadk_key_down, eadk_key_ok};
+ static const uint8_t SCREEN[] = {S_MENU, S_RULES, S_PLAY, S_PAUSE, S_PAUSE, S_PAUSE, S_PAUSE, S_ASK, S_PAUSE, S_PLAY,
+ S_PAUSE, S_PAUSE, S_PAUSE, S_PAUSE, S_ASK, S_ASK};
+ if (f % 4 || f / 4 < 1 || f / 4 > sizeof STEPS) return 0;
+ CHECK(screen == SCREEN[f / 4 - 1], "keys: step %u on screen %d", f / 4, screen);
+ return K(STEPS[f / 4 - 1]);
+}
+/* Tug of War against the computer: down to it, the computer, OK, OK, then OK mashed; once the match is
+ over, mashing on doesn't pick Rematch (for 600 ms) */
+static uint32_t over_at;
+static uint64_t mash_script(uint32_t f) {
+ if (f < 30) return f % 2 ? 0 : K(f < 12 ? eadk_key_down : f < 14 ? eadk_key_right : eadk_key_ok);
+ if (screen == S_OVER && !over_at) over_at = clock_ms;
+ if (over_at && clock_ms - over_at > 600) {
+ CHECK(screen == S_OVER, "a mashed key picked from the end card");
+ return K(eadk_key_home);
+ }
+ return f % 2 ? K(eadk_key_ok) : 0;
+}
+/* alone, EXE is OK: Tug of War against the computer, OK and EXE pressed in turn, one always held: a single pull */
+static float rope_was;
+static int pulls;
+static uint64_t both_script(uint32_t f) {
+ if (f < 30) return f % 2 ? 0 : K(f < 12 ? eadk_key_down : f < 14 ? eadk_key_right : f < 18 ? eadk_key_exe : 0);
+ if (f == 30) CHECK(screen == S_PLAY && game == TUG && cpu == 1 && round_ms() < 600, "exe: in the match %d %d", screen, game);
+ pulls += pg.rope < rope_was - 5, rope_was = pg.rope;
+ if (f < 90) return f % 4 == 0 ? K(eadk_key_ok) : f % 4 == 2 ? K(eadk_key_exe) : K(eadk_key_ok) | K(eadk_key_exe);
+ CHECK(pulls == 1, "exe: %d pulls", pulls);
+ return K(eadk_key_home);
+}
+static void keys(void) {
+ reset_all(), frames = 0, script = both_script, pulls = 0, rope_was = 0;
+ tp_main();
+ reset_all(), disk_len = 0, frames = 0, script = walk_script, session_frames = 0;
+ tp_main();
+ CHECK(frames == 64 && disk_len == sizeof sv, "quit game asks first, then quits and saves (frame %u)", frames);
+ reset_all(), frames = 0, script = mash_script, over_at = 0;
+ tp_main();
+ CHECK(over_at && game == TUG && cpu == 1, "tug: over %u game %d cpu %d", over_at, game, cpu);
+ script = 0;
+}
+
+/* ---------------------------------------------------------------- bots against bots */
+static void bots(void) {
+ static const char *const GAME[] = {"Pong", "Air Hockey", "Tanks", "Light Cycles"};
+ int n = 12;
+ for (int g = PONG; g <= CYCLES; g++) {
+ printf("%-12s player 1 at easy/normal/hard against the computer at easy/normal/hard, of %d:\n", GAME[g], n);
+ int won[4][4] = {{0}};
+ for (int l1 = 1; l1 <= 3; l1++) {
+ printf(" ");
+ for (int l2 = 1; l2 <= 3; l2++) {
+ for (int i = 0; i < n; i++) won[l1][l2] += !match(g, l1, 0, l2, 1000u * g + 100u * l1 + 10u * l2 + i);
+ printf(" %2d", won[l1][l2]);
+ }
+ printf("\n");
+ }
+ /* the levels: a harder computer wins more; a good player beats easy, and can beat hard; hard beats a
+ weak player */
+ CHECK(won[2][1] >= won[2][3] && won[3][1] > n / 2 && won[3][3] > 0 && won[1][3] < n, "%s: the levels", GAME[g]);
+ }
+ /* Quick Draw: a quick player (250 ms) and a slow one (400 ms); Tug of War: 7 and 5 taps a second */
+ int quick[4][2] = {{0}}, tug[4][2] = {{0}};
+ for (int l2 = 1; l2 <= 3; l2++)
+ for (int i = 0; i < n; i++) {
+ quick[l2][0] += !match(QUICK, 0, 250, l2, 50u + 10u * l2 + i), quick[l2][1] += !match(QUICK, 0, 400, l2, 90u + 10u * l2 + i);
+ tug[l2][0] += !match(TUG, 0, 143, l2, 50u + 10u * l2 + i), tug[l2][1] += !match(TUG, 0, 200, l2, 90u + 10u * l2 + i);
+ }
+ printf("Quick Draw a 250 ms / 400 ms player against easy, normal, hard, of %d: %d/%d %d/%d %d/%d\n", n, quick[1][0], quick[1][1],
+ quick[2][0], quick[2][1], quick[3][0], quick[3][1]);
+ printf("Tug of War 7 / 5 taps a second against easy, normal, hard, of %d: %d/%d %d/%d %d/%d\n", n, tug[1][0], tug[1][1], tug[2][0],
+ tug[2][1], tug[3][0], tug[3][1]);
+ printf("unfinished (the puck pinned by two minds): %d\n", unfinished);
+ CHECK(unfinished <= 4, "unfinished matches");
+ CHECK(quick[1][1] > n / 2 && quick[3][0] > 0 && quick[3][1] < n, "quick draw: levels");
+ CHECK(tug[1][1] > n / 2 && tug[3][0] > 0 && tug[3][1] < n, "tug of war: levels");
+}
+
+/* ---------------------------------------------------------------- pictures */
+static void shot(const char *dir, const char *name) {
+ char path[256];
+ snprintf(path, sizeof path, "%s/%s.ppm", dir, name);
+ FILE *f = fopen(path, "wb");
+ if (!f) return;
+ fprintf(f, "P6\n%d %d\n255\n", SCREEN_W, SCREEN_H);
+ gfx_render(scene, 0, 0, SCREEN_H);
+ for (int y = 0; y < SCREEN_H; y++)
+ for (int x = 0; x < SCREEN_W; x++) {
+ unsigned c = fb[y][x];
+ unsigned char p[3] = {(unsigned char)((c >> 11) * 255 / 31), (unsigned char)((c >> 5 & 63) * 255 / 63), (unsigned char)((c & 31) * 255 / 31)};
+ fwrite(p, 1, 3, f);
+ }
+ fclose(f);
+}
+static void shots(const char *dir) {
+ char name[32];
+ reset_all();
+ sv.wins[QUICK][1][0] = 3, sv.wins[QUICK][1][1] = 1, sv.wins[PONG][1][0] = 2, sv.wins[TUG][1][1] = 1;
+ static const uint16_t AT[NG] = {170, 120, 400, 160, 20, 90}; /* (frames into a round: the README's pictures) */
+ for (int g = 0; g < NG; g++) {
+ begin(g, 3, 11u + g);
+ memset(&mind, 0, sizeof mind), ai_aim1 = -1, ai_t1 = ai_shot1 = now;
+ for (int i = 0; i < AT[g] && screen == S_PLAY; i++) {
+ int p = 0, t = 0;
+ if (g <= CYCLES) think(3, &p, &t);
+ else if (g == TUG) t = i % 6 == 0 ? B_FIRE : 0;
+ frame(p | t, t, 0, 0);
+ }
+ if (g == QUICK) now = pg.at + 150; /* (the signal) */
+ snprintf(name, sizeof name, "game_%02d", g), shot(dir, name);
+ screen = S_RULES, top = clampi(top, g - ROWS + 1, g), shot(dir, (snprintf(name, sizeof name, "rules_%02d", g), name));
+ if (g == 0 || g == QUICK) screen = S_MENU, shot(dir, (snprintf(name, sizeof name, "menu_%02d", g), name));
+ }
+ begin(PONG, 0, 3), shot(dir, "pong_2p");
+ begin(TUG, 0, 3), screen = S_RULES, shot(dir, "rules_tug_2p");
+ begin(HOCKEY, 1, 5);
+ for (int i = 0; i < 60; i++) frame(0, 0, 0, 0);
+ screen = S_PAUSE, sel = 1, shot(dir, "pause_hockey");
+ begin(TUG, 1, 5), screen = S_PAUSE, shot(dir, "pause_tug");
+ score[0] = 5, score[1] = 3, screen = S_OVER, sel = 0, shot(dir, "over_tug");
+ screen = S_ASK, back_to = S_PAUSE, sel = 0, shot(dir, "ask_tug");
+ begin(TUG, 1, 5);
+ for (int i = 0; i < 100; i++) frame(i % 6 ? 0 : B_FIRE, i % 6 ? 0 : B_FIRE, 0, 0);
+ point(0), now += 300, shot(dir, "point_tug");
+}
+
+int main(int argc, char **argv) {
+ gfx_buf = strip;
+ static const uint32_t TEAM[2] = {0xEF4136, 0x2E7CF6};
+ for (int p = 0; p < 2; p++)
+ team[p] = gfx_rgb(TEAM[p]), light[p] = gfx_rgb(gfx_lerp888(TEAM[p], 0xFFFFFF, 120)), dark[p] = gfx_rgb(gfx_lerp888(TEAM[p], 0, 110));
+ if (argc > 2 && !strcmp(argv[1], "shot")) {
+ shots(argv[2]);
+ return 0;
+ }
+ rules();
+ saves();
+ keys();
+ sessions(argc > 2 ? atoi(argv[2]) : 300);
+ bots();
+ printf(fails ? "%d FAILED\n" : "all passed\n", fails);
+ return fails != 0;
+}
diff --git a/games/twoplayer/tools/bot.py b/games/twoplayer/tools/bot.py
index 95501b2b..56f779bc 100644
--- a/games/twoplayer/tools/bot.py
+++ b/games/twoplayer/tools/bot.py
@@ -2,9 +2,9 @@
"""Plays 2 Players in the ARM emulator for the README's pictures.
Runs the calculator build (TwoPlayers.nwa) in tools/emu.py as player 1 against the hard computer,
-game after game: it reads the screen (the ball, the puck, the tanks, the light cycles) and presses
-the keys a player would. Saves the raw frames, then the gallery's GIF (a few seconds of each game)
-and a screenshot of each game.
+game after game: it reads the screen (the ball, the puck, the tanks, the light cycles, the signal)
+and presses the keys a player would. Saves the raw frames, then the gallery's GIF (a few seconds of
+each game) and a screenshot of each game.
Usage: bot.py TwoPlayers.nwa OUT [--fps 25] [--seconds 8] (needs NumPy; gifsicle for small GIFs)
"""
@@ -42,6 +42,7 @@ def __init__(self, nwa, out, fps):
self.last = None
self.dir = 0
self.fired = 0
+ self.seen = None
def grab(self, calc):
open(os.path.join(self.out, f"{self.frame:06d}_{int(calc.now_ms):06d}.raw"), "wb").write(bytes(calc.screen))
@@ -138,7 +139,7 @@ def tanks(self):
if abs(d) < 0.6 and math.hypot(x1 - x0, y1 - y0) > 50:
keys.append("up")
if abs(d) < 0.15 and self.c.now_ms - self.fired > 700:
- keys.append("shift")
+ keys.append("ok")
self.fired = self.c.now_ms
self.hold(keys)
@@ -183,6 +184,14 @@ def cycles(self):
best, bv = d, v
self.hold([] if best == self.dir else [["right", "down", "left", "up"][best]])
+ def quick(self): # Quick Draw: OK a quarter of a second after the ground turns at the signal
+ go = near(self.screen()[100:101, 20:21], 0x3A2F1C, 12).any()
+ self.seen = (self.seen or self.c.now_ms) if go else None
+ self.hold(["ok"] if go and self.c.now_ms - self.seen >= 250 else [])
+
+ def tug(self): # Tug of War: OK, let go, OK...
+ self.hold(["ok"] if int(self.c.now_ms // STEP) % 4 == 0 else [])
+
def play(self, game, seconds):
self.tap("ok", 1400) # the rules
self.tap("ok", 300) # play
@@ -190,7 +199,7 @@ def play(self, game, seconds):
self.last = None
end = self.c.now_ms + seconds * 1000
while self.c.now_ms < end:
- [self.pong, self.hockey, self.tanks, self.cycles][game]()
+ [self.pong, self.hockey, self.tanks, self.cycles, self.quick, self.tug][game]()
self.marks.append((self.c.now_ms, -1))
self.tap("back") # Paused, Other games
self.tap("down")
@@ -212,7 +221,7 @@ def main():
for _ in range(3): # the hard computer
bot.tap("right", 150)
bot.run(500)
- for g in range(4):
+ for g in range(6):
bot.play(g, a.seconds)
bot.tap("down", 300)
frames = record.load_frames(raw)
diff --git a/tests/test_games.py b/tests/test_games.py
index 79ff3e9d..0313aaf7 100644
--- a/tests/test_games.py
+++ b/tests/test_games.py
@@ -466,9 +466,9 @@ def dk_frames(v):
and struct.unpack_from("= 10,
lambda v: v[4] == 0 and struct.unpack_from("= 1), # best kept, shake off
- # Air Hockey and the easy computer chosen; then the normal computer and Tanks, the tallies kept
- "twoplayer": (lambda v: len(v) == 36 and v[:4] == b"P\1\1\1",
- lambda v: len(v) == 36 and v[:4] == b"P\1\2\2"),
+ # Air Hockey and the easy computer chosen; then the normal computer and Tanks, the tallies of the six games kept
+ "twoplayer": (lambda v: len(v) == 52 and v[:4] == b"P\1\1\1",
+ lambda v: len(v) == 52 and v[:4] == b"P\1\2\2"),
# a game kept (Easy, the guide read) with the two digits the hints wrote (the digits at 109); then three
"sudoku": (lambda v: len(v) == 352 and v[:4] == b"S\1\3\0" and sum(1 <= b <= 9 for b in v[109:190]) == 2,
lambda v: v[2] & 1 and sum(1 <= b <= 9 for b in v[109:190]) == 3),