Test moves, items, and abilities in battle

Thank you to SBird for providing mgba-rom-test binaries and Spikes/Toxic
Spikes tests!

Co-authored-by: sbird <sbird@no.tld>
This commit is contained in:
Martin Griffin 2022-12-24 05:13:42 +00:00
parent c11774acb8
commit f1b9872bf0
79 changed files with 6564 additions and 77 deletions

View File

@ -29,7 +29,7 @@ jobs:
repository: pret/agbcc
- name: Install binutils
run: sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi
run: sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi libelf-dev
# build-essential, git, and libpng-dev are already installed
# gcc-arm-none-eabi is only needed for the modern build
# as an alternative to dkP
@ -41,10 +41,15 @@ jobs:
working-directory: agbcc
- name: Agbcc
run: make -j${nproc} all
run: make -j${nproc} -O all
- name: Modern
env:
MODERN: 1
COMPARE: 0
run: make -j${nproc} all
run: make -j${nproc} -O all
- name: Test
run: |
make -j${nproc} -O pokeemerald-test.elf
make -j${nproc} check

View File

@ -79,6 +79,9 @@ ELF = $(ROM:.gba=.elf)
MAP = $(ROM:.gba=.map)
SYM = $(ROM:.gba=.sym)
TESTELF = $(ROM:.gba=-test.elf)
HEADLESSELF = $(ROM:.gba=-test-headless.elf)
C_SUBDIR = src
GFLIB_SUBDIR = gflib
ASM_SUBDIR = asm
@ -88,6 +91,7 @@ SONG_SUBDIR = sound/songs
MID_SUBDIR = sound/songs/midi
SAMPLE_SUBDIR = sound/direct_sound_samples
CRY_SUBDIR = sound/direct_sound_samples/cries
TEST_SUBDIR = test
C_BUILDDIR = $(OBJ_DIR)/$(C_SUBDIR)
GFLIB_BUILDDIR = $(OBJ_DIR)/$(GFLIB_SUBDIR)
@ -95,6 +99,7 @@ ASM_BUILDDIR = $(OBJ_DIR)/$(ASM_SUBDIR)
DATA_ASM_BUILDDIR = $(OBJ_DIR)/$(DATA_ASM_SUBDIR)
SONG_BUILDDIR = $(OBJ_DIR)/$(SONG_SUBDIR)
MID_BUILDDIR = $(OBJ_DIR)/$(MID_SUBDIR)
TEST_BUILDDIR = $(OBJ_DIR)/$(TEST_SUBDIR)
ASFLAGS := -mcpu=arm7tdmi --defsym MODERN=$(MODERN)
@ -131,10 +136,13 @@ RAMSCRGEN := tools/ramscrgen/ramscrgen$(EXE)
FIX := tools/gbafix/gbafix$(EXE)
MAPJSON := tools/mapjson/mapjson$(EXE)
JSONPROC := tools/jsonproc/jsonproc$(EXE)
PATCHELF := tools/patchelf/patchelf$(EXE)
ROMTEST ?= $(shell { command -v mgba-rom-test || command -v tools/mgba/mgba-rom-test$(EXE); } 2>/dev/null)
ROMTESTHYDRA := tools/mgba-rom-test-hydra/mgba-rom-test-hydra$(EXE)
PERL := perl
TOOLDIRS := $(filter-out tools/agbcc tools/binutils,$(wildcard tools/*))
TOOLDIRS := $(filter-out tools/mgba tools/agbcc tools/binutils,$(wildcard tools/*))
TOOLBASE = $(TOOLDIRS:tools/%=%)
TOOLS = $(foreach tool,$(TOOLBASE),tools/$(tool)/$(tool)$(EXE))
@ -150,7 +158,7 @@ MAKEFLAGS += --no-print-directory
# Secondary expansion is required for dependency variables in object rules.
.SECONDEXPANSION:
.PHONY: all rom clean compare tidy tools mostlyclean clean-tools $(TOOLDIRS) libagbsyscall modern tidymodern tidynonmodern
.PHONY: all rom clean compare tidy tools mostlyclean clean-tools $(TOOLDIRS) libagbsyscall modern tidymodern tidynonmodern check
infoshell = $(foreach line, $(shell $1 | sed "s/ /__SPACE__/g"), $(info $(subst __SPACE__, ,$(line))))
@ -158,7 +166,7 @@ infoshell = $(foreach line, $(shell $1 | sed "s/ /__SPACE__/g"), $(info $(subst
# Disable dependency scanning for clean/tidy/tools
# Use a separate minimal makefile for speed
# Since we don't need to reload most of this makefile
ifeq (,$(filter-out all rom compare modern libagbsyscall syms,$(MAKECMDGOALS)))
ifeq (,$(filter-out all rom compare modern check libagbsyscall syms,$(MAKECMDGOALS)))
$(call infoshell, $(MAKE) -f make_tools.mk)
else
NODEP ?= 1
@ -182,6 +190,11 @@ C_SRCS_IN := $(wildcard $(C_SUBDIR)/*.c $(C_SUBDIR)/*/*.c $(C_SUBDIR)/*/*/*.c)
C_SRCS := $(foreach src,$(C_SRCS_IN),$(if $(findstring .inc.c,$(src)),,$(src)))
C_OBJS := $(patsubst $(C_SUBDIR)/%.c,$(C_BUILDDIR)/%.o,$(C_SRCS))
TEST_SRCS_IN := $(wildcard $(TEST_SUBDIR)/*.c $(TEST_SUBDIR)/*/*.c $(TEST_SUBDIR)/*/*/*.c)
TEST_SRCS := $(foreach src,$(TEST_SRCS_IN),$(if $(findstring .inc.c,$(src)),,$(src)))
TEST_OBJS := $(patsubst $(TEST_SUBDIR)/%.c,$(TEST_BUILDDIR)/%.o,$(TEST_SRCS))
TEST_OBJS_REL := $(patsubst $(OBJ_DIR)/%,%,$(TEST_OBJS))
GFLIB_SRCS := $(wildcard $(GFLIB_SUBDIR)/*.c)
GFLIB_OBJS := $(patsubst $(GFLIB_SUBDIR)/%.c,$(GFLIB_BUILDDIR)/%.o,$(GFLIB_SRCS))
@ -206,7 +219,7 @@ MID_OBJS := $(patsubst $(MID_SUBDIR)/%.mid,$(MID_BUILDDIR)/%.o,$(MID_SRCS))
OBJS := $(C_OBJS) $(GFLIB_OBJS) $(C_ASM_OBJS) $(ASM_OBJS) $(DATA_ASM_OBJS) $(SONG_OBJS) $(MID_OBJS)
OBJS_REL := $(patsubst $(OBJ_DIR)/%,%,$(OBJS))
SUBDIRS := $(sort $(dir $(OBJS)))
SUBDIRS := $(sort $(dir $(OBJS) $(dir $(TEST_OBJS))))
$(shell mkdir -p $(SUBDIRS))
endif
@ -407,6 +420,14 @@ $(OBJ_DIR)/sym_common.ld: sym_common.txt $(C_OBJS) $(wildcard common_syms/*.txt)
$(OBJ_DIR)/sym_ewram.ld: sym_ewram.txt
$(RAMSCRGEN) ewram_data $< ENGLISH > $@
# NOTE: Based on C_DEP above, but without NODEP and KEEP_TEMPS handling.
define TEST_DEP
$1: $2 $$(shell $(SCANINC) -I include -I tools/agbcc/include -I gflib -I test $2)
@echo "$$(CC1) <flags> -o $$@ $$<"
@$$(CPP) $$(CPPFLAGS) $$< | $$(PREPROC) $$< charmap.txt -i | $$(CC1) $$(CFLAGS) -o - - | cat - <(echo -e ".text\n\t.align\t2, 0") | $$(AS) $$(ASFLAGS) -o $$@ -
endef
$(foreach src, $(TEST_SRCS), $(eval $(call TEST_DEP,$(patsubst $(TEST_SUBDIR)/%.c,$(TEST_BUILDDIR)/%.o,$(src)),$(src),$(patsubst $(TEST_SUBDIR)/%.c,%,$(src)))))
ifeq ($(MODERN),0)
LD_SCRIPT := ld_script.txt
LD_SCRIPT_DEPS := $(OBJ_DIR)/sym_bss.ld $(OBJ_DIR)/sym_common.ld $(OBJ_DIR)/sym_ewram.ld
@ -429,6 +450,28 @@ $(ROM): $(ELF)
modern: all
LD_SCRIPT_TEST := ld_script_test.txt
$(OBJ_DIR)/ld_script_test.ld: $(LD_SCRIPT_TEST) $(LD_SCRIPT_DEPS)
cd $(OBJ_DIR) && sed "s#tools/#../../tools/#g" ../../$(LD_SCRIPT_TEST) > ld_script_test.ld
$(TESTELF): $(OBJ_DIR)/ld_script_test.ld $(OBJS) $(TEST_OBJS) libagbsyscall
@echo "cd $(OBJ_DIR) && $(LD) -T ld_script_test.ld -o ../../$@ <objects> <test-objects> <lib>"
@cd $(OBJ_DIR) && $(LD) $(TESTLDFLAGS) -T ld_script_test.ld -o ../../$@ $(OBJS_REL) $(TEST_OBJS_REL) $(LIB)
$(FIX) $@ -t"$(TITLE)" -c$(GAME_CODE) -m$(MAKER_CODE) -r$(REVISION) --silent
$(PATCHELF) pokeemerald-test.elf gTestRunnerArgv "$(TESTS)\0"
ifeq ($(GITHUB_REPOSITORY_OWNER),rh-hideout)
TEST_SKIP_IS_FAIL := \x01
else
TEST_SKIP_IS_FAIL := \x00
endif
check: $(TESTELF)
@cp $< $(HEADLESSELF)
$(PATCHELF) $(HEADLESSELF) gTestRunnerHeadless '\x01' gTestRunnerSkipIsFail "$(TEST_SKIP_IS_FAIL)"
$(ROMTESTHYDRA) $(ROMTEST) $(HEADLESSELF)
libagbsyscall:
@$(MAKE) -C libagbsyscall TOOLCHAIN=$(TOOLCHAIN) MODERN=$(MODERN)

View File

@ -7,3 +7,4 @@ gIntrTable
gLinkVSyncDisabled
IntrMain_Buffer
gPcmDmaCounter
gAgbMainLoop_sp

View File

@ -139,6 +139,14 @@
#define NUM_FLAG_BYTES ROUND_BITS_TO_BYTES(FLAGS_COUNT)
#define NUM_ADDITIONAL_PHRASE_BYTES ROUND_BITS_TO_BYTES(NUM_ADDITIONAL_PHRASES)
// Calls m0/m1/.../m8 depending on how many arguments are passed.
#define VARARG_8(m, ...) CAT(m, NARG_8(__VA_ARGS__))(__VA_ARGS__)
#define NARG_8(...) NARG_8_(_, ##__VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#define NARG_8_(_, a, b, c, d, e, f, g, h, N, ...) N
#define CAT(a, b) CAT_(a, b)
#define CAT_(a, b) a ## b
// This produces an error at compile-time if expr is zero.
// It looks like file.c:line: size of array `id' is negative
#define STATIC_ASSERT(expr, id) typedef char id[(expr) ? 1 : -1];

View File

@ -57,6 +57,7 @@ extern u32 IntrMain_Buffer[];
extern s8 gPcmDmaCounter;
void AgbMain(void);
void AgbMainLoop(void);
void SetMainCallback2(MainCallback callback);
void InitKeys(void);
void SetVBlankCallback(IntrCallback callback);

View File

@ -563,5 +563,6 @@ u16 MonTryLearningNewMoveEvolution(struct Pokemon *mon, bool8 firstMove);
bool32 ShouldShowFemaleDifferences(u16 species, u32 personality);
void TryToSetBattleFormChangeMoves(struct Pokemon *mon);
u32 GetMonFriendshipScore(struct Pokemon *pokemon);
void UpdateMonPersonality(struct BoxPokemon *boxMon, u32 personality);
#endif // GUARD_POKEMON_H

View File

@ -1,6 +1,51 @@
#ifndef GUARD_RECORDED_BATTLE_H
#define GUARD_RECORDED_BATTLE_H
#include "constants/battle.h"
#define BATTLER_RECORD_SIZE 664
struct RecordedBattleSave
{
struct Pokemon playerParty[PARTY_SIZE];
struct Pokemon opponentParty[PARTY_SIZE];
u8 playersName[MAX_BATTLERS_COUNT][PLAYER_NAME_LENGTH + 1];
u8 playersGender[MAX_BATTLERS_COUNT];
u32 playersTrainerId[MAX_BATTLERS_COUNT];
u8 playersLanguage[MAX_BATTLERS_COUNT];
u32 rngSeed;
u32 battleFlags;
u8 playersBattlers[MAX_BATTLERS_COUNT];
u16 opponentA;
u16 opponentB;
u16 partnerId;
u16 multiplayerId;
u8 lvlMode;
u8 frontierFacility;
u8 frontierBrainSymbol;
u8 battleScene:1;
u8 textSpeed:3;
u32 AI_scripts;
u8 recordMixFriendName[PLAYER_NAME_LENGTH + 1];
u8 recordMixFriendClass;
u8 apprenticeId;
u16 easyChatSpeech[EASY_CHAT_BATTLE_WORDS_COUNT];
u8 recordMixFriendLanguage;
u8 apprenticeLanguage;
u8 battleRecord[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE];
u32 checksum;
};
enum
{
RECORDED_BYTE, // Generic.
RECORDED_ACTION_TYPE,
RECORDED_MOVE_SLOT,
RECORDED_MOVE_TARGET,
RECORDED_PARTY_INDEX,
RECORDED_BATTLE_PALACE_ACTION,
};
extern u32 gRecordedBattleRngSeed;
extern u32 gBattlePalaceMoveSelectionRngValue;
extern u8 gRecordedBattleMultiplayerId;
@ -12,11 +57,12 @@ void RecordedBattle_Init(u8 mode);
void RecordedBattle_SetTrainerInfo(void);
void RecordedBattle_SetBattlerAction(u8 battlerId, u8 action);
void RecordedBattle_ClearBattlerAction(u8 battlerId, u8 bytesToClear);
u8 RecordedBattle_GetBattlerAction(u8 battlerId);
u8 RecordedBattle_GetBattlerAction(u32 actionType, u8 battlerId);
u8 RecordedBattle_BufferNewBattlerData(u8 *dst);
void RecordedBattle_RecordAllBattlerData(u8 *data);
bool32 CanCopyRecordedBattleSaveData(void);
bool32 MoveRecordedBattleToSaveData(void);
void SetVariablesForRecordedBattle(struct RecordedBattleSave *);
void PlayRecordedBattle(void (*CB2_After)(void));
u8 GetRecordedBattleFrontierFacility(void);
u8 GetRecordedBattleFronterBrainSymbol(void);

17
include/test_runner.h Normal file
View File

@ -0,0 +1,17 @@
#ifndef GUARD_TEST_RUNNER_H
#define GUARD_TEST_RUNNER_H
extern const bool8 gTestRunnerEnabled;
extern const bool8 gTestRunnerHeadless;
extern const bool8 gTestRunnerSkipIsFail;
void TestRunner_Battle_RecordAbilityPopUp(u32 battlerId, u32 ability);
void TestRunner_Battle_RecordAnimation(u32 animType, u32 animId);
void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP);
void TestRunner_Battle_RecordMessage(const u8 *message);
void TestRunner_Battle_RecordStatus1(u32 battlerId, u32 status1);
void TestRunner_Battle_AfterLastTurn(void);
void BattleTest_CheckBattleRecordActionType(u32 battlerId, u32 recordIndex, u32 actionType);
#endif

View File

@ -2,6 +2,7 @@ ENTRY(Start)
gNumMusicPlayers = 4;
gMaxLines = 0;
gInitialMainCB2 = CB2_InitCopyrightScreenAfterBootup;
SECTIONS {
. = 0x2000000;

View File

@ -2,6 +2,7 @@ ENTRY(Start)
gNumMusicPlayers = 4;
gMaxLines = 0;
gInitialMainCB2 = CB2_InitCopyrightScreenAfterBootup;
SECTIONS {
. = 0x2000000;

140
ld_script_test.txt Normal file
View File

@ -0,0 +1,140 @@
ENTRY(Start)
gNumMusicPlayers = 4;
gMaxLines = 0;
gInitialMainCB2 = CB2_TestRunner;
SECTIONS {
. = 0x2000000;
ewram (NOLOAD) :
ALIGN(4)
{
gHeap = .;
. = 0x1C000;
src/*.o(ewram_data);
gflib/*.o(ewram_data);
test/*.o(ewram_data);
. = 0x40000;
}
. = 0x3000000;
iwram (NOLOAD) :
ALIGN(4)
{
/* .bss starts at 0x3000000 */
src/*.o(.bss);
gflib/*.o(.bss);
data/*.o(.bss);
test/*.o(.bss);
*libc.a:*.o(.bss*);
*libgcc.a:*.o(.bss*);
*libnosys.a:*.o(.bss*);
/* .bss.code starts at 0x3001AA8 */
src/m4a.o(.bss.code);
/* COMMON starts at 0x30022A8 */
src/*.o(COMMON);
gflib/*.o(COMMON);
data/*.o(COMMON);
test/*.o(COMMON);
*libc.a:sbrkr.o(COMMON);
end = .;
. = 0x8000;
}
. = 0x8000000;
.text :
ALIGN(4)
{
src/rom_header.o(.text);
src/rom_header_gf.o(.text.*);
src/*.o(.text);
gflib/*.o(.text);
} =0
script_data :
ALIGN(4)
{
data/*.o(script_data);
} =0
lib_text :
ALIGN(4)
{
*libagbsyscall.a:*.o(.text*);
*libgcc.a:*.o(.text*);
*libc.a:*.o(.text*);
*libnosys.a:*.o(.text*);
} =0
.rodata :
ALIGN(4)
{
src/*.o(.rodata);
gflib/*.o(.rodata);
data/*.o(.rodata);
} =0
song_data :
ALIGN(4)
{
sound/songs/*.o(.rodata);
} =0
lib_rodata :
SUBALIGN(4)
{
*libgcc.a:*.o(.rodata*);
*libc.a:*.o(.rodata*);
*libc.a:*.o(.data*);
src/libisagbprn.o(.rodata);
} =0
tests :
ALIGN(4)
{
__start_tests = .;
test/*.o(.tests);
__stop_tests = .;
test/*.o(.text);
test/*.o(.rodata);
} =0
/* DWARF debug sections.
Symbols in the DWARF debugging sections are relative to the beginning
of the section so we begin them at 0. */
/* DWARF 1 */
.debug 0 : { *(.debug) }
.line 0 : { *(.line) }
/* GNU DWARF 1 extensions */
.debug_srcinfo 0 : { *(.debug_srcinfo) }
.debug_sfnames 0 : { *(.debug_sfnames) }
/* DWARF 1.1 and DWARF 2 */
.debug_aranges 0 : { *(.debug_aranges) }
.debug_pubnames 0 : { *(.debug_pubnames) }
/* DWARF 2 */
.debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) }
.debug_abbrev 0 : { *(.debug_abbrev) }
.debug_line 0 : { *(.debug_line) }
.debug_frame 0 : { *(.debug_frame) }
.debug_str 0 : { *(.debug_str) }
.debug_loc 0 : { *(.debug_loc) }
.debug_macinfo 0 : { *(.debug_macinfo) }
/* Discard everything not specifically mentioned above. */
/DISCARD/ :
{
*(*);
}
}

View File

@ -1,7 +1,7 @@
MAKEFLAGS += --no-print-directory
TOOLDIRS := $(filter-out tools/agbcc tools/binutils,$(wildcard tools/*))
TOOLDIRS := $(filter-out tools/mgba tools/agbcc tools/binutils,$(wildcard tools/*))
.PHONY: all $(TOOLDIRS)

View File

@ -16,6 +16,7 @@
#include "sound.h"
#include "sprite.h"
#include "task.h"
#include "test_runner.h"
#include "constants/battle_anim.h"
#include "constants/moves.h"
@ -217,12 +218,27 @@ void DoMoveAnim(u16 move)
LaunchBattleAnimation(ANIM_TYPE_MOVE, move);
}
static void Nop(void)
{
}
void LaunchBattleAnimation(u32 animType, u32 animId)
{
s32 i;
const u8 *const *animsTable;
bool32 hideHpBoxes;
if (gTestRunnerEnabled)
{
TestRunner_Battle_RecordAnimation(animType, animId);
if (gTestRunnerHeadless)
{
gAnimScriptCallback = Nop;
gAnimScriptActive = FALSE;
return;
}
}
switch (animType)
{
case ANIM_TYPE_GENERAL:
@ -239,7 +255,7 @@ void LaunchBattleAnimation(u32 animType, u32 animId)
break;
}
hideHpBoxes = !(animType == ANIM_TYPE_MOVE && animId == MOVE_TRANSFORM);
hideHpBoxes = !(animType == ANIM_TYPE_MOVE && animId == MOVE_TRANSFORM);
if (animType != ANIM_TYPE_MOVE)
{
switch (animId)

View File

@ -22,6 +22,7 @@
#include "sound.h"
#include "string_util.h"
#include "task.h"
#include "test_runner.h"
#include "text.h"
#include "util.h"
#include "window.h"
@ -1386,6 +1387,17 @@ static void RecordedOpponentHandlePrintString(void)
gBattle_BG0_Y = 0;
stringId = (u16 *)(&gBattleResources->bufferA[gActiveBattler][2]);
BufferStringBattle(*stringId);
if (gTestRunnerEnabled)
{
TestRunner_Battle_RecordMessage(gDisplayedStringBattle);
if (gTestRunnerHeadless)
{
RecordedOpponentBufferExecCompleted();
return;
}
}
BattlePutTextOnWindow(gDisplayedStringBattle, B_WIN_MSG);
gBattlerControllerFuncs[gActiveBattler] = CompleteOnInactiveTextPrinter;
}
@ -1397,7 +1409,7 @@ static void RecordedOpponentHandlePrintSelectionString(void)
static void RecordedOpponentHandleChooseAction(void)
{
BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(gActiveBattler), 0);
BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(RECORDED_ACTION_TYPE, gActiveBattler), 0);
RecordedOpponentBufferExecCompleted();
}
@ -1414,8 +1426,8 @@ static void RecordedOpponentHandleChooseMove(void)
}
else
{
u8 moveId = RecordedBattle_GetBattlerAction(gActiveBattler);
u8 target = RecordedBattle_GetBattlerAction(gActiveBattler);
u8 moveId = RecordedBattle_GetBattlerAction(RECORDED_MOVE_SLOT, gActiveBattler);
u8 target = RecordedBattle_GetBattlerAction(RECORDED_MOVE_TARGET, gActiveBattler);
BtlController_EmitTwoReturnValues(BUFFER_B, 10, moveId | (target << 8));
}
@ -1429,7 +1441,7 @@ static void RecordedOpponentHandleChooseItem(void)
static void RecordedOpponentHandleChoosePokemon(void)
{
*(gBattleStruct->monToSwitchIntoId + gActiveBattler) = RecordedBattle_GetBattlerAction(gActiveBattler);
*(gBattleStruct->monToSwitchIntoId + gActiveBattler) = RecordedBattle_GetBattlerAction(RECORDED_PARTY_INDEX, gActiveBattler);
BtlController_EmitChosenMonReturnValue(BUFFER_B, *(gBattleStruct->monToSwitchIntoId + gActiveBattler), NULL);
RecordedOpponentBufferExecCompleted();
}
@ -1442,22 +1454,23 @@ static void RecordedOpponentHandleCmd23(void)
static void RecordedOpponentHandleHealthBarUpdate(void)
{
s16 hpVal;
s32 maxHP, curHP;
LoadBattleBarGfx(0);
hpVal = gBattleResources->bufferA[gActiveBattler][2] | (gBattleResources->bufferA[gActiveBattler][3] << 8);
maxHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP);
curHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_HP);
if (hpVal != INSTANT_HP_BAR_DROP)
{
u32 maxHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP);
u32 curHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_HP);
SetBattleBarStruct(gActiveBattler, gHealthboxSpriteIds[gActiveBattler], maxHP, curHP, hpVal);
TestRunner_Battle_RecordHP(gActiveBattler, curHP, min(maxHP, max(0, curHP - hpVal)));
}
else
{
u32 maxHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP);
SetBattleBarStruct(gActiveBattler, gHealthboxSpriteIds[gActiveBattler], maxHP, 0, hpVal);
TestRunner_Battle_RecordHP(gActiveBattler, curHP, 0);
}
gBattlerControllerFuncs[gActiveBattler] = CompleteOnHealthbarDone;
@ -1478,6 +1491,9 @@ static void RecordedOpponentHandleStatusIconUpdate(void)
battlerId = gActiveBattler;
gBattleSpritesDataPtr->healthBoxesData[battlerId].statusAnimActive = 0;
gBattlerControllerFuncs[gActiveBattler] = CompleteOnFinishedStatusAnimation;
if (gTestRunnerEnabled)
TestRunner_Battle_RecordStatus1(battlerId, GetMonData(&gEnemyParty[gBattlerPartyIndexes[battlerId]], MON_DATA_STATUS));
}
}

View File

@ -19,6 +19,7 @@
#include "sound.h"
#include "string_util.h"
#include "task.h"
#include "test_runner.h"
#include "text.h"
#include "util.h"
#include "window.h"
@ -1394,6 +1395,17 @@ static void RecordedPlayerHandlePrintString(void)
gBattle_BG0_Y = 0;
stringId = (u16 *)(&gBattleResources->bufferA[gActiveBattler][2]);
BufferStringBattle(*stringId);
if (gTestRunnerEnabled)
{
TestRunner_Battle_RecordMessage(gDisplayedStringBattle);
if (gTestRunnerHeadless)
{
RecordedPlayerBufferExecCompleted();
return;
}
}
BattlePutTextOnWindow(gDisplayedStringBattle, B_WIN_MSG);
gBattlerControllerFuncs[gActiveBattler] = CompleteOnInactiveTextPrinter;
}
@ -1407,7 +1419,7 @@ static void ChooseActionInBattlePalace(void)
{
if (gBattleCommunication[4] >= gBattlersCount / 2)
{
BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(gActiveBattler), 0);
BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(RECORDED_BATTLE_PALACE_ACTION, gActiveBattler), 0);
RecordedPlayerBufferExecCompleted();
}
}
@ -1420,7 +1432,7 @@ static void RecordedPlayerHandleChooseAction(void)
}
else
{
BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(gActiveBattler), 0);
BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(RECORDED_ACTION_TYPE, gActiveBattler), 0);
RecordedPlayerBufferExecCompleted();
}
}
@ -1438,8 +1450,8 @@ static void RecordedPlayerHandleChooseMove(void)
}
else
{
u8 moveId = RecordedBattle_GetBattlerAction(gActiveBattler);
u8 target = RecordedBattle_GetBattlerAction(gActiveBattler);
u8 moveId = RecordedBattle_GetBattlerAction(RECORDED_MOVE_SLOT, gActiveBattler);
u8 target = RecordedBattle_GetBattlerAction(RECORDED_MOVE_TARGET, gActiveBattler);
BtlController_EmitTwoReturnValues(BUFFER_B, 10, moveId | (target << 8));
}
@ -1453,7 +1465,7 @@ static void RecordedPlayerHandleChooseItem(void)
static void RecordedPlayerHandleChoosePokemon(void)
{
*(gBattleStruct->monToSwitchIntoId + gActiveBattler) = RecordedBattle_GetBattlerAction(gActiveBattler);
*(gBattleStruct->monToSwitchIntoId + gActiveBattler) = RecordedBattle_GetBattlerAction(RECORDED_PARTY_INDEX, gActiveBattler);
BtlController_EmitChosenMonReturnValue(BUFFER_B, *(gBattleStruct->monToSwitchIntoId + gActiveBattler), NULL);
RecordedPlayerBufferExecCompleted();
}
@ -1466,23 +1478,24 @@ static void RecordedPlayerHandleCmd23(void)
static void RecordedPlayerHandleHealthBarUpdate(void)
{
s16 hpVal;
s32 maxHP, curHP;
LoadBattleBarGfx(0);
hpVal = gBattleResources->bufferA[gActiveBattler][2] | (gBattleResources->bufferA[gActiveBattler][3] << 8);
maxHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP);
curHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_HP);
if (hpVal != INSTANT_HP_BAR_DROP)
{
u32 maxHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP);
u32 curHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_HP);
SetBattleBarStruct(gActiveBattler, gHealthboxSpriteIds[gActiveBattler], maxHP, curHP, hpVal);
TestRunner_Battle_RecordHP(gActiveBattler, curHP, min(maxHP, max(0, curHP - hpVal)));
}
else
{
u32 maxHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP);
SetBattleBarStruct(gActiveBattler, gHealthboxSpriteIds[gActiveBattler], maxHP, 0, hpVal);
UpdateHpTextInHealthbox(gHealthboxSpriteIds[gActiveBattler], HP_CURRENT, 0, maxHP);
TestRunner_Battle_RecordHP(gActiveBattler, curHP, 0);
}
gBattlerControllerFuncs[gActiveBattler] = CompleteOnHealthbarDone;
@ -1503,6 +1516,9 @@ static void RecordedPlayerHandleStatusIconUpdate(void)
battlerId = gActiveBattler;
gBattleSpritesDataPtr->healthBoxesData[battlerId].statusAnimActive = 0;
gBattlerControllerFuncs[gActiveBattler] = CompleteOnFinishedStatusAnimation;
if (gTestRunnerEnabled)
TestRunner_Battle_RecordStatus1(battlerId, GetMonData(&gPlayerParty[gBattlerPartyIndexes[battlerId]], MON_DATA_STATUS));
}
}

View File

@ -28,6 +28,7 @@
#include "item.h"
#include "item_icon.h"
#include "item_use.h"
#include "test_runner.h"
#include "constants/battle_anim.h"
#include "constants/rgb.h"
#include "constants/songs.h"
@ -3072,6 +3073,13 @@ void CreateAbilityPopUp(u8 battlerId, u32 ability, bool32 isDoubleBattle)
const s16 (*coords)[2];
u8 spriteId1, spriteId2, battlerPosition, taskId;
if (gTestRunnerEnabled)
{
TestRunner_Battle_RecordAbilityPopUp(battlerId, ability);
if (gTestRunnerHeadless)
return;
}
if (gBattleScripting.abilityPopupOverwrite != 0)
ability = gBattleScripting.abilityPopupOverwrite;
@ -3187,9 +3195,12 @@ static void SpriteCb_AbilityPopUp(struct Sprite *sprite)
void DestroyAbilityPopUp(u8 battlerId)
{
gSprites[gBattleStruct->abilityPopUpSpriteIds[battlerId][0]].tFrames = 0;
gSprites[gBattleStruct->abilityPopUpSpriteIds[battlerId][1]].tFrames = 0;
gBattleScripting.fixedPopup = FALSE;
if (gBattleStruct->activeAbilityPopUps & gBitTable[battlerId])
{
gSprites[gBattleStruct->abilityPopUpSpriteIds[battlerId][0]].tFrames = 0;
gSprites[gBattleStruct->abilityPopUpSpriteIds[battlerId][1]].tFrames = 0;
gBattleScripting.fixedPopup = FALSE;
}
}
static void Task_FreeAbilityPopUpGfx(u8 taskId)

View File

@ -45,6 +45,7 @@
#include "string_util.h"
#include "strings.h"
#include "task.h"
#include "test_runner.h"
#include "text.h"
#include "trig.h"
#include "tv.h"
@ -474,7 +475,8 @@ const u8 *const gStatusConditionStringsTable[][2] =
void CB2_InitBattle(void)
{
MoveSaveBlocks_ResetHeap();
if (!gTestRunnerEnabled)
MoveSaveBlocks_ResetHeap();
AllocateBattleResources();
AllocateBattleSpritesData();
AllocateMonSpritesGfx();
@ -1805,6 +1807,8 @@ void CB2_QuitRecordedBattle(void)
{
m4aMPlayStop(&gMPlayInfo_SE1);
m4aMPlayStop(&gMPlayInfo_SE2);
if (gTestRunnerEnabled)
TestRunner_Battle_AfterLastTurn();
FreeRestoreBattleData();
FreeAllWindowBuffers();
SetMainCallback2(gMain.savedCallback);
@ -5233,6 +5237,8 @@ static void HandleEndTurn_FinishBattle(void)
}
RecordedBattle_SetPlaybackFinished();
if (gTestRunnerEnabled)
TestRunner_Battle_AfterLastTurn();
BeginFastPaletteFade(3);
FadeOutMapMusic(5);
#if B_TRAINERS_KNOCK_OFF_ITEMS == TRUE

View File

@ -524,11 +524,11 @@ static const u8 sText_Trainer2LoseText[];
static const s8 sText_EnduredViaSturdy[] = _("{B_DEF_NAME_WITH_PREFIX} endured\nthe hit using {B_DEF_ABILITY}!");
static const s8 sText_PowerHerbActivation[] = _("{B_ATK_NAME_WITH_PREFIX} became fully charged\ndue to its {B_LAST_ITEM}!");
static const s8 sText_HurtByItem[] = _("{B_ATK_NAME_WITH_PREFIX} was hurt\nby its {B_LAST_ITEM}!");
static const s8 sText_BadlyPoisonedByItem[] = _("{B_EFF_NAME_WITH_PREFIX} was badly \npoisoned by the {B_LAST_ITEM}!");
static const s8 sText_BadlyPoisonedByItem[] = _("{B_EFF_NAME_WITH_PREFIX} was badly\npoisoned by the {B_LAST_ITEM}!");
static const s8 sText_BurnedByItem[] = _("{B_EFF_NAME_WITH_PREFIX} was burned\nby the {B_LAST_ITEM}!");
static const s8 sText_TargetAbilityActivates[] = _("{B_DEF_NAME_WITH_PREFIX}'s {B_DEF_ABILITY} activates!");
static const u8 sText_GravityIntensified[] = _("Gravity intensified!");
static const u8 sText_TargetIdentified[] = _("{B_DEF_NAME_WITH_PREFIX} was \nidentified!");
static const u8 sText_TargetIdentified[] = _("{B_DEF_NAME_WITH_PREFIX} was\nidentified!");
static const u8 sText_TargetWokeUp[] = _("{B_DEF_NAME_WITH_PREFIX} woke up!");
static const u8 sText_PkmnStoleAndAteItem[] = _("{B_ATK_NAME_WITH_PREFIX} stole and\nate {B_DEF_NAME_WITH_PREFIX}'s {B_LAST_ITEM}!");
static const u8 sText_TailWindBlew[] = _("The tailwind blew from\nbehind {B_ATK_TEAM2} team!");
@ -597,7 +597,7 @@ static const u8 sText_TargetsStatWasMaxedOut[] = _("{B_DEF_NAME_WITH_PREFIX}'s {
static const u8 sText_PoisonHealHpUp[] = _("The poisoning healed {B_ATK_NAME_WITH_PREFIX}\na little bit!");
static const u8 sText_BadDreamsDmg[] = _("{B_DEF_NAME_WITH_PREFIX} is tormented!");
static const u8 sText_MoldBreakerEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} breaks the mold!");
static const u8 sText_TeravoltEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} is radiating \na bursting aura!");
static const u8 sText_TeravoltEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} is radiating\na bursting aura!");
static const u8 sText_TurboblazeEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} is radiating\na blazing aura!");
static const u8 sText_SlowStartEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} can't get it going!");
static const u8 sText_SlowStartEnd[] = _("{B_ATK_NAME_WITH_PREFIX} finally got\nits act together!");
@ -642,7 +642,7 @@ static const u8 sText_TargetElectrified[] = _("The {B_DEF_NAME_WITH_PREFIX}'s mo
static const u8 sText_AssaultVestDoesntAllow[] = _("{B_LAST_ITEM}'s effects prevent\nstatus moves from being used!\p");
static const u8 sText_GravityPreventsUsage[] = _("{B_ATK_NAME_WITH_PREFIX} can't use {B_CURRENT_MOVE}\nbecause of gravity!\p");
static const u8 sText_HealBlockPreventsUsage[] = _("{B_ATK_NAME_WITH_PREFIX} was\nprevented from healing!\p");
static const u8 sText_MegaEvoReacting[] = _("{B_ATK_NAME_WITH_PREFIX}'s {B_LAST_ITEM} is \nreacting to {B_ATK_TRAINER_NAME}'s Mega Ring!");
static const u8 sText_MegaEvoReacting[] = _("{B_ATK_NAME_WITH_PREFIX}'s {B_LAST_ITEM} is\nreacting to {B_ATK_TRAINER_NAME}'s Mega Ring!");
static const u8 sText_FerventWishReached[] = _("{B_ATK_TRAINER_NAME}'s fervent wish\nhas reached {B_ATK_NAME_WITH_PREFIX}!");
static const u8 sText_MegaEvoEvolved[] = _("{B_ATK_NAME_WITH_PREFIX} has Mega Evolved into\nMega {B_BUFF1}!");
static const u8 sText_drastically[] = _("drastically ");

View File

@ -134,7 +134,7 @@ static const u8 sWireless_ASCIItoRSETable[256] = {
0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94
};
static const u8 sWireless_RSEtoASCIITable[256] = {
const u8 gWireless_RSEtoASCIITable[256] = {
[CHAR_SPACE] = ' ',
0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d,
0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95,
@ -612,7 +612,7 @@ static void PkmnStrToASCII(u8 *asciiStr, const u8 *pkmnStr)
s32 i;
for (i = 0; pkmnStr[i] != EOS; i++)
asciiStr[i] = sWireless_RSEtoASCIITable[pkmnStr[i]];
asciiStr[i] = gWireless_RSEtoASCIITable[pkmnStr[i]];
asciiStr[i] = 0;
}

View File

@ -31,6 +31,9 @@ static void VCountIntr(void);
static void SerialIntr(void);
static void IntrDummy(void);
// Defined in the linker script so that the test build can override it.
extern void gInitialMainCB2(void);
const u8 gGameVersion = GAME_VERSION;
const u8 gGameLanguage = GAME_LANGUAGE; // English
@ -68,6 +71,7 @@ IntrFunc gIntrTable[INTR_COUNT];
u8 gLinkVSyncDisabled;
u32 IntrMain_Buffer[0x200];
s8 gPcmDmaCounter;
void *gAgbMainLoop_sp;
static EWRAM_DATA u16 sTrainerId = 0;
@ -126,6 +130,12 @@ void AgbMain()
AGBPrintfInit();
#endif
#endif
gAgbMainLoop_sp = __builtin_frame_address(0);
AgbMainLoop();
}
void AgbMainLoop(void)
{
for (;;)
{
ReadKeys();
@ -178,7 +188,7 @@ static void InitMainCallbacks(void)
gTrainerHillVBlankCounter = NULL;
gMain.vblankCounter2 = 0;
gMain.callback1 = NULL;
SetMainCallback2(CB2_InitCopyrightScreenAfterBootup);
SetMainCallback2(gInitialMainCB2);
gSaveBlock2Ptr = &gSaveblock2.block;
gPokemonStoragePtr = &gPokemonStorage.block;
}

View File

@ -8681,3 +8681,32 @@ u32 GetMonFriendshipScore(struct Pokemon *pokemon)
return FRIENDSHIP_NONE;
}
void UpdateMonPersonality(struct BoxPokemon *boxMon, u32 personality)
{
struct PokemonSubstruct0 *old0, *new0;
struct PokemonSubstruct1 *old1, *new1;
struct PokemonSubstruct2 *old2, *new2;
struct PokemonSubstruct3 *old3, *new3;
struct BoxPokemon old;
old = *boxMon;
old0 = &(GetSubstruct(&old, old.personality, 0)->type0);
old1 = &(GetSubstruct(&old, old.personality, 1)->type1);
old2 = &(GetSubstruct(&old, old.personality, 2)->type2);
old3 = &(GetSubstruct(&old, old.personality, 3)->type3);
new0 = &(GetSubstruct(boxMon, personality, 0)->type0);
new1 = &(GetSubstruct(boxMon, personality, 1)->type1);
new2 = &(GetSubstruct(boxMon, personality, 2)->type2);
new3 = &(GetSubstruct(boxMon, personality, 3)->type3);
DecryptBoxMon(&old);
boxMon->personality = personality;
*new0 = *old0;
*new1 = *old1;
*new2 = *old2;
*new3 = *old3;
boxMon->checksum = CalculateBoxMonChecksum(boxMon);
EncryptBoxMon(boxMon);
}

View File

@ -14,14 +14,13 @@
#include "malloc.h"
#include "util.h"
#include "task.h"
#include "test_runner.h"
#include "text.h"
#include "battle_setup.h"
#include "frontier_util.h"
#include "constants/trainers.h"
#include "constants/rgb.h"
#define BATTLER_RECORD_SIZE 664
struct PlayerInfo
{
u32 trainerId;
@ -31,37 +30,6 @@ struct PlayerInfo
u16 language;
};
struct RecordedBattleSave
{
struct Pokemon playerParty[PARTY_SIZE];
struct Pokemon opponentParty[PARTY_SIZE];
u8 playersName[MAX_BATTLERS_COUNT][PLAYER_NAME_LENGTH + 1];
u8 playersGender[MAX_BATTLERS_COUNT];
u32 playersTrainerId[MAX_BATTLERS_COUNT];
u8 playersLanguage[MAX_BATTLERS_COUNT];
u32 rngSeed;
u32 battleFlags;
u8 playersBattlers[MAX_BATTLERS_COUNT];
u16 opponentA;
u16 opponentB;
u16 partnerId;
u16 multiplayerId;
u8 lvlMode;
u8 frontierFacility;
u8 frontierBrainSymbol;
u8 battleScene:1;
u8 textSpeed:3;
u32 AI_scripts;
u8 recordMixFriendName[PLAYER_NAME_LENGTH + 1];
u8 recordMixFriendClass;
u8 apprenticeId;
u16 easyChatSpeech[EASY_CHAT_BATTLE_WORDS_COUNT];
u8 recordMixFriendLanguage;
u8 apprenticeLanguage;
u8 battleRecord[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE];
u32 checksum;
};
// Save data using TryWriteSpecialSaveSector is allowed to exceed SECTOR_DATA_SIZE (up to the counter field)
STATIC_ASSERT(sizeof(struct RecordedBattleSave) <= SECTOR_COUNTER_OFFSET, RecordedBattleSaveFreeSpace);
@ -205,8 +173,11 @@ void RecordedBattle_ClearBattlerAction(u8 battlerId, u8 bytesToClear)
}
}
u8 RecordedBattle_GetBattlerAction(u8 battlerId)
u8 RecordedBattle_GetBattlerAction(u32 actionType, u8 battlerId)
{
if (gTestRunnerEnabled)
BattleTest_CheckBattleRecordActionType(battlerId, sBattlerRecordSizes[battlerId], actionType);
// Trying to read past array or invalid action byte, battle is over.
if (sBattlerRecordSizes[battlerId] >= BATTLER_RECORD_SIZE || sBattleRecords[battlerId][sBattlerRecordSizes[battlerId]] == 0xFF)
{
@ -522,7 +493,7 @@ static void Task_StartAfterCountdown(u8 taskId)
}
}
static void SetVariablesForRecordedBattle(struct RecordedBattleSave *src)
void SetVariablesForRecordedBattle(struct RecordedBattleSave *src)
{
bool8 var;
s32 i, j;
@ -755,14 +726,14 @@ void RecordedBattle_CheckMovesetChanges(u8 mode)
// We know the current action is ACTION_MOVE_CHANGE, retrieve
// it without saving it to move on to the next action.
RecordedBattle_GetBattlerAction(battlerId);
RecordedBattle_GetBattlerAction(RECORDED_BYTE, battlerId);
for (j = 0; j < MAX_MON_MOVES; j++)
ppBonuses[j] = ((gBattleMons[battlerId].ppBonuses & (3 << (j << 1))) >> (j << 1));
for (j = 0; j < MAX_MON_MOVES; j++)
{
moveSlots[j] = RecordedBattle_GetBattlerAction(battlerId);
moveSlots[j] = RecordedBattle_GetBattlerAction(RECORDED_BYTE, battlerId);
movePp.moves[j] = gBattleMons[battlerId].moves[moveSlots[j]];
movePp.currentPp[j] = gBattleMons[battlerId].pp[moveSlots[j]];
movePp.maxPp[j] = ppBonuses[moveSlots[j]];

46
src/test_runner_stub.c Normal file
View File

@ -0,0 +1,46 @@
#include "global.h"
#include "test_runner.h"
__attribute__((weak))
const bool8 gTestRunnerEnabled = FALSE;
// The Makefile patches gTestRunnerHeadless as part of make test.
// This allows us to open the ROM in an mgba with a UI and see the
// animations and messages play, which helps when debugging a test.
const bool8 gTestRunnerHeadless = FALSE;
const bool8 gTestRunnerSkipIsFail = FALSE;
__attribute__((weak))
void TestRunner_Battle_RecordAbilityPopUp(u32 battlerId, u32 ability)
{
}
__attribute__((weak))
void TestRunner_Battle_RecordAnimation(u32 animType, u32 animId)
{
}
__attribute__((weak))
void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP)
{
}
__attribute__((weak))
void TestRunner_Battle_RecordMessage(const u8 *string)
{
}
__attribute__((weak))
void TestRunner_Battle_RecordStatus1(u32 battlerId, u32 status1)
{
}
__attribute__((weak))
void TestRunner_Battle_AfterLastTurn(void)
{
}
__attribute__((weak))
void BattleTest_CheckBattleRecordActionType(u32 battlerId, u32 recordIndex, u32 actionType)
{
}

20
test/ability_blaze.c Normal file
View File

@ -0,0 +1,20 @@
#include "global.h"
#include "test_battle.h"
SINGLE_BATTLE_TEST("Blaze boosts Fire-type moves in a pinch", s16 damage)
{
u16 hp;
PARAMETRIZE { hp = 99; }
PARAMETRIZE { hp = 33; }
GIVEN {
ASSUME(gBattleMoves[MOVE_EMBER].type == TYPE_FIRE);
PLAYER(SPECIES_CHARMANDER) { Ability(ABILITY_BLAZE); MaxHP(99); HP(hp); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_EMBER); }
} SCENE {
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_GT(results[1].damage, results[0].damage);
}
}

48
test/ability_cute_charm.c Normal file
View File

@ -0,0 +1,48 @@
#include "global.h"
#include "test_battle.h"
// TODO: Currently PASSES_RANDOMLY is incapable of testing Cute Charm
// because it only activates 33% of the time, but we only want to
// measure the 50% of the time that the infatuation prevents our move.
SINGLE_BATTLE_TEST("Cute Charm inflicts infatuation on contact")
{
u32 move;
PARAMETRIZE { move = MOVE_TACKLE; }
PARAMETRIZE { move = MOVE_SWIFT; }
GIVEN {
ASSUME(gBattleMoves[MOVE_TACKLE].flags & FLAG_MAKES_CONTACT);
ASSUME(!(gBattleMoves[MOVE_SWIFT].flags & FLAG_MAKES_CONTACT));
PLAYER(SPECIES_WOBBUFFET) { Gender(MON_MALE); }
OPPONENT(SPECIES_CLEFAIRY) { Gender(MON_FEMALE); Ability(ABILITY_CUTE_CHARM); }
} WHEN {
TURN { MOVE(player, move); }
TURN { MOVE(player, move); }
} SCENE {
if (gBattleMoves[move].flags & FLAG_MAKES_CONTACT) {
ABILITY_POPUP(opponent, ABILITY_CUTE_CHARM);
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_INFATUATION, player);
MESSAGE("Foe Clefairy's Cute Charm infatuated Wobbuffet!");
MESSAGE("Wobbuffet is in love with Foe Clefairy!");
} else {
NOT ABILITY_POPUP(opponent, ABILITY_CUTE_CHARM);
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_INFATUATION, player);
NOT MESSAGE("Foe Clefairy's Cute Charm infatuated Wobbuffet!");
NOT MESSAGE("Wobbuffet is in love with Foe Clefairy!");
}
}
}
SINGLE_BATTLE_TEST("Cute Charm cannot infatuate same gender")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Gender(MON_MALE); }
OPPONENT(SPECIES_CLEFAIRY) { Gender(MON_MALE); Ability(ABILITY_CUTE_CHARM); }
} WHEN {
TURN { MOVE(player, MOVE_TACKLE); }
TURN { MOVE(player, MOVE_TACKLE); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
NOT ABILITY_POPUP(opponent, ABILITY_CUTE_CHARM);
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
}
}

29
test/ability_flame_body.c Normal file
View File

@ -0,0 +1,29 @@
#include "global.h"
#include "test_battle.h"
SINGLE_BATTLE_TEST("Flame Body inflicts burn on contact")
{
u32 move;
PARAMETRIZE { move = MOVE_TACKLE; }
PARAMETRIZE { move = MOVE_SWIFT; }
GIVEN {
ASSUME(gBattleMoves[MOVE_TACKLE].flags & FLAG_MAKES_CONTACT);
ASSUME(!(gBattleMoves[MOVE_SWIFT].flags & FLAG_MAKES_CONTACT));
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_MAGMAR) { Ability(ABILITY_FLAME_BODY); }
} WHEN {
TURN { MOVE(player, move); }
} SCENE {
if (gBattleMoves[move].flags & FLAG_MAKES_CONTACT) {
ABILITY_POPUP(opponent, ABILITY_FLAME_BODY);
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_BRN, player);
MESSAGE("Foe Magmar's Flame Body burned Wobbuffet!");
STATUS_ICON(player, burn: TRUE);
} else {
NOT ABILITY_POPUP(opponent, ABILITY_FLAME_BODY);
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_BRN, player);
NOT MESSAGE("Foe Magmar's Flame Body burned Wobbuffet!");
NOT STATUS_ICON(player, burn: TRUE);
}
}
}

47
test/ability_immunity.c Normal file
View File

@ -0,0 +1,47 @@
#include "global.h"
#include "test_battle.h"
SINGLE_BATTLE_TEST("Immunity prevents Poison Sting poison")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_SNORLAX) { Ability(ABILITY_IMMUNITY); }
} WHEN {
TURN { MOVE(player, MOVE_POISON_STING); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, player);
NOT STATUS_ICON(opponent, poison: TRUE);
}
}
SINGLE_BATTLE_TEST("Immunity prevents Toxic bad poison")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_SNORLAX) { Ability(ABILITY_IMMUNITY); }
} WHEN {
TURN { MOVE(player, MOVE_TOXIC); }
} SCENE {
MESSAGE("Wobbuffet used Toxic!");
ABILITY_POPUP(opponent, ABILITY_IMMUNITY);
MESSAGE("Foe Snorlax's Immunity prevents poisoning!");
NOT STATUS_ICON(opponent, poison: TRUE);
}
}
SINGLE_BATTLE_TEST("Immunity prevents Toxic Spikes poison")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_TOXIC_SPIKES].effect == EFFECT_TOXIC_SPIKES);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_SNORLAX) { Ability(ABILITY_IMMUNITY); }
} WHEN {
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
TURN { SWITCH(opponent, 1); }
} SCENE {
NOT STATUS_ICON(opponent, poison: TRUE);
}
}

169
test/ability_pastel_veil.c Normal file
View File

@ -0,0 +1,169 @@
#include "global.h"
#include "test_battle.h"
SINGLE_BATTLE_TEST("Pastel Veil prevents Poison Sting poison")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
} WHEN {
TURN { MOVE(player, MOVE_POISON_STING); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, player);
NOT STATUS_ICON(opponent, poison: TRUE);
}
}
DOUBLE_BATTLE_TEST("Pastel Veil prevents Poison Sting poison on partner")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT);
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
OPPONENT(SPECIES_WYNAUT);
} WHEN {
TURN { MOVE(playerLeft, MOVE_POISON_STING, target: opponentRight); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, playerLeft);
NOT STATUS_ICON(opponentRight, poison: TRUE);
}
}
SINGLE_BATTLE_TEST("Pastel Veil immediately cures Mold Breaker poison")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC);
PLAYER(SPECIES_DRILBUR) { Ability(ABILITY_MOLD_BREAKER); }
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
} WHEN {
TURN { MOVE(player, MOVE_TOXIC); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, player);
STATUS_ICON(opponent, badPoison: TRUE);
ABILITY_POPUP(opponent, ABILITY_PASTEL_VEIL);
MESSAGE("Foe Ponyta's Pastel Veil cured its poison problem!");
STATUS_ICON(opponent, none: TRUE);
}
}
DOUBLE_BATTLE_TEST("Pastel Veil does not cure Mold Breaker poison on partner")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC);
PLAYER(SPECIES_DRILBUR) { Ability(ABILITY_MOLD_BREAKER); }
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
OPPONENT(SPECIES_WYNAUT);
} WHEN {
TURN { MOVE(playerLeft, MOVE_TOXIC, target: opponentRight); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, playerLeft, target: opponentRight);
STATUS_ICON(opponentRight, badPoison: TRUE);
NOT STATUS_ICON(opponentRight, none: TRUE);
}
}
SINGLE_BATTLE_TEST("Pastel Veil prevents Toxic bad poison")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
} WHEN {
TURN { MOVE(player, MOVE_TOXIC); }
} SCENE {
MESSAGE("Wobbuffet used Toxic!");
ABILITY_POPUP(opponent, ABILITY_PASTEL_VEIL);
MESSAGE("Foe Ponyta is protected by a pastel veil!");
NOT STATUS_ICON(opponent, badPoison: TRUE);
}
}
DOUBLE_BATTLE_TEST("Pastel Veil prevents Toxic bad poison on partner")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC);
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
OPPONENT(SPECIES_WYNAUT);
} WHEN {
TURN { MOVE(playerLeft, MOVE_TOXIC, target: opponentRight); }
} SCENE {
MESSAGE("Wobbuffet used Toxic!");
ABILITY_POPUP(opponentLeft, ABILITY_PASTEL_VEIL);
MESSAGE("Foe Wynaut is protected by a pastel veil!");
NOT STATUS_ICON(opponentRight, badPoison: TRUE);
}
}
SINGLE_BATTLE_TEST("Pastel Veil prevents Toxic Spikes poison")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_TOXIC_SPIKES].effect == EFFECT_TOXIC_SPIKES);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
} WHEN {
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
TURN { SWITCH(opponent, 1); }
} SCENE {
MESSAGE("2 sent out Ponyta!");
NOT STATUS_ICON(opponent, poison: TRUE);
}
}
DOUBLE_BATTLE_TEST("Pastel Veil prevents Toxic Spikes poison on partner")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_TOXIC_SPIKES].effect == EFFECT_TOXIC_SPIKES);
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WYNAUT);
} WHEN {
TURN { MOVE(playerLeft, MOVE_TOXIC_SPIKES); }
TURN { SWITCH(opponentRight, 2); }
} SCENE {
MESSAGE("2 sent out Wynaut!");
NOT STATUS_ICON(opponentRight, poison: TRUE);
}
}
DOUBLE_BATTLE_TEST("Pastel Veil cures partner's poison on initial switch in")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_WOBBUFFET) { Status1(STATUS1_POISON); }
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
} WHEN {
TURN {}
} SCENE {
MESSAGE("2 sent out Wobbuffet and Ponyta!");
ABILITY_POPUP(opponentRight, ABILITY_PASTEL_VEIL);
MESSAGE("Foe Wobbuffet was cured of its poisoning!");
STATUS_ICON(opponentLeft, none: TRUE);
}
}
DOUBLE_BATTLE_TEST("Pastel Veil cures partner's poison on switch in")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_WOBBUFFET) { Status1(STATUS1_POISON); }
OPPONENT(SPECIES_WYNAUT);
OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); }
} WHEN {
TURN { SWITCH(opponentRight, 2); }
} SCENE {
MESSAGE("2 sent out Ponyta!");
ABILITY_POPUP(opponentRight, ABILITY_PASTEL_VEIL);
MESSAGE("Foe Wobbuffet was cured of its poisoning!");
STATUS_ICON(opponentLeft, none: TRUE);
}
}

View File

@ -0,0 +1,30 @@
#include "global.h"
#include "test_battle.h"
SINGLE_BATTLE_TEST("Poison Point inflicts poison on contact")
{
u32 move;
PARAMETRIZE { move = MOVE_TACKLE; }
PARAMETRIZE { move = MOVE_SWIFT; }
GIVEN {
ASSUME(gBattleMoves[MOVE_TACKLE].flags & FLAG_MAKES_CONTACT);
ASSUME(!(gBattleMoves[MOVE_SWIFT].flags & FLAG_MAKES_CONTACT));
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_NIDORAN_M) { Ability(ABILITY_POISON_POINT); }
} WHEN {
TURN { MOVE(player, move); }
TURN {}
} SCENE {
if (gBattleMoves[move].flags & FLAG_MAKES_CONTACT) {
ABILITY_POPUP(opponent, ABILITY_POISON_POINT);
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, player);
MESSAGE("Wobbuffet was poisoned by Foe Nidoran♂'s Poison Point!");
STATUS_ICON(player, poison: TRUE);
} else {
NOT ABILITY_POPUP(opponent, ABILITY_POISON_POINT);
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, player);
NOT MESSAGE("Wobbuffet was poisoned by Foe Nidoran♂'s Poison Point!");
NOT STATUS_ICON(player, poison: TRUE);
}
}
}

29
test/ability_static.c Normal file
View File

@ -0,0 +1,29 @@
#include "global.h"
#include "test_battle.h"
SINGLE_BATTLE_TEST("Static inflicts paralysis on contact")
{
u32 move;
PARAMETRIZE { move = MOVE_TACKLE; }
PARAMETRIZE { move = MOVE_SWIFT; }
GIVEN {
ASSUME(gBattleMoves[MOVE_TACKLE].flags & FLAG_MAKES_CONTACT);
ASSUME(!(gBattleMoves[MOVE_SWIFT].flags & FLAG_MAKES_CONTACT));
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_PIKACHU) { Ability(ABILITY_STATIC); }
} WHEN {
TURN { MOVE(player, move); }
} SCENE {
if (gBattleMoves[move].flags & FLAG_MAKES_CONTACT) {
ABILITY_POPUP(opponent, ABILITY_STATIC);
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PRZ, player);
MESSAGE("Foe Pikachu's Static paralyzed Wobbuffet! It may be unable to move!");
STATUS_ICON(player, paralysis: TRUE);
} else {
NOT ABILITY_POPUP(opponent, ABILITY_STATIC);
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PRZ, player);
NOT MESSAGE("Foe Pikachu's Static paralyzed Wobbuffet! It may be unable to move!");
NOT STATUS_ICON(player, paralysis: TRUE);
}
}
}

47
test/ability_sturdy.c Normal file
View File

@ -0,0 +1,47 @@
#include "global.h"
#include "test_battle.h"
SINGLE_BATTLE_TEST("Sturdy prevents OHKO moves")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_FISSURE].effect == EFFECT_OHKO);
PLAYER(SPECIES_GEODUDE) { Ability(ABILITY_STURDY); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(opponent, MOVE_FISSURE); }
} SCENE {
MESSAGE("Foe Wobbuffet used Fissure!");
ABILITY_POPUP(player, ABILITY_STURDY);
MESSAGE("Geodude was protected by Sturdy!");
} THEN {
EXPECT_EQ(player->hp, player->maxHP);
}
}
SINGLE_BATTLE_TEST("Sturdy prevents OHKOs")
{
GIVEN {
PLAYER(SPECIES_GEODUDE) { Ability(ABILITY_STURDY); MaxHP(100); HP(100); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(opponent, MOVE_SEISMIC_TOSS); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_SEISMIC_TOSS, opponent);
HP_BAR(player, hp: 1);
ABILITY_POPUP(player, ABILITY_STURDY);
MESSAGE("Geodude endured the hit using Sturdy!");
}
}
SINGLE_BATTLE_TEST("Sturdy does not prevent non-OHKOs")
{
GIVEN {
PLAYER(SPECIES_GEODUDE) { Ability(ABILITY_STURDY); MaxHP(100); HP(99); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(opponent, MOVE_SEISMIC_TOSS); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_SEISMIC_TOSS, opponent);
HP_BAR(player, hp: 0);
}
}

View File

@ -0,0 +1,54 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
gItems[ITEM_LEFTOVERS].holdEffect == HOLD_EFFECT_LEFTOVERS;
};
SINGLE_BATTLE_TEST("Leftovers recovers 1/16th HP at end of turn")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { MaxHP(100); HP(1); Item(ITEM_LEFTOVERS); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN {}
} SCENE {
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player);
MESSAGE("Wobbuffet's Leftovers restored its HP a little!");
HP_BAR(player, damage: -maxHP / 16);
}
}
SINGLE_BATTLE_TEST("Leftovers does nothing if max HP")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_LEFTOVERS); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN {}
} SCENE {
NONE_OF {
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player);
MESSAGE("Wobbuffet's Leftovers restored its HP a little!");
HP_BAR(player);
}
}
}
SINGLE_BATTLE_TEST("Leftovers does nothing if Heal Block applies")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { MaxHP(100); HP(1); Item(ITEM_LEFTOVERS); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(opponent, MOVE_HEAL_BLOCK); }
} SCENE {
NONE_OF {
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player);
MESSAGE("Wobbuffet's Leftovers restored its HP a little!");
HP_BAR(player);
}
}
}

68
test/mega_evolution.c Normal file
View File

@ -0,0 +1,68 @@
#include "global.h"
#include "test_battle.h"
SINGLE_BATTLE_TEST("Venusaur can Mega Evolve holding Venusaurite")
{
GIVEN {
PLAYER(SPECIES_VENUSAUR) { Item(ITEM_VENUSAURITE); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_CELEBRATE, megaEvolve: TRUE); }
} SCENE {
MESSAGE("Venusaur's Venusaurite is reacting to 1's Mega Ring!");
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_MEGA_EVOLUTION, player);
MESSAGE("Venusaur has Mega Evolved into Mega Venusaur!");
} THEN {
EXPECT_EQ(player->species, SPECIES_VENUSAUR_MEGA);
}
}
SINGLE_BATTLE_TEST("Rayquaza can Mega Evolve knowing Dragon Ascent")
{
GIVEN {
PLAYER(SPECIES_RAYQUAZA) { Moves(MOVE_DRAGON_ASCENT, MOVE_CELEBRATE); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_CELEBRATE, megaEvolve: TRUE); }
} SCENE {
MESSAGE("1's fervent wish has reached Rayquaza!");
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_MEGA_EVOLUTION, player);
MESSAGE("Rayquaza has Mega Evolved into Mega Rayquaza!");
} THEN {
EXPECT_EQ(player->species, SPECIES_RAYQUAZA_MEGA);
}
}
SINGLE_BATTLE_TEST("Mega Evolution affects turn order")
{
GIVEN {
ASSUME(B_MEGA_EVO_TURN_ORDER);
PLAYER(SPECIES_DIANCIE) { Item(ITEM_DIANCITE); Speed(105); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(106); }
} WHEN {
TURN { MOVE(player, MOVE_CELEBRATE, megaEvolve: TRUE); }
} SCENE {
MESSAGE("Diancie used Celebrate!");
MESSAGE("Foe Wobbuffet used Celebrate!");
} THEN {
ASSUME(player->speed == 225);
}
}
SINGLE_BATTLE_TEST("Abilities replaced by Mega Evolution do not affect turn order")
{
GIVEN {
ASSUME(B_MEGA_EVO_TURN_ORDER);
ASSUME(gSpeciesInfo[SPECIES_SABLEYE_MEGA].abilities[0] != ABILITY_STALL
&& gSpeciesInfo[SPECIES_SABLEYE_MEGA].abilities[1] != ABILITY_STALL);
PLAYER(SPECIES_SABLEYE) { Item(ITEM_SABLENITE); Ability(ABILITY_STALL); Speed(105); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(44); }
} WHEN {
TURN { MOVE(player, MOVE_CELEBRATE, megaEvolve: TRUE); }
} SCENE {
MESSAGE("Sableye used Celebrate!");
MESSAGE("Foe Wobbuffet used Celebrate!");
} THEN {
ASSUME(player->speed == 45);
}
}

158
test/move.c Normal file
View File

@ -0,0 +1,158 @@
#include "global.h"
#include "test_battle.h"
SINGLE_BATTLE_TEST("Accuracy controls the proportion of misses")
{
u32 move;
PARAMETRIZE { move = MOVE_DYNAMIC_PUNCH; }
PARAMETRIZE { move = MOVE_THUNDER; }
PARAMETRIZE { move = MOVE_HYDRO_PUMP; }
PARAMETRIZE { move = MOVE_RAZOR_LEAF; }
PARAMETRIZE { move = MOVE_SCRATCH; }
ASSUME(0 < gBattleMoves[move].accuracy && gBattleMoves[move].accuracy <= 100);
PASSES_RANDOMLY(gBattleMoves[move].accuracy, 100);
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, move); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, move, player);
}
}
SINGLE_BATTLE_TEST("Secondary Effect Chance controls the proportion of secondary effects")
{
u32 move;
PARAMETRIZE { move = MOVE_THUNDER_SHOCK; }
PARAMETRIZE { move = MOVE_DISCHARGE; }
PARAMETRIZE { move = MOVE_NUZZLE; }
ASSUME(gBattleMoves[move].accuracy == 100);
ASSUME(gBattleMoves[move].effect == EFFECT_PARALYZE_HIT);
ASSUME(0 < gBattleMoves[move].secondaryEffectChance && gBattleMoves[move].secondaryEffectChance <= 100);
PASSES_RANDOMLY(gBattleMoves[move].secondaryEffectChance, 100);
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, move); }
} SCENE {
STATUS_ICON(opponent, paralysis: TRUE);
}
}
SINGLE_BATTLE_TEST("Turn order is determined by priority")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_QUICK_ATTACK].priority > gBattleMoves[MOVE_TACKLE].priority);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_QUICK_ATTACK); MOVE(opponent, MOVE_TACKLE); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
}
}
SINGLE_BATTLE_TEST("Turn order is determined by speed if priority ties")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Speed(2); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(1); }
} WHEN {
TURN { MOVE(player, MOVE_QUICK_ATTACK); MOVE(opponent, MOVE_QUICK_ATTACK); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, opponent);
}
}
SINGLE_BATTLE_TEST("Turn order is determined randomly if priority and speed tie")
{
PASSES_RANDOMLY(1, 2);
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Speed(1); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(1); }
} WHEN {
TURN { MOVE(player, MOVE_QUICK_ATTACK); MOVE(opponent, MOVE_QUICK_ATTACK); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, opponent);
}
}
SINGLE_BATTLE_TEST("Critical hits occur at a 1/24 rate")
{
ASSUME(B_CRIT_CHANCE >= GEN_7);
ASSUME(gBattleMoves[MOVE_SCRATCH].accuracy == 100);
PASSES_RANDOMLY(100 / 24, 100);
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_SCRATCH); }
} SCENE {
MESSAGE("It's a critical hit!");
}
}
SINGLE_BATTLE_TEST("Critical hits deal 50% more damage", s16 damage)
{
bool32 criticalHit;
PARAMETRIZE { criticalHit = FALSE; }
PARAMETRIZE { criticalHit = TRUE; }
GIVEN {
ASSUME(B_CRIT_MULTIPLIER >= GEN_6);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_SCRATCH, criticalHit: criticalHit); }
} SCENE {
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage);
}
}
SINGLE_BATTLE_TEST("Critical hits do not ignore positive stat stages", s16 damage)
{
u32 move;
PARAMETRIZE { move = MOVE_CELEBRATE; }
PARAMETRIZE { move = MOVE_HOWL; }
PARAMETRIZE { move = MOVE_TAIL_WHIP; }
GIVEN {
ASSUME(gBattleMoves[MOVE_SCRATCH].split == SPLIT_PHYSICAL);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, move); }
TURN { MOVE(player, MOVE_SCRATCH, criticalHit: TRUE); }
} SCENE {
HP_BAR(opponent, captureDamage: &results[i].damage);
} THEN {
if (i > 0)
EXPECT_LT(results[0].damage, results[i].damage);
}
}
SINGLE_BATTLE_TEST("Critical hits ignore negative stat stages", s16 damage)
{
u32 move;
PARAMETRIZE { move = MOVE_CELEBRATE; }
PARAMETRIZE { move = MOVE_HARDEN; }
PARAMETRIZE { move = MOVE_GROWL; }
GIVEN {
ASSUME(gBattleMoves[MOVE_SCRATCH].split == SPLIT_PHYSICAL);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(opponent, move); }
TURN { MOVE(player, MOVE_SCRATCH, criticalHit: TRUE); }
} SCENE {
HP_BAR(opponent, captureDamage: &results[i].damage);
} THEN {
if (i > 0)
EXPECT_EQ(results[0].damage, results[i].damage);
}
}

41
test/move_effect_absorb.c Normal file
View File

@ -0,0 +1,41 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_ABSORB].effect == EFFECT_ABSORB);
}
SINGLE_BATTLE_TEST("Absorb recovers 50% of the damage dealt")
{
s16 damage;
s16 healed;
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { HP(1); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_ABSORB); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_ABSORB, player);
HP_BAR(opponent, captureDamage: &damage);
HP_BAR(player, captureDamage: &healed);
} THEN {
EXPECT_MUL_EQ(damage, Q_4_12(-0.5), healed);
}
}
SINGLE_BATTLE_TEST("Absorb fails if Heal Block applies")
{
ASSUME(B_HEAL_BLOCKING >= GEN_6);
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { HP(1); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(opponent, MOVE_HEAL_BLOCK); MOVE(player, MOVE_ABSORB); }
} SCENE {
MESSAGE("Wobbuffet was prevented from healing!");
NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_ABSORB, player);
NOT HP_BAR(opponent);
NOT HP_BAR(player);
}
}

View File

@ -0,0 +1,24 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_SAND_ATTACK].effect == EFFECT_ACCURACY_DOWN);
}
SINGLE_BATTLE_TEST("Sand Attack lowers Accuracy")
{
ASSUME(gBattleMoves[MOVE_SCRATCH].accuracy == 100);
PASSES_RANDOMLY(gBattleMoves[MOVE_SCRATCH].accuracy * 3 / 4, 100);
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_SAND_ATTACK); MOVE(opponent, MOVE_SCRATCH); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_SAND_ATTACK, player);
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent);
MESSAGE("Foe Wobbuffet's accuracy fell!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent);
}
}

View File

@ -0,0 +1,54 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_AFTER_YOU].effect == EFFECT_AFTER_YOU);
}
DOUBLE_BATTLE_TEST("After You makes the target move after user")
{
if (B_RECALC_TURN_AFTER_ACTIONS >= GEN_8) KNOWN_FAILING; // #2615.
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Speed(4); }
PLAYER(SPECIES_WYNAUT) { Speed(1); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(3); }
OPPONENT(SPECIES_WYNAUT) { Speed(2); }
} WHEN {
TURN {
MOVE(playerLeft, MOVE_AFTER_YOU, target: playerRight);
MOVE(playerRight, MOVE_CELEBRATE);
MOVE(opponentLeft, MOVE_CELEBRATE);
MOVE(opponentRight, MOVE_CELEBRATE);
}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_AFTER_YOU, playerLeft);
MESSAGE("Wynaut took the kind offer!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, playerRight);
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentLeft);
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentRight);
}
}
DOUBLE_BATTLE_TEST("After You does nothing if the target has already moved")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Speed(4); }
PLAYER(SPECIES_WYNAUT) { Speed(1); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(3); }
OPPONENT(SPECIES_WYNAUT) { Speed(2); }
} WHEN {
TURN {
MOVE(playerLeft, MOVE_CELEBRATE);
MOVE(playerRight, MOVE_CELEBRATE);
MOVE(opponentLeft, MOVE_CELEBRATE);
MOVE(opponentRight, MOVE_AFTER_YOU, target: opponentLeft);
}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, playerLeft);
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentLeft);
MESSAGE("Foe Wynaut used After You!");
MESSAGE("But it failed!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, playerRight);
}
}

View File

@ -0,0 +1,32 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_GROWL].effect == EFFECT_ATTACK_DOWN);
}
SINGLE_BATTLE_TEST("Growl lowers Attack", s16 damage)
{
bool32 lowerAttack;
PARAMETRIZE { lowerAttack = FALSE; }
PARAMETRIZE { lowerAttack = TRUE; }
GIVEN {
ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
if (lowerAttack) TURN { MOVE(player, MOVE_GROWL); }
TURN { MOVE(opponent, MOVE_TACKLE); }
} SCENE {
if (lowerAttack) {
ANIMATION(ANIM_TYPE_MOVE, MOVE_GROWL, player);
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent);
MESSAGE("Foe Wobbuffet's attack fell!");
}
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
HP_BAR(player, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[1].damage, Q_4_12(1.5), results[0].damage);
}
}

View File

@ -0,0 +1,32 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_MEDITATE].effect == EFFECT_ATTACK_UP);
}
SINGLE_BATTLE_TEST("Meditate raises Attack", s16 damage)
{
bool32 raiseAttack;
PARAMETRIZE { raiseAttack = FALSE; }
PARAMETRIZE { raiseAttack = TRUE; }
GIVEN {
ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
if (raiseAttack) TURN { MOVE(player, MOVE_MEDITATE); }
TURN { MOVE(player, MOVE_TACKLE); }
} SCENE {
if (raiseAttack) {
ANIMATION(ANIM_TYPE_MOVE, MOVE_MEDITATE, player);
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player);
MESSAGE("Wobbuffet's attack rose!");
}
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage);
}
}

34
test/move_effect_bide.c Normal file
View File

@ -0,0 +1,34 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_BIDE].effect == EFFECT_BIDE);
}
SINGLE_BATTLE_TEST("Bide deals twice the taken damage over two turns")
{
s16 damage1;
s16 damage2;
s16 bideDamage;
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_BIDE); MOVE(opponent, MOVE_TACKLE); }
TURN { SKIP_TURN(player); MOVE(opponent, MOVE_TACKLE); }
TURN { SKIP_TURN(player); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_BIDE, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
HP_BAR(player, captureDamage: &damage1);
MESSAGE("Wobbuffet is storing energy!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
HP_BAR(player, captureDamage: &damage2);
MESSAGE("Wobbuffet unleashed energy!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_BIDE, player);
HP_BAR(opponent, captureDamage: &bideDamage);
} FINALLY {
EXPECT_EQ(bideDamage, damage1 + damage2);
}
}

View File

@ -0,0 +1,38 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_EMBER].effect == EFFECT_BURN_HIT);
}
SINGLE_BATTLE_TEST("Ember inflicts burn")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_EMBER); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_EMBER, player);
HP_BAR(opponent);
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_BRN, opponent);
STATUS_ICON(opponent, burn: TRUE);
}
}
SINGLE_BATTLE_TEST("Ember cannot burn a Fire-type")
{
GIVEN {
ASSUME(gSpeciesInfo[SPECIES_CHARMANDER].types[0] == TYPE_FIRE);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_CHARMANDER);
} WHEN {
TURN { MOVE(player, MOVE_EMBER); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_EMBER, player);
HP_BAR(opponent);
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_BRN, opponent);
NOT STATUS_ICON(opponent, burn: TRUE);
}
}

View File

@ -0,0 +1,32 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_TAIL_WHIP].effect == EFFECT_DEFENSE_DOWN);
}
SINGLE_BATTLE_TEST("Tail Whip lowers Defense", s16 damage)
{
bool32 lowerDefense;
PARAMETRIZE { lowerDefense = FALSE; }
PARAMETRIZE { lowerDefense = TRUE; }
GIVEN {
ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
if (lowerDefense) TURN { MOVE(player, MOVE_TAIL_WHIP); }
TURN { MOVE(player, MOVE_TACKLE); }
} SCENE {
if (lowerDefense) {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TAIL_WHIP, player);
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent);
MESSAGE("Foe Wobbuffet's defense fell!");
}
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage);
}
}

View File

@ -0,0 +1,32 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_HARDEN].effect == EFFECT_DEFENSE_UP);
}
SINGLE_BATTLE_TEST("Harden raises Defense", s16 damage)
{
bool32 raiseDefense;
PARAMETRIZE { raiseDefense = FALSE; }
PARAMETRIZE { raiseDefense = TRUE; }
GIVEN {
ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
if (raiseDefense) TURN { MOVE(player, MOVE_HARDEN); }
TURN { MOVE(opponent, MOVE_TACKLE); }
} SCENE {
if (raiseDefense) {
ANIMATION(ANIM_TYPE_MOVE, MOVE_HARDEN, player);
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player);
MESSAGE("Wobbuffet's defense rose!");
}
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
HP_BAR(player, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[1].damage, Q_4_12(1.5), results[0].damage);
}
}

View File

@ -0,0 +1,54 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_DREAM_EATER].effect == EFFECT_DREAM_EATER);
}
SINGLE_BATTLE_TEST("Dream Eater recovers 50% of the damage dealt")
{
s16 damage;
s16 healed;
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { HP(1); }
OPPONENT(SPECIES_WOBBUFFET) { Status1(STATUS1_SLEEP); }
} WHEN {
TURN { MOVE(player, MOVE_DREAM_EATER); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_DREAM_EATER, player);
HP_BAR(opponent, captureDamage: &damage);
HP_BAR(player, captureDamage: &healed);
} THEN {
EXPECT_MUL_EQ(damage, Q_4_12(-1.0/2.0), healed);
}
}
SINGLE_BATTLE_TEST("Dream Eater fails on awake targets")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_DREAM_EATER); }
} SCENE {
MESSAGE("Wobbuffet used Dream Eater!");
MESSAGE("Foe Wobbuffet wasn't affected!");
}
}
SINGLE_BATTLE_TEST("Dream Eater fails if Heal Block applies")
{
ASSUME(B_HEAL_BLOCKING >= GEN_6);
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { HP(1); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(opponent, MOVE_HEAL_BLOCK); MOVE(player, MOVE_DREAM_EATER); }
} SCENE {
MESSAGE("Wobbuffet was prevented from healing!");
NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_DREAM_EATER, player);
NOT HP_BAR(opponent);
NOT HP_BAR(player);
}
}

54
test/move_effect_encore.c Normal file
View File

@ -0,0 +1,54 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_ENCORE].effect == EFFECT_ENCORE);
}
SINGLE_BATTLE_TEST("Encore forces consecutive move uses for 2 turns")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_ENCORE); }
TURN { FORCED_MOVE(player); }
TURN { FORCED_MOVE(player); }
TURN { MOVE(player, MOVE_SPLASH); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_ENCORE, opponent);
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, player);
}
}
SINGLE_BATTLE_TEST("Encore has no effect if no previous move")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(opponent, MOVE_ENCORE); MOVE(player, MOVE_CELEBRATE); }
} SCENE {
MESSAGE("Foe Wobbuffet used Encore!");
MESSAGE("But it failed!");
}
}
SINGLE_BATTLE_TEST("Encore overrides the chosen move if it occurs first")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_CELEBRATE); }
TURN { MOVE(opponent, MOVE_ENCORE); MOVE(player, MOVE_SPLASH); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_ENCORE, opponent);
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player);
}
}

View File

@ -0,0 +1,24 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_DOUBLE_TEAM].effect == EFFECT_EVASION_UP);
}
SINGLE_BATTLE_TEST("Double Team raises Evasion")
{
ASSUME(gBattleMoves[MOVE_SCRATCH].accuracy == 100);
PASSES_RANDOMLY(gBattleMoves[MOVE_SCRATCH].accuracy * 3 / 4, 100);
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_DOUBLE_TEAM); MOVE(opponent, MOVE_SCRATCH); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_DOUBLE_TEAM, player);
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player);
MESSAGE("Wobbuffet's evasiveness rose!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent);
}
}

View File

@ -0,0 +1,53 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_EXPLOSION].effect == EFFECT_EXPLOSION);
}
SINGLE_BATTLE_TEST("Explosion causes the user to faint")
{
u16 remainingHP;
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_EXPLOSION); }
} SCENE {
HP_BAR(player, hp: 0);
ANIMATION(ANIM_TYPE_MOVE, MOVE_EXPLOSION, player);
}
}
SINGLE_BATTLE_TEST("Explosion causes the user to faint even if it misses")
{
u16 remainingHP;
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_EXPLOSION, hit: FALSE); }
} SCENE {
HP_BAR(player, hp: 0);
ANIMATION(ANIM_TYPE_MOVE, MOVE_EXPLOSION, player);
}
}
SINGLE_BATTLE_TEST("Explosion causes the user to faint even if it has no effect")
{
u16 remainingHP;
GIVEN {
ASSUME(gBattleMoves[MOVE_EXPLOSION].type == TYPE_NORMAL);
ASSUME(gSpeciesInfo[SPECIES_GASTLY].types[0] == TYPE_GHOST);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_GASTLY);
} WHEN {
TURN { MOVE(player, MOVE_EXPLOSION); }
} SCENE {
HP_BAR(player, hp: 0);
ANIMATION(ANIM_TYPE_MOVE, MOVE_EXPLOSION, player);
MESSAGE("It doesn't affect Foe Gastly…");
NOT HP_BAR(opponent);
}
}

View File

@ -0,0 +1,38 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_POWDER_SNOW].effect == EFFECT_FREEZE_HIT);
}
SINGLE_BATTLE_TEST("Powder Snow inflicts freeze")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_POWDER_SNOW); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_POWDER_SNOW, player);
HP_BAR(opponent);
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_FRZ, opponent);
STATUS_ICON(opponent, freeze: TRUE);
}
}
SINGLE_BATTLE_TEST("Powder Snow cannot freeze an Ice-type")
{
GIVEN {
ASSUME(gSpeciesInfo[SPECIES_SNORUNT].types[0] == TYPE_ICE);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_SNORUNT);
} WHEN {
TURN { MOVE(player, MOVE_POWDER_SNOW); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_POWDER_SNOW, player);
HP_BAR(opponent);
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_FRZ, opponent);
NOT STATUS_ICON(opponent, freeze: TRUE);
}
}

32
test/move_effect_haze.c Normal file
View File

@ -0,0 +1,32 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_HAZE].effect == EFFECT_HAZE);
}
SINGLE_BATTLE_TEST("Haze resets stat changes", s16 damage)
{
bool32 haze;
PARAMETRIZE { haze = FALSE; }
PARAMETRIZE { haze = TRUE; }
GIVEN {
ASSUME(gBattleMoves[MOVE_MEDITATE].effect == EFFECT_ATTACK_UP);
ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
if (haze) TURN { MOVE(player, MOVE_MEDITATE); MOVE(opponent, MOVE_HAZE); }
TURN { MOVE(player, MOVE_TACKLE); }
} SCENE {
if (haze) {
ANIMATION(ANIM_TYPE_MOVE, MOVE_HAZE, opponent);
MESSAGE("All stat changes were eliminated!");
}
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_EQ(results[0].damage, results[1].damage);
}
}

33
test/move_effect_hex.c Normal file
View File

@ -0,0 +1,33 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_HEX].effect == EFFECT_HEX);
}
SINGLE_BATTLE_TEST("Hex deals double damage to foes with a status", s16 damage)
{
u32 status1;
PARAMETRIZE { status1 = STATUS1_NONE; }
PARAMETRIZE { status1 = STATUS1_SLEEP; }
PARAMETRIZE { status1 = STATUS1_POISON; }
PARAMETRIZE { status1 = STATUS1_BURN; }
PARAMETRIZE { status1 = STATUS1_FREEZE; }
PARAMETRIZE { status1 = STATUS1_PARALYSIS; }
PARAMETRIZE { status1 = STATUS1_TOXIC_POISON; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) { Status1(status1); }
} WHEN {
TURN { MOVE(player, MOVE_HEX); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_HEX, player);
HP_BAR(opponent, captureDamage: &results[i].damage);
} THEN {
if (i > 0)
EXPECT_MUL_EQ(results[0].damage, Q_4_12(2.0), results[i].damage);
if (i > 1)
EXPECT_EQ(results[i-1].damage, results[i].damage);
}
}

View File

@ -0,0 +1,96 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_U_TURN].effect == EFFECT_HIT_ESCAPE);
}
SINGLE_BATTLE_TEST("U-turn switches the user out")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_U_TURN); SEND_OUT(player, 1); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player);
HP_BAR(opponent);
MESSAGE("Go! Wynaut!");
}
}
SINGLE_BATTLE_TEST("U-turn does not switch the user out if the battle ends")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_WOBBUFFET) { HP(1); }
} WHEN {
TURN { MOVE(player, MOVE_U_TURN); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player);
HP_BAR(opponent);
}
}
SINGLE_BATTLE_TEST("U-turn does not switch the user out if no replacements")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_U_TURN); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player);
HP_BAR(opponent);
}
}
SINGLE_BATTLE_TEST("U-turn does not switch the user out if replacements fainted")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT) { HP(0); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_U_TURN); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player);
HP_BAR(opponent);
}
}
SINGLE_BATTLE_TEST("U-turn does not switch the user out if Wimp Out activates")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_WIMPOD) { MaxHP(100); HP(51); Ability(ABILITY_WIMP_OUT); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_U_TURN); SEND_OUT(opponent, 1); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player);
HP_BAR(opponent);
ABILITY_POPUP(opponent, ABILITY_WIMP_OUT);
MESSAGE("2 sent out Wobbuffet!");
}
}
SINGLE_BATTLE_TEST("U-turn switches the user out if Wimp Out fails to activate")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_WIMPOD) { MaxHP(100); HP(51); Ability(ABILITY_WIMP_OUT); }
} WHEN {
TURN { MOVE(player, MOVE_U_TURN); SEND_OUT(player, 1); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player);
HP_BAR(opponent);
NOT ABILITY_POPUP(opponent);
MESSAGE("Your foe's weak! Get 'em, Wynaut!");
}
}

View File

@ -0,0 +1,39 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_THUNDER_SHOCK].effect == EFFECT_PARALYZE_HIT);
}
SINGLE_BATTLE_TEST("Thunder Shock inflicts paralysis")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_THUNDER_SHOCK); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_THUNDER_SHOCK, player);
HP_BAR(opponent);
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PRZ, opponent);
STATUS_ICON(opponent, paralysis: TRUE);
}
}
SINGLE_BATTLE_TEST("Thunder Shock cannot paralyze an Electric-type")
{
GIVEN {
ASSUME(B_PARALYZE_ELECTRIC >= GEN_6);
ASSUME(gSpeciesInfo[SPECIES_PIKACHU].types[0] == TYPE_ELECTRIC);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_PIKACHU);
} WHEN {
TURN { MOVE(player, MOVE_THUNDER_SHOCK); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_THUNDER_SHOCK, player);
HP_BAR(opponent);
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PRZ, opponent);
NOT STATUS_ICON(opponent, paralysis: TRUE);
}
}

View File

@ -0,0 +1,39 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT);
}
SINGLE_BATTLE_TEST("Poison Sting inflicts poison")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_POISON_STING); }
TURN {}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, player);
HP_BAR(opponent);
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
STATUS_ICON(opponent, poison: TRUE);
}
}
SINGLE_BATTLE_TEST("Poison Sting cannot poison Poison-type")
{
GIVEN {
ASSUME(gSpeciesInfo[SPECIES_NIDORAN_M].types[0] == TYPE_POISON);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_NIDORAN_M);
} WHEN {
TURN { MOVE(player, MOVE_POISON_STING); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, player);
HP_BAR(opponent);
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
NOT STATUS_ICON(opponent, poison: TRUE);
}
}

View File

@ -0,0 +1,91 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_THRASH].effect == EFFECT_RAMPAGE);
}
SINGLE_BATTLE_TEST("Thrash lasts for 2 or 3 turns")
{
PASSES_RANDOMLY(1, 2);
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_THRASH); }
TURN { SKIP_TURN(player); }
TURN { SKIP_TURN(player); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player);
}
}
SINGLE_BATTLE_TEST("Thrash confuses the user after it finishes")
{
GIVEN {
RNGSeed(0x00000000);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_THRASH); }
TURN { SKIP_TURN(player); }
TURN { SKIP_TURN(player); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player);
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_CONFUSION, player);
}
}
SINGLE_BATTLE_TEST("Thrash does not confuse the user if it is canceled on turn 1 of 3")
{
GIVEN {
ASSUME(B_RAMPAGE_CANCELLING >= GEN_5);
RNGSeed(0x00000000);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_THRASH); }
TURN { MOVE(opponent, MOVE_PROTECT); SKIP_TURN(player); }
TURN { SKIP_TURN(player); }
} SCENE {
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_CONFUSION, player);
}
}
SINGLE_BATTLE_TEST("Thrash does not confuse the user if it is canceled on turn 2 of 3")
{
GIVEN {
ASSUME(B_RAMPAGE_CANCELLING >= GEN_5);
RNGSeed(0x00000000);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_THRASH); }
TURN { MOVE(opponent, MOVE_PROTECT); SKIP_TURN(player); }
TURN { SKIP_TURN(player); }
} SCENE {
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_CONFUSION, player);
}
}
SINGLE_BATTLE_TEST("Thrash confuses the user if it is canceled on turn 3 of 3")
{
KNOWN_FAILING;
GIVEN {
ASSUME(B_RAMPAGE_CANCELLING >= GEN_5);
RNGSeed(0x00000000);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_THRASH); }
TURN { SKIP_TURN(player); }
TURN { MOVE(opponent, MOVE_PROTECT); SKIP_TURN(player); }
} SCENE {
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_CONFUSION, player);
}
}

View File

@ -0,0 +1,57 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_JUMP_KICK].effect == EFFECT_RECOIL_IF_MISS);
}
SINGLE_BATTLE_TEST("Jump Kick has 50% recoil on miss")
{
s16 recoil;
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_JUMP_KICK, hit: FALSE); }
} SCENE {
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
MESSAGE("Wobbuffet used Jump Kick!");
MESSAGE("Wobbuffet's attack missed!");
MESSAGE("Wobbuffet kept going and crashed!");
HP_BAR(player, damage: maxHP / 2);
}
}
SINGLE_BATTLE_TEST("Jump Kick has 50% recoil on protect")
{
s16 recoil;
GIVEN {
ASSUME(gBattleMoves[MOVE_JUMP_KICK].flags & FLAG_PROTECT_AFFECTED);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(opponent, MOVE_PROTECT); MOVE(player, MOVE_JUMP_KICK, hit: FALSE); }
} SCENE {
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
ANIMATION(ANIM_TYPE_MOVE, MOVE_PROTECT, opponent);
NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_JUMP_KICK, player);
HP_BAR(player, damage: maxHP / 2);
}
}
SINGLE_BATTLE_TEST("Jump Kick has no recoil if no target")
{
KNOWN_FAILING; // #2596.
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WYNAUT);
} WHEN {
TURN { MOVE(opponent, MOVE_HEALING_WISH); MOVE(player, MOVE_JUMP_KICK, hit: FALSE); SEND_OUT(opponent, 1); }
} SCENE {
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
ANIMATION(ANIM_TYPE_MOVE, MOVE_HEALING_WISH, opponent);
NOT HP_BAR(player, damage: maxHP / 2);
}
}

View File

@ -0,0 +1,77 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_REFLECT].effect == EFFECT_REFLECT);
}
SINGLE_BATTLE_TEST("Reflect reduces physical damage", s16 damage)
{
u32 move;
PARAMETRIZE { move = MOVE_CELEBRATE; }
PARAMETRIZE { move = MOVE_REFLECT; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, move); MOVE(opponent, MOVE_TACKLE); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, move, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
HP_BAR(player, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_LT(results[1].damage, results[0].damage);
}
}
SINGLE_BATTLE_TEST("Reflect applies for 5 turns")
{
u16 damage[6];
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_REFLECT); MOVE(opponent, MOVE_TACKLE); }
TURN { MOVE(opponent, MOVE_TACKLE); }
TURN { MOVE(opponent, MOVE_TACKLE); }
TURN { MOVE(opponent, MOVE_TACKLE); }
TURN { MOVE(opponent, MOVE_TACKLE); }
TURN { MOVE(opponent, MOVE_TACKLE); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_REFLECT, player);
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
HP_BAR(player, captureDamage: &damage[0]);
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
HP_BAR(player, captureDamage: &damage[1]);
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
HP_BAR(player, captureDamage: &damage[2]);
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
HP_BAR(player, captureDamage: &damage[3]);
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
HP_BAR(player, captureDamage: &damage[4]);
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
HP_BAR(player, captureDamage: &damage[5]);
} THEN {
EXPECT_MUL_EQ(damage[0], Q_4_12(1.0), damage[1]);
EXPECT_MUL_EQ(damage[0], Q_4_12(1.0), damage[2]);
EXPECT_MUL_EQ(damage[0], Q_4_12(1.0), damage[3]);
EXPECT_MUL_EQ(damage[0], Q_4_12(1.0), damage[4]);
EXPECT_LT(damage[0], damage[5]);
}
}
SINGLE_BATTLE_TEST("Reflect fails if already active")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_REFLECT); }
TURN { MOVE(player, MOVE_REFLECT); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_REFLECT, player);
MESSAGE("Wobbuffet used Reflect!");
MESSAGE("But it failed!");
}
}

21
test/move_effect_sleep.c Normal file
View File

@ -0,0 +1,21 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_HYPNOSIS].effect == EFFECT_SLEEP);
}
SINGLE_BATTLE_TEST("Hypnosis inflicts sleep")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_HYPNOSIS); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_HYPNOSIS, player);
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_SLP, opponent);
STATUS_ICON(opponent, sleep: TRUE);
}
}

View File

@ -0,0 +1,32 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_CONFIDE].effect == EFFECT_SPECIAL_ATTACK_DOWN);
}
SINGLE_BATTLE_TEST("Confide lowers Special Attack", s16 damage)
{
bool32 lowerSpecialAttack;
PARAMETRIZE { lowerSpecialAttack = FALSE; }
PARAMETRIZE { lowerSpecialAttack = TRUE; }
GIVEN {
ASSUME(gBattleMoves[MOVE_GUST].split == SPLIT_SPECIAL);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
if (lowerSpecialAttack) TURN { MOVE(player, MOVE_CONFIDE); }
TURN { MOVE(opponent, MOVE_GUST); }
} SCENE {
if (lowerSpecialAttack) {
ANIMATION(ANIM_TYPE_MOVE, MOVE_CONFIDE, player);
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent);
MESSAGE("Foe Wobbuffet's sp. attack fell!");
}
ANIMATION(ANIM_TYPE_MOVE, MOVE_GUST, opponent);
HP_BAR(player, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[1].damage, Q_4_12(1.5), results[0].damage);
}
}

View File

@ -0,0 +1,32 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_TAIL_GLOW].effect == EFFECT_SPECIAL_ATTACK_UP_3);
}
SINGLE_BATTLE_TEST("Tail Glow drastically raises Special Attack", s16 damage)
{
bool32 raiseSpecialAttack;
PARAMETRIZE { raiseSpecialAttack = FALSE; }
PARAMETRIZE { raiseSpecialAttack = TRUE; }
GIVEN {
ASSUME(gBattleMoves[MOVE_GUST].split == SPLIT_SPECIAL);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
if (raiseSpecialAttack) TURN { MOVE(player, MOVE_TAIL_GLOW); }
TURN { MOVE(player, MOVE_GUST); }
} SCENE {
if (raiseSpecialAttack) {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TAIL_GLOW, player);
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player);
MESSAGE("Wobbuffet's sp. attack drastically rose!");
}
ANIMATION(ANIM_TYPE_MOVE, MOVE_GUST, player);
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[0].damage, Q_4_12(2.5), results[1].damage);
}
}

135
test/move_effect_spikes.c Normal file
View File

@ -0,0 +1,135 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_SPIKES].effect == EFFECT_SPIKES);
}
SINGLE_BATTLE_TEST("Spikes damage on switch in")
{
u32 layers;
u32 divisor;
PARAMETRIZE { layers = 1; divisor = 8; }
PARAMETRIZE { layers = 2; divisor = 6; }
PARAMETRIZE { layers = 3; divisor = 4; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WYNAUT);
} WHEN {
u32 count;
for (count = 0; count < layers; ++count) {
TURN { MOVE(player, MOVE_SPIKES); }
}
TURN { SWITCH(opponent, 1); }
} SCENE {
u32 count;
s32 maxHP = GetMonData(&OPPONENT_PARTY[1], MON_DATA_MAX_HP);
for (count = 0; count < layers; ++count) {
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, player);
MESSAGE("Spikes were scattered all around the opponent's side!");
}
MESSAGE("2 sent out Wynaut!");
HP_BAR(opponent, damage: maxHP / divisor);
MESSAGE("Foe Wynaut is hurt by spikes!");
}
}
SINGLE_BATTLE_TEST("Spikes fails after 3 layers")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WYNAUT);
} WHEN {
TURN { MOVE(player, MOVE_SPIKES); }
TURN { MOVE(player, MOVE_SPIKES); }
TURN { MOVE(player, MOVE_SPIKES); }
TURN { MOVE(player, MOVE_SPIKES); }
TURN { SWITCH(opponent, 1); }
} SCENE {
s32 maxHP = GetMonData(&OPPONENT_PARTY[1], MON_DATA_MAX_HP);
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, player);
MESSAGE("Spikes were scattered all around the opponent's side!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, player);
MESSAGE("Spikes were scattered all around the opponent's side!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, player);
MESSAGE("Spikes were scattered all around the opponent's side!");
MESSAGE("Wobbuffet used Spikes!");
MESSAGE("But it failed!");
MESSAGE("2 sent out Wynaut!");
HP_BAR(opponent, damage: maxHP / 4);
MESSAGE("Foe Wynaut is hurt by spikes!");
}
}
SINGLE_BATTLE_TEST("Spikes damage on subsequent switch ins")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WYNAUT);
} WHEN {
TURN { MOVE(player, MOVE_SPIKES); }
TURN { SWITCH(opponent, 1); }
TURN { SWITCH(opponent, 0); }
} SCENE {
s32 maxHP0 = GetMonData(&OPPONENT_PARTY[0], MON_DATA_MAX_HP);
s32 maxHP1 = GetMonData(&OPPONENT_PARTY[1], MON_DATA_MAX_HP);
MESSAGE("2 sent out Wynaut!");
HP_BAR(opponent, damage: maxHP1 / 8);
MESSAGE("Foe Wynaut is hurt by spikes!");
MESSAGE("2 sent out Wobbuffet!");
HP_BAR(opponent, damage: maxHP0 / 8);
MESSAGE("Foe Wobbuffet is hurt by spikes!");
}
}
SINGLE_BATTLE_TEST("Spikes do not damage airborne Pokemon")
{
u32 species = SPECIES_WOBBUFFET;
u32 item = ITEM_NONE;
u32 move1 = MOVE_CELEBRATE;
u32 move2 = MOVE_CELEBRATE;
bool32 airborne;
ASSUME(gSpeciesInfo[SPECIES_PIDGEY].types[1] == TYPE_FLYING);
PARAMETRIZE { species = SPECIES_PIDGEY; airborne = TRUE; }
PARAMETRIZE { species = SPECIES_PIDGEY; item = ITEM_IRON_BALL; airborne = FALSE; }
PARAMETRIZE { species = SPECIES_PIDGEY; move1 = MOVE_GRAVITY; airborne = FALSE; }
PARAMETRIZE { species = SPECIES_PIDGEY; move1 = MOVE_INGRAIN; airborne = FALSE; }
ASSUME(gSpeciesInfo[SPECIES_UNOWN].abilities[0] == ABILITY_LEVITATE);
PARAMETRIZE { species = SPECIES_UNOWN; airborne = TRUE; }
PARAMETRIZE { species = SPECIES_UNOWN; item = ITEM_IRON_BALL; airborne = FALSE; }
PARAMETRIZE { species = SPECIES_UNOWN; move1 = MOVE_GRAVITY; airborne = FALSE; }
PARAMETRIZE { species = SPECIES_UNOWN; move1 = MOVE_INGRAIN; airborne = FALSE; }
PARAMETRIZE { move1 = MOVE_MAGNET_RISE; airborne = TRUE; }
PARAMETRIZE { move1 = MOVE_MAGNET_RISE; item = ITEM_IRON_BALL; airborne = FALSE; }
PARAMETRIZE { move1 = MOVE_MAGNET_RISE; move2 = MOVE_GRAVITY; airborne = FALSE; }
// Magnet Rise fails under Gravity.
// Magnet Rise fails under Ingrain and vice-versa.
PARAMETRIZE { item = ITEM_AIR_BALLOON; airborne = TRUE; }
PARAMETRIZE { item = ITEM_AIR_BALLOON; move1 = MOVE_GRAVITY; airborne = FALSE; }
PARAMETRIZE { item = ITEM_AIR_BALLOON; move1 = MOVE_INGRAIN; airborne = FALSE; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(species) { Item(item); }
} WHEN {
TURN { MOVE(player, MOVE_SPIKES); MOVE(opponent, move1); }
TURN { MOVE(opponent, move2); }
TURN { MOVE(opponent, MOVE_BATON_PASS); SEND_OUT(opponent, 1); }
} SCENE {
s32 maxHP = GetMonData(&OPPONENT_PARTY[1], MON_DATA_MAX_HP);
if (airborne) {
NOT HP_BAR(opponent, damage: maxHP / 8);
} else {
HP_BAR(opponent, damage: maxHP / 8);
}
}
}

View File

@ -0,0 +1,55 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_TAILWIND].effect == EFFECT_TAILWIND);
}
SINGLE_BATTLE_TEST("Tailwind applies for 4 turns")
{
GIVEN {
ASSUME(B_TAILWIND_TURNS >= GEN_5);
PLAYER(SPECIES_WOBBUFFET) { Speed(10); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(15); }
} WHEN {
TURN { MOVE(opponent, MOVE_CELEBRATE); MOVE(player, MOVE_TAILWIND); }
TURN {}
TURN {}
TURN {}
TURN {}
} SCENE {
MESSAGE("Foe Wobbuffet used Celebrate!");
MESSAGE("Wobbuffet used Tailwind!");
MESSAGE("Wobbuffet used Celebrate!");
MESSAGE("Foe Wobbuffet used Celebrate!");
MESSAGE("Wobbuffet used Celebrate!");
MESSAGE("Foe Wobbuffet used Celebrate!");
MESSAGE("Wobbuffet used Celebrate!");
MESSAGE("Foe Wobbuffet used Celebrate!");
MESSAGE("Foe Wobbuffet used Celebrate!");
MESSAGE("Wobbuffet used Celebrate!");
}
}
DOUBLE_BATTLE_TEST("Tailwind affects partner on first turn")
{
GIVEN {
ASSUME(B_RECALC_TURN_AFTER_ACTIONS);
PLAYER(SPECIES_WOBBUFFET) { Speed(20); }
PLAYER(SPECIES_WYNAUT) { Speed(10); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(15); }
OPPONENT(SPECIES_WYNAUT) { Speed(14); }
} WHEN {
TURN { MOVE(playerLeft, MOVE_TAILWIND); }
} SCENE {
MESSAGE("Wobbuffet used Tailwind!");
MESSAGE("Wynaut used Celebrate!");
MESSAGE("Foe Wobbuffet used Celebrate!");
MESSAGE("Foe Wynaut used Celebrate!");
}
}

View File

@ -0,0 +1,53 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_TORMENT].effect == EFFECT_TORMENT);
}
SINGLE_BATTLE_TEST("Torment prevents consecutive move uses")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_SPLASH, MOVE_CELEBRATE); }
} WHEN {
TURN { MOVE(player, MOVE_TORMENT); MOVE(opponent, MOVE_SPLASH); }
TURN { MOVE(opponent, MOVE_SPLASH, allowed: FALSE); MOVE(opponent, MOVE_CELEBRATE); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TORMENT, player);
MESSAGE("Foe Wobbuffet was subjected to torment!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, opponent);
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent);
}
}
SINGLE_BATTLE_TEST("Torment forces Struggle if the only move is prevented")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_SPLASH); }
} WHEN {
TURN { MOVE(player, MOVE_TORMENT); MOVE(opponent, MOVE_SPLASH); }
TURN { MOVE(opponent, MOVE_SPLASH, allowed: FALSE); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, opponent);
ANIMATION(ANIM_TYPE_MOVE, MOVE_STRUGGLE, opponent);
}
}
SINGLE_BATTLE_TEST("Torment allows non-consecutive move uses")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_TORMENT); MOVE(opponent, MOVE_SPLASH); }
TURN { MOVE(opponent, MOVE_CELEBRATE); }
TURN { MOVE(opponent, MOVE_SPLASH); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, opponent);
ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent);
ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, opponent);
}
}

48
test/move_effect_toxic.c Normal file
View File

@ -0,0 +1,48 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC);
}
SINGLE_BATTLE_TEST("Toxic inflicts bad poison")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_TOXIC); }
TURN {}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, player);
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
STATUS_ICON(opponent, badPoison: TRUE);
}
}
SINGLE_BATTLE_TEST("Toxic cannot miss if used by a Poison-type")
{
u32 species;
bool32 hit;
PARAMETRIZE { species = SPECIES_WOBBUFFET; hit = FALSE; }
PARAMETRIZE { species = SPECIES_NIDORAN_M; hit = TRUE; }
GIVEN {
ASSUME(B_TOXIC_NEVER_MISS >= GEN_6);
ASSUME(gSpeciesInfo[SPECIES_NIDORAN_M].types[0] == TYPE_POISON);
PLAYER(species);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_TOXIC, hit: FALSE); }
} SCENE {
if (hit) {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, player);
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
STATUS_ICON(opponent, badPoison: TRUE);
} else {
NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, player);
NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
NOT STATUS_ICON(opponent, badPoison: TRUE);
}
}
}

View File

@ -0,0 +1,210 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_TOXIC_SPIKES].effect == EFFECT_TOXIC_SPIKES);
}
SINGLE_BATTLE_TEST("Toxic Spikes inflicts poison on switch in")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WYNAUT);
} WHEN {
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
TURN { SWITCH(opponent, 1); }
TURN {}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player);
MESSAGE("Poison Spikes were scattered all around the opposing team's feet!");
MESSAGE("2 sent out Wynaut!");
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
STATUS_ICON(opponent, poison: TRUE);
}
}
SINGLE_BATTLE_TEST("Toxic Spikes inflicts bad poison on switch in")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WYNAUT);
} WHEN {
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
TURN { SWITCH(opponent, 1); }
TURN {}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player);
MESSAGE("Poison Spikes were scattered all around the opposing team's feet!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player);
MESSAGE("Poison Spikes were scattered all around the opposing team's feet!");
MESSAGE("2 sent out Wynaut!");
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
STATUS_ICON(opponent, badPoison: TRUE);
}
}
SINGLE_BATTLE_TEST("Toxic Spikes fails after 2 layers")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WYNAUT);
} WHEN {
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
TURN { SWITCH(opponent, 1); }
TURN {}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player);
MESSAGE("Poison Spikes were scattered all around the opposing team's feet!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player);
MESSAGE("Poison Spikes were scattered all around the opposing team's feet!");
MESSAGE("Wobbuffet used Toxic Spikes!");
MESSAGE("But it failed!");
MESSAGE("2 sent out Wynaut!");
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent);
STATUS_ICON(opponent, badPoison: TRUE);
}
}
SINGLE_BATTLE_TEST("Toxic Spikes inflicts poison on subsequent switch ins")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WYNAUT);
} WHEN {
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
TURN { SWITCH(opponent, 1); }
TURN { SWITCH(opponent, 0); }
TURN {}
} SCENE {
MESSAGE("2 sent out Wynaut!");
STATUS_ICON(opponent, poison: TRUE);
}
}
SINGLE_BATTLE_TEST("Toxic Spikes do not poison airborne Pokemon")
{
u32 species = SPECIES_WOBBUFFET;
u32 item = ITEM_NONE;
u32 move1 = MOVE_CELEBRATE;
u32 move2 = MOVE_CELEBRATE;
bool32 airborne;
ASSUME(gSpeciesInfo[SPECIES_PIDGEY].types[1] == TYPE_FLYING);
PARAMETRIZE { species = SPECIES_PIDGEY; airborne = TRUE; }
PARAMETRIZE { species = SPECIES_PIDGEY; item = ITEM_IRON_BALL; airborne = FALSE; }
PARAMETRIZE { species = SPECIES_PIDGEY; move1 = MOVE_GRAVITY; airborne = FALSE; }
PARAMETRIZE { species = SPECIES_PIDGEY; move1 = MOVE_INGRAIN; airborne = FALSE; }
ASSUME(gSpeciesInfo[SPECIES_UNOWN].abilities[0] == ABILITY_LEVITATE);
PARAMETRIZE { species = SPECIES_UNOWN; airborne = TRUE; }
PARAMETRIZE { species = SPECIES_UNOWN; item = ITEM_IRON_BALL; airborne = FALSE; }
PARAMETRIZE { species = SPECIES_UNOWN; move1 = MOVE_GRAVITY; airborne = FALSE; }
PARAMETRIZE { species = SPECIES_UNOWN; move1 = MOVE_INGRAIN; airborne = FALSE; }
PARAMETRIZE { move1 = MOVE_MAGNET_RISE; airborne = TRUE; }
PARAMETRIZE { move1 = MOVE_MAGNET_RISE; item = ITEM_IRON_BALL; airborne = FALSE; }
PARAMETRIZE { move1 = MOVE_MAGNET_RISE; move2 = MOVE_GRAVITY; airborne = FALSE; }
// Magnet Rise fails under Gravity.
// Magnet Rise fails under Ingrain and vice-versa.
PARAMETRIZE { item = ITEM_AIR_BALLOON; airborne = TRUE; }
PARAMETRIZE { item = ITEM_AIR_BALLOON; move1 = MOVE_GRAVITY; airborne = FALSE; }
PARAMETRIZE { item = ITEM_AIR_BALLOON; move1 = MOVE_INGRAIN; airborne = FALSE; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(species) { Item(item); }
} WHEN {
TURN { MOVE(player, MOVE_TOXIC_SPIKES); MOVE(opponent, move1); }
TURN { MOVE(opponent, move2); }
TURN { MOVE(opponent, MOVE_BATON_PASS); SEND_OUT(opponent, 1); }
} SCENE {
if (airborne) {
NOT STATUS_ICON(opponent, poison: TRUE);
} else {
STATUS_ICON(opponent, poison: TRUE);
}
}
}
SINGLE_BATTLE_TEST("Toxic Spikes do not affect Steel-types")
{
GIVEN {
ASSUME(gSpeciesInfo[SPECIES_STEELIX].types[0] == TYPE_STEEL);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_STEELIX);
} WHEN {
TURN { MOVE(player, MOVE_TOXIC_SPIKES); }
TURN { SWITCH(opponent, 1); }
} SCENE {
NOT STATUS_ICON(opponent, poison: TRUE);
}
}
SINGLE_BATTLE_TEST("Toxic Spikes are removed by grounded Poison-types")
{
u32 species;
u32 item = ITEM_NONE;
u32 move = MOVE_CELEBRATE;
bool32 grounded;
PARAMETRIZE { species = SPECIES_EKANS; grounded = TRUE; }
PARAMETRIZE { species = SPECIES_ZUBAT; grounded = FALSE; }
PARAMETRIZE { species = SPECIES_ZUBAT; item = ITEM_IRON_BALL; grounded = TRUE; }
PARAMETRIZE { species = SPECIES_ZUBAT; move = MOVE_GRAVITY; grounded = TRUE; }
PARAMETRIZE { species = SPECIES_ZUBAT; move = MOVE_INGRAIN; grounded = TRUE; }
GIVEN {
ASSUME(gSpeciesInfo[SPECIES_EKANS].types[0] == TYPE_POISON);
ASSUME(gSpeciesInfo[SPECIES_ZUBAT].types[0] == TYPE_POISON);
ASSUME(gSpeciesInfo[SPECIES_ZUBAT].types[1] == TYPE_FLYING);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(species) { Item(item); }
} WHEN {
TURN { MOVE(player, MOVE_TOXIC_SPIKES); MOVE(opponent, move); }
TURN { MOVE(opponent, MOVE_BATON_PASS); SEND_OUT(opponent, 1); }
TURN { SWITCH(opponent, 0); }
} SCENE {
if (grounded) {
NOT STATUS_ICON(opponent, poison: TRUE);
MESSAGE("The poison spikes disappeared from around the opposing team's feet!");
NOT STATUS_ICON(opponent, poison: TRUE);
} else {
NOT STATUS_ICON(opponent, poison: TRUE);
ANIMATION(ANIM_TYPE_MOVE, MOVE_BATON_PASS, opponent);
STATUS_ICON(opponent, poison: TRUE);
}
}
}
// This would test for what I believe to be a bug in the mainline games.
// A Pokémon that gets passed magnet rise should still remove the Toxic
// Spikes even though it is airborne.
// The test currently fails, because we don't incorporate this bug.
SINGLE_BATTLE_TEST("Toxic Spikes are removed by Poison-types affected by Magnet Rise")
{
KNOWN_FAILING;
GIVEN {
ASSUME(gSpeciesInfo[SPECIES_EKANS].types[0] == TYPE_POISON);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_EKANS);
} WHEN {
TURN { MOVE(opponent, MOVE_MAGNET_RISE); }
TURN { MOVE(player, MOVE_TOXIC_SPIKES); MOVE(opponent, MOVE_BATON_PASS); SEND_OUT(opponent, 1); }
TURN { SWITCH(opponent, 0); }
} SCENE {
NOT STATUS_ICON(opponent, poison: TRUE);
MESSAGE("The poison spikes disappeared from around the opposing team's feet!");
NOT STATUS_ICON(opponent, poison: TRUE);
}
}

194
test/status1.c Normal file
View File

@ -0,0 +1,194 @@
#include "global.h"
#include "test_battle.h"
SINGLE_BATTLE_TEST("Sleep prevents the battler from using a move")
{
u32 turns;
PARAMETRIZE { turns = 1; }
PARAMETRIZE { turns = 2; }
PARAMETRIZE { turns = 3; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_SLEEP_TURN(turns)); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
for (i = 0; i < turns; i++)
TURN { MOVE(player, MOVE_CELEBRATE); }
} SCENE {
for (i = 0; i < turns - 1; i++)
MESSAGE("Wobbuffet is fast asleep.");
MESSAGE("Wobbuffet woke up!");
STATUS_ICON(player, none: TRUE);
MESSAGE("Wobbuffet used Celebrate!");
}
}
SINGLE_BATTLE_TEST("Poison deals 1/8th damage per turn")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_POISON); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
for (i = 0; i < 4; i++)
TURN {}
} SCENE {
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
for (i = 0; i < 4; i++)
HP_BAR(player, damage: maxHP / 8);
}
}
SINGLE_BATTLE_TEST("Burn deals 1/16th damage per turn")
{
GIVEN {
ASSUME(B_BURN_DAMAGE >= GEN_LATEST);
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_BURN); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
for (i = 0; i < 4; i++)
TURN {}
} SCENE {
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
for (i = 0; i < 4; i++)
HP_BAR(player, damage: maxHP / 16);
}
}
SINGLE_BATTLE_TEST("Burn reduces attack by 50%", s16 damage)
{
bool32 burned;
PARAMETRIZE { burned = FALSE; }
PARAMETRIZE { burned = TRUE; }
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { if (burned) Status1(STATUS1_BURN); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_TACKLE); }
} SCENE {
HP_BAR(opponent, captureDamage: &results[i].damage);
} FINALLY {
EXPECT_MUL_EQ(results[0].damage, Q_4_12(0.5), results[1].damage);
}
}
SINGLE_BATTLE_TEST("Freeze has a 20% chance of being thawed")
{
PASSES_RANDOMLY(20, 100);
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_FREEZE); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_CELEBRATE); }
} SCENE {
STATUS_ICON(player, none: TRUE);
}
}
SINGLE_BATTLE_TEST("Freeze is thawed by opponent's Fire-type attacks")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_EMBER].type == TYPE_FIRE);
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_FREEZE); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_EMBER); }
} SCENE {
MESSAGE("Wobbuffet is frozen solid!");
MESSAGE("Foe Wobbuffet used Ember!");
MESSAGE("Wobbuffet was defrosted!");
STATUS_ICON(player, none: TRUE);
}
}
SINGLE_BATTLE_TEST("Freeze is thawed by user's Flame Wheel")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_FLAME_WHEEL].flags & FLAG_THAW_USER);
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_FREEZE); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_FLAME_WHEEL); }
} SCENE {
MESSAGE("Wobbuffet was defrosted by Flame Wheel!");
STATUS_ICON(player, none: TRUE);
MESSAGE("Wobbuffet used Flame Wheel!");
}
}
SINGLE_BATTLE_TEST("Paralysis reduces speed by 50%")
{
u16 playerSpeed;
bool32 playerFirst;
PARAMETRIZE { playerSpeed = 98; playerFirst = FALSE; }
PARAMETRIZE { playerSpeed = 102; playerFirst = TRUE; }
GIVEN {
ASSUME(B_PARALYSIS_SPEED >= GEN_7);
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_PARALYSIS); Speed(playerSpeed); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(50); }
} WHEN {
TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_CELEBRATE); }
} SCENE {
if (playerFirst) {
ONE_OF {
MESSAGE("Wobbuffet used Celebrate!");
MESSAGE("Wobbuffet is paralyzed! It can't move!");
}
MESSAGE("Foe Wobbuffet used Celebrate!");
} else {
MESSAGE("Foe Wobbuffet used Celebrate!");
ONE_OF {
MESSAGE("Wobbuffet used Celebrate!");
MESSAGE("Wobbuffet is paralyzed! It can't move!");
}
}
}
}
SINGLE_BATTLE_TEST("Paralysis has a 25% chance of skipping the turn")
{
PASSES_RANDOMLY(25, 100);
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_PARALYSIS); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_CELEBRATE); }
} SCENE {
MESSAGE("Wobbuffet is paralyzed! It can't move!");
}
}
SINGLE_BATTLE_TEST("Bad poison deals 1/16th cumulative damage per turn")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_TOXIC_POISON); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
for (i = 0; i < 4; i++)
TURN {}
} SCENE {
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
for (i = 0; i < 4; i++)
HP_BAR(player, damage: maxHP / 16 * (i + 1));
}
}
SINGLE_BATTLE_TEST("Bad poison cumulative damage resets on switch")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_TOXIC_POISON); }
PLAYER(SPECIES_WYNAUT);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN {}
TURN {}
TURN { SWITCH(player, 1); }
TURN { SWITCH(player, 0); }
TURN {}
TURN {}
} SCENE {
s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP);
for (i = 0; i < 2; i++)
HP_BAR(player, damage: maxHP / 16 * (i + 1));
for (i = 0; i < 2; i++)
HP_BAR(player, damage: maxHP / 16 * (i + 1));
}
}

142
test/test.h Normal file
View File

@ -0,0 +1,142 @@
#ifndef GUARD_TEST_H
#define GUARD_TEST_H
#include "test_runner.h"
#define MAX_PROCESSES 32 // See also tools/mgba-rom-test-hydra/main.c
enum TestResult
{
TEST_RESULT_FAIL,
TEST_RESULT_PASS,
TEST_RESULT_SKIP,
TEST_RESULT_INVALID,
TEST_RESULT_ERROR,
TEST_RESULT_TIMEOUT,
};
struct TestRunner
{
u32 (*estimateCost)(void *);
void (*setUp)(void *);
void (*run)(void *);
void (*tearDown)(void *);
bool32 (*checkProgress)(void *);
bool32 (*handleExitWithResult)(void *, enum TestResult);
};
struct Test
{
const char *name;
const char *filename;
const struct TestRunner *runner;
void *data;
};
struct TestRunnerState
{
u8 state;
u8 exitCode;
s32 tests;
s32 passes;
s32 skips;
const char *skipFilename;
const struct Test *test;
u32 processCosts[MAX_PROCESSES];
u8 result;
u8 expectedResult;
u32 timeoutSeconds;
};
extern const u8 gTestRunnerN;
extern const u8 gTestRunnerI;
extern const char gTestRunnerArgv[256];
extern const struct TestRunner gAssumptionsRunner;
extern struct TestRunnerState gTestRunnerState;
void CB2_TestRunner(void);
void Test_ExpectedResult(enum TestResult);
void Test_ExitWithResult(enum TestResult, const char *fmt, ...);
s32 MgbaPrintf_(const char *fmt, ...);
#define ASSUMPTIONS \
static void Assumptions(void); \
__attribute__((section(".tests"))) static const struct Test sAssumptions = \
{ \
.name = "ASSUMPTIONS: " __FILE__, \
.filename = __FILE__, \
.runner = &gAssumptionsRunner, \
.data = Assumptions, \
}; \
static void Assumptions(void)
#define ASSUME(c) \
do \
{ \
if (!(c)) \
Test_ExitWithResult(TEST_RESULT_SKIP, "%s:%d: ASSUME failed", gTestRunnerState.test->filename, __LINE__); \
} while (0)
#define EXPECT(c) \
do \
{ \
if (!(c)) \
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT failed", gTestRunnerState.test->filename, __LINE__); \
} while (0)
#define EXPECT_EQ(a, b) \
do \
{ \
typeof(a) _a = (a), _b = (b); \
if (_a != _b) \
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_EQ(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \
} while (0)
#define EXPECT_NE(a, b) \
do \
{ \
typeof(a) _a = (a), _b = (b); \
if (_a == _b) \
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_NE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \
} while (0)
#define EXPECT_LT(a, b) \
do \
{ \
typeof(a) _a = (a), _b = (b); \
if (_a >= _b) \
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_LT(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \
} while (0)
#define EXPECT_LE(a, b) \
do \
{ \
typeof(a) _a = (a), _b = (b); \
if (_a > _b) \
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_LE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \
} while (0)
#define EXPECT_GT(a, b) \
do \
{ \
typeof(a) _a = (a), _b = (b); \
if (_a <= _b) \
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_GT(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \
} while (0)
#define EXPECT_GE(a, b) \
do \
{ \
typeof(a) _a = (a), _b = (b); \
if (_a < _b) \
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_GE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \
} while (0)
#define KNOWN_FAILING \
Test_ExpectedResult(TEST_RESULT_FAIL)
#endif

826
test/test_battle.h Normal file
View File

@ -0,0 +1,826 @@
/* Embedded DSL for automated black-box testing of battle mechanics.
*
* To run all the tests use:
* make check
* To run specific tests, e.g. Spikes ones, use:
* make check TESTS='Spikes'
* To build a ROM (pokemerald-test.elf) that can be opened in mgba to
* view specific tests, e.g. Spikes ones, use:
* make pokeemerald-test.elf TESTS='Spikes'
*
* Manually testing a battle mechanic often follows this pattern:
* 1. Create a party which can activate the mechanic.
* 2. Start a battle and play a few turns which activate the mechanic.
* 3. Look at the UI outputs to decide if the mechanic works.
*
* Automated testing follows the same pattern:
* 1. Initialize the party in GIVEN.
* 2. Play the turns in WHEN.
* 3. Check the UI outputs in SCENE.
*
* As a concrete example, to manually test EFFECT_PARALYZE, e.g. the
* effect of Stun Spore you might:
* 1. Put a Wobbuffet that knows Stun Spore in your party.
* 2. Battle a wild Wobbuffet.
* 3. Use Stun Spore.
* 4. Check that the Wobbuffet is paralyzed.
*
* This can be translated to an automated test as follows:
*
* ASSUMPTIONS
* {
* ASSUME(gBattleMoves[MOVE_STUN_SPORE].effect == EFFECT_PARALYZE);
* }
*
* SINGLE_BATTLE_TEST("Stun Spore inflicts paralysis")
* {
* GIVEN {
* PLAYER(SPECIES_WOBBUFFET); // 1.
* OPPONENT(SPECIES_WOBBUFFET); // 2.
* } WHEN {
* TURN { MOVE(player, MOVE_STUN_SPORE); } // 3.
* } SCENE {
* ANIMATION(ANIM_TYPE_MOVE, MOVE_STUN_SPORE, player);
* MESSAGE("Foe Wobbuffet is paralyzed! It may be unable to move!"); // 4
* STATUS_ICON(opponent, paralysis: TRUE); // 4.
* }
* }
*
* The ASSUMPTIONS block documents that Stun Spore has EFFECT_PARALYZE.
* If Stun Spore did not have that effect it would cause the tests in
* the file to be skipped. We write our tests like this so that hackers
* can change the effects of moves without causing tests to fail.
*
* SINGLE_BATTLE_TEST defines the name of the test. Related tests should
* start with the same prefix, e.g. Stun Spore tests should start with
* "Stun Spore", this allows just the Stun Spore-related tests to be run
* with:
* make check TESTS='Stun Spore'
*
* GIVEN initializes the parties, PLAYER and OPPONENT add a Pokémon to
* their respective parties. They can both accept a block which further
* customizes the Pokémon's stats, moves, item, ability, etc.
*
* WHEN describes the turns, and TURN describes the choices made in a
* single turn. MOVE causes the player to use Stun Spore and adds the
* move to the Pokémon's moveset if an explicit Moves was not specified.
* Pokémon that are not mentioned in a TURN use Celebrate.
* The test runner attempts to rig the RNG so that the first move used
* in a turn does not miss and activates its secondary effects (if any).
*
* SCENE describes the player-visible output of the battle. In this case
* ANIMATION checks that the Stun Spore animation played, MESSAGE checks
* the paralysis message was shown, and STATUS_ICON checks that the
* opponent's HP bar shows a PRZ icon.
*
* As a second example, to manually test that Stun Spore does not effect
* Grass-types you might:
* 1. Put a Wobbuffet that knows Stun Spore in your party.
* 2. Battle a wild Oddish.
* 3. Use Stun Spore.
* 4. Check that the move animation does not play.
* 5. Check that a "It doesn't affect Foe Oddish…" message is shown.
*
* This can again be translated as follows:
*
* SINGLE_BATTLE_TEST("Stun Spore does not affect Grass-types")
* {
* GIVEN {
* ASSUME(gBattleMoves[MOVE_STUN_SPORE].flags & FLAG_POWDER);
* ASSUME(gSpeciesInfo[SPECIES_ODDISH].types[0] == TYPE_GRASS);
* PLAYER(SPECIES_ODDISH); // 1.
* OPPONENT(SPECIES_ODDISH); // 2.
* } WHEN {
* TURN { MOVE(player, MOVE_STUN_SPORE); } // 3.
* } SCENE {
* NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_STUN_SPORE, player); // 4.
* MESSAGE("It doesn't affect Foe Oddish…"); // 5.
* }
* }
*
* The ASSUMEs are documenting the reasons why Stun Spore does not
* affect Oddish, namely that Stun Spore is a powder move, and Oddish
* is a Grass-type. These ASSUMEs function similarly to the ones in
* ASSUMPTIONS but apply only to the one test.
*
* NOT inverts the meaning of a SCENE check, so applying it to ANIMATION
* requires that the Stun Spore animation does not play. MESSAGE checks
* that the message was shown. The checks in SCENE are ordered, so
* together this says "The doesn't affect message is shown, and the Stun
* Spore animation does not play at any time before that". Normally you
* would only test one or the other, or even better, just
* NOT STATUS_ICON(opponent, paralysis: TRUE); to say that Oddish was
* not paralyzed without specifying the exact outputs which led to that.
*
* As a final example, to test that Howl works you might:
* 1. Put a Wobbuffet that knows Howl and Tackle in your party.
* 2. Battle a wild Wobbuffet.
* 3. Use Tackle and note the amount the HP bar reduced.
* 4. Battle a wild Wobbuffet.
* 5. Use Howl and that that the stat change animation and message play.
* 6. Use Tackle and check that the HP bar reduced by more than in 3.
*
* This can be translated to an automated test as follows:
*
* SINGLE_BATTLE_TEST("Howl raises Attack", s16 damage)
* {
* bool32 raiseAttack;
* PARAMETRIZE { raiseAttack = FALSE; }
* PARAMETRIZE { raiseAttack = TRUE; }
* GIVEN {
* ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL);
* PLAYER(SPECIES_WOBBUFFET);
* OPPONENT(SPECIES_WOBBUFFET);
* } WHEN {
* if (raiseAttack) TURN { MOVE(player, MOVE_HOWL); } // 5.
* TURN { MOVE(player, MOVE_TACKLE); } // 3 & 6.
* } SCENE {
* if (raiseAttack) {
* ANIMATION(ANIM_TYPE_MOVE, MOVE_HOWL, player);
* ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); // 5.
* MESSAGE("Wobbuffet's attack rose!"); // 5.
* }
* ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
* HP_BAR(opponent, captureDamage: &results[i].damage); // 3 & 6.
* } FINALLY {
* EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage); // 6.
* }
* }
*
* PARAMETRIZE causes a test to run multiple times, once per PARAMETRIZE
* block (e.g. once with raiseAttack = FALSE and once with raiseAttack =
* TRUE).
* HP_BAR's captureDamage causes the change in HP to be stored in a
* variable, and the variable chosen is results[i].damage. results[i]
* contains all the variables defined at the end of SINGLE_BATTLE_TEST,
* i is the current PARAMETRIZE index.
* FINALLY runs after the last parameter has finished, and uses
* EXPECT_MUL_EQ to check that the second battle deals 1.5× the damage
* of the first battle (with a small tolerance to account for rounding).
*
* You might notice that all the tests check the outputs the player
* could see rather than the internal battle state. e.g. the Howl test
* could have used gBattleMons[B_POSITION_OPPONENT_LEFT].hp instead of
* using HP_BAR to capture the damage. This is a deliberate choice, by
* checking what the player can observe the tests are more robust to
* refactoring, e.g. if gBattleMons got moved into gBattleStruct then
* any test that used it would need to be updated.
*
* REFERENCE
* =========
*
* ASSUME(cond)
* Causes the test to be skipped if cond is false. Used to document any
* prerequisites of the test, e.g. to test Burn reducing the Attack of a
* Pokémon we can observe the damage of a physical attack with and
* without the burn. To document that this test assumes the attack is
* physical we can use:
* ASSUME(gBattleMoves[MOVE_WHATEVER].split == SPLIT_PHYSICAL);
*
* ASSUMPTIONS
* Should be placed immediately after any #includes and contain any
* ASSUMEs which should apply to the whole file, e.g. to test
* EFFECT_POISON_HIT we need to choose a move with that effect, if
* we chose to use Poison Sting in every test then the top of
* move_effect_poison_hit.c should be:
* ASSUMPTIONS
* {
* ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT);
* }
*
* SINGLE_BATTLE_TEST(name, results...) and DOUBLE_BATTLE_TEST(name, results...)
* Define single- and double- battles. The names should start with the
* name of the mechanic being tested so that it is easier to run all the
* related tests. results contains variable declarations to be placed
* into the results array which is available in PARAMETRIZEd tests.
* The main differences for doubles are:
* - Move targets sometimes need to be explicit.
* - Instead of player and opponent there is playerLeft, playerRight,
* opponentLeft, and opponentRight.
*
* KNOWN_FAILING
* Marks a test as not passing due to a bug. If there is an issue number
* associated with the bug it should be included in a comment. If the
* test passes the developer will be notified to remove KNOWN_FAILING.
* For example:
* SINGLE_BATTLE_TEST("Jump Kick has no recoil if no target")
* {
* KNOWN_FAILING; // #2596.
*
* PARAMETRIZE
* Runs a test multiple times. i will be set to which parameter is
* running, and results will contain an entry for each parameter, e.g.:
* SINGLE_BATTLE_TEST("Blaze boosts Fire-type moves in a pinch", s16 damage)
* {
* u16 hp;
* PARAMETRIZE { hp = 99; }
* PARAMETRIZE { hp = 33; }
* GIVEN {
* ASSUME(gBattleMoves[MOVE_EMBER].type == TYPE_FIRE);
* PLAYER(SPECIES_CHARMANDER) { Ability(ABILITY_BLAZE); MaxHP(99); HP(hp); }
* OPPONENT(SPECIES_WOBBUFFET);
* } WHEN {
* TURN { MOVE(player, MOVE_EMBER); }
* } SCENE {
* HP_BAR(opponent, captureDamage: &results[i].damage);
* } FINALLY {
* EXPECT(results[1].damage > results[0].damage);
* }
* }
*
* PASSES_RANDOMLY(successes, trials)
* Checks that the test passes approximately successes/trials. Used for
* testing RNG-based attacks, e.g.:
* PASSES_RANDOMLY(gBattleMoves[move].accuracy, 100);
* Note that PASSES_RANDOMLY makes the tests run very slowly and should
* be avoided where possible.
*
* GIVEN
* Contains the initial state of the parties before the battle.
*
* RNGSeed(seed)
* Explicitly sets the RNG seed. Try to avoid using this because it is a
* very fragile tool.
* Example:
* GIVEN {
* RNGSeed(0xC0DEIDEA);
*
* PLAYER(species) and OPPONENT(species)
* Adds the species to the player's or opponent's party respectively.
* The Pokémon can be further customized with the following functions:
* - Gender(MON_MALE | MON_FEMALE)
* - Nature(nature)
* - Ability(ability)
* - Level(level)
* - MaxHP(n), HP(n), Attack(n), Defense(n), SpAttack(n), SpDefense(n)
* - Speed(n)
* - Item(item)
* - Moves(moves...)
* - Friendship(friendship)
* - Status1(status1)
* For example to create a Wobbuffet that is poisoned:
* PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_POISON); }
* Note if Speed is specified for any Pokémon then it must be specified
* for all Pokémon.
* Note if Moves is specified then MOVE will not automatically add moves
* to the moveset.
*
* WHEN
* Contains the choices that battlers make during the battle.
*
* TURN
* Groups the choices made by the battlers on a single turn. If Speeds
* have not been explicitly specified then the order of the MOVEs in the
* TURN will be used to infer the Speeds of the Pokémon, e.g.:
* // player's speed will be greater than opponent's speed.
* TURN { MOVE(player, MOVE_SPLASH); MOVE(opponent, MOVE_SPLASH); }
* // opponent's speed will be greater than player's speed.
* TURN { MOVE(opponent, MOVE_SPLASH); MOVE(player, MOVE_SPLASH); }
* The inference process is naive, if your test contains anything that
* modifies the speed of a battler you should specify them explicitly.
*
* MOVE(battler, move | moveSlot:, [megaEvolve:], [hit:], [criticalHit:], [target:], [allowed:])
* Used when the battler chooses Fight. Either the move ID or move slot
* must be specified. megaEvolve: TRUE causes the battler to Mega Evolve
* if able, hit: FALSE causes the move to miss, criticalHit: TRUE causes
* the move to land a critical hit, target: is used in double battles to
* choose the target (when necessary), and allowed: FALSE is used to
* reject an illegal move e.g. a Disabled one.
* MOVE(playerLeft, MOVE_TACKLE, target: opponentRight);
* If the battler does not have an explicit Moves specified the moveset
* will be populated based on the MOVEs it uses.
*
* FORCED_MOVE(battler)
* Used when the battler chooses Fight and then their move is chosen for
* them, e.g. when affected by Encore.
* FORCED_MOVE(player);
*
* SWITCH(battler, partyIndex)
* Used when the battler chooses Switch.
* SWITCH(player, 1);
*
* SKIP_TURN(battler)
* Used when the battler cannot choose an action, e.g. when locked into
* Thrash.
* SKIP_TURN(player);
*
* SEND_OUT(battler, partyIndex)
* Used when the battler chooses to switch to another Pokémon but not
* via Switch, e.g. after fainting or due to a U-turn.
* SEND_OUT(player, 1);
*
* SCENE
* Contains an abridged description of the UI during the THEN. The order
* of the description must match too, e.g.
* // ABILITY_POPUP followed by a MESSAGE
* ABILITY_POPUP(player, ABILITY_STURDY);
* MESSAGE("Geodude was protected by Sturdy!");
*
* ABILITY_POPUP(battler, [ability])
* Causes the test to fail if the battler's ability pop-up is not shown.
* If specified, ability is the ability shown in the pop-up.
* ABILITY_POPUP(opponent, ABILITY_MOLD_BREAKER);
*
* ANIMATION(type, animId, [battler], [target:])
* Causes the test to fail if the animation does not play. A common use
* of this command is to check if a move was successful, e.g.:
* ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
* target can only be specified for ANIM_TYPE_MOVE.
*
* HP_BAR(battler, [damage: | hp: | captureDamage: | captureHP:])
* If hp: or damage: are used, causes the test to fail if that amount of
* damage is not dealt, e.g.:
* HP_BAR(player, hp: 0);
* If captureDamage: or captureHP: are used, causes the test to fail if
* the HP bar does not change, and then writes that change to the
* pointer, e.g.:
* s16 damage;
* HP_BAR(player, captureDamage: &damage);
* If none of the above are used, causes the test to fail if the HP
* changes at all.
*
* MESSAGE(pattern)
* Causes the test to fail if the message in pattern is not displayed.
* Spaces in pattern match newlines (\n, \l, and \p) in the message.
* Often used to check that a battler took its turn but it failed, e.g.:
* MESSAGE("Wobbuffet used Dream Eater!");
* MESSAGE("Foe Wobbuffet wasn't affected!");
*
* STATUS_ICON(battler, status1 | none: | sleep: | poison: | burn: | freeze: | paralysis:, badPoison:)
* Causes the test to fail if the battler's status is not changed to the
* specified status.
* STATUS_ICON(player, badPoison: TRUE);
* If the expected status icon is parametrized the corresponding STATUS1
* constant can be provided, e.g.:
* u32 status1;
* PARAMETRIZE { status1 = STATUS1_NONE; }
* PARAMETRIZE { status1 = STATUS1_BURN; }
* ...
* STATUS_ICON(player, status1);
*
* NOT
* Causes the test to fail if the SCENE command succeeds before the
* following command succeeds.
* // Our Wobbuffet does not Celebrate before the foe's.
* NOT MESSAGE("Wobbuffet used Celebrate!");
* MESSAGE("Foe Wobbuffet used Celebrate!");
* WARNING: NOT is an alias of NONE_OF, so it behaves surprisingly when
* applied to multiple commands wrapped in braces.
*
* ONE_OF
* Causes the test to fail unless one of the SCENE commands succeeds.
* ONE_OF {
* MESSAGE("Wobbuffet used Celebrate!");
* MESSAGE("Wobbuffet is paralyzed! It can't move!");
* }
*
* NONE_OF
* Causes the test to fail if one of the SCENE commands succeeds before
* the command after the NONE_OF succeeds.
* // Our Wobbuffet does not move before the foe's.
* NONE_OF {
* MESSAGE("Wobbuffet used Celebrate!");
* MESSAGE("Wobbuffet is paralyzed! It can't move!");
* }
* MESSAGE("Foe Wobbuffet used Celebrate!");
*
* PLAYER_PARTY and OPPONENT_PARTY
* Refer to the party members defined in GIVEN, e.g.:
* s32 maxHP = GetMonData(&OPPONENT_PARTY[0], MON_DATA_MAX_HP);
* HP_BAR(opponent, damage: maxHP / 2);
*
* THEN
* Contains code to run after the battle has finished. If the test is
* PARAMETRIZEd then EXPECTs between the results should go here. Is also
* occasionally used to check the internal battle state when checking
* the behavior via a SCENE is too difficult, verbose, or error-prone.
*
* FINALLY
* Contains checks to run after all PARAMETERIZEs have run. Prefer to
* write your checks in THEN where possible, because a failure in THEN
* will be tagged with which parameter it corresponds to.
*
* EXPECT(cond)
* Causes the test to fail if cond is false.
*
* EXPECT_EQ(a, b), EXPECT_NE(a, b), EXPECT_LT(a, b), EXPECT_LE(a, b), EXPECT_GT(a, b), EXPECT_GE(a, b)
* Causes the test to fail if a and b compare incorrectly, e.g.
* EXPECT_EQ(results[0].damage, results[1].damage);
*
* EXPECT_MUL_EQ(a, m, b)
* Causes the test to fail if a*m != b (within a threshold), e.g.
* // Expect results[0].damage * 1.5 == results[1].damage.
* EXPECT_EQ(results[0].damage, Q_4_12(1.5), results[1].damage); */
#ifndef GUARD_TEST_BATTLE_H
#define GUARD_TEST_BATTLE_H
#include "battle.h"
#include "battle_anim.h"
#include "data.h"
#include "item.h"
#include "recorded_battle.h"
#include "test.h"
#include "util.h"
#include "constants/abilities.h"
#include "constants/battle_anim.h"
#include "constants/battle_move_effects.h"
#include "constants/hold_effects.h"
#include "constants/items.h"
#include "constants/moves.h"
#include "constants/species.h"
// NOTE: If the stack is too small the test runner will probably crash
// or loop.
#define BATTLE_TEST_STACK_SIZE 1024
#define MAX_QUEUED_EVENTS 16
enum { BATTLE_TEST_SINGLES, BATTLE_TEST_DOUBLES };
typedef void (*SingleBattleTestFunction)(void *, u32, struct BattlePokemon *, struct BattlePokemon *);
typedef void (*DoubleBattleTestFunction)(void *, u32, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *);
struct BattleTest
{
u8 type;
u16 sourceLine;
union
{
SingleBattleTestFunction singles;
DoubleBattleTestFunction doubles;
} function;
size_t resultsSize;
};
enum
{
QUEUED_ABILITY_POPUP_EVENT,
QUEUED_ANIMATION_EVENT,
QUEUED_HP_EVENT,
QUEUED_MESSAGE_EVENT,
QUEUED_STATUS_EVENT,
};
struct QueuedAbilityEvent
{
u8 battlerId;
u16 ability;
};
struct QueuedAnimationEvent
{
u8 type;
u16 id;
u8 attacker:4;
u8 target:4;
};
enum { HP_EVENT_NEW_HP, HP_EVENT_DELTA_HP };
struct QueuedHPEvent
{
u32 battlerId:3;
u32 type:1;
u32 address:28;
};
struct QueuedMessageEvent
{
const u8 *pattern;
};
struct QueuedStatusEvent
{
u32 battlerId:3;
u32 mask:8;
u32 unused_01:21;
};
struct QueuedEvent
{
u8 type;
u8 sourceLineOffset;
u8 groupType:2;
u8 groupSize:6;
union
{
struct QueuedAbilityEvent ability;
struct QueuedAnimationEvent animation;
struct QueuedHPEvent hp;
struct QueuedMessageEvent message;
struct QueuedStatusEvent status;
} as;
};
struct BattleTestData
{
u8 stack[BATTLE_TEST_STACK_SIZE];
u8 playerPartySize;
u8 opponentPartySize;
u8 explicitMoves[NUM_BATTLE_SIDES];
bool8 hasExplicitSpeeds;
u8 explicitSpeeds[NUM_BATTLE_SIDES];
u16 slowerThan[NUM_BATTLE_SIDES][PARTY_SIZE];
u8 currentSide;
u8 currentPartyIndex;
struct Pokemon *currentMon;
u8 gender;
u8 nature;
u8 currentMonIndexes[MAX_BATTLERS_COUNT];
u8 turnState;
u8 turns;
u8 actionBattlers;
u8 moveBattlers;
bool8 hasRNGActions:1;
struct RecordedBattleSave recordedBattle;
u8 battleRecordTypes[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE];
u8 battleRecordSourceLineOffsets[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE];
u16 recordIndexes[MAX_BATTLERS_COUNT];
u8 lastActionTurn;
u8 nextRNGTurn;
u8 queuedEventsCount;
u8 queueGroupType;
u8 queueGroupStart;
u8 queuedEvent;
struct QueuedEvent queuedEvents[MAX_QUEUED_EVENTS];
};
struct BattleTestRunnerState
{
u8 battlersCount;
u8 parametersCount; // Valid only in BattleTest_Setup.
u8 parameters;
u8 runParameter;
u8 trials;
u8 expectedPasses;
u8 observedPasses;
u8 skippedTrials;
u8 runTrial;
bool8 runRandomly:1;
bool8 runGiven:1;
bool8 runWhen:1;
bool8 runScene:1;
bool8 runThen:1;
bool8 runFinally:1;
bool8 runningFinally:1;
struct BattleTestData data;
u8 *results;
u8 checkProgressParameter;
u8 checkProgressTrial;
u8 checkProgressTurn;
};
extern const struct TestRunner gBattleTestRunner;
extern struct BattleTestRunnerState *gBattleTestRunnerState;
#define MEMBERS(...) VARARG_8(MEMBERS_, __VA_ARGS__)
#define MEMBERS_0()
#define MEMBERS_1(a) a;
#define MEMBERS_2(a, b) a; b;
#define MEMBERS_3(a, b, c) a; b; c;
#define MEMBERS_4(a, b, c, d) a; b; c; d;
#define MEMBERS_5(a, b, c, d, e) a; b; c; d; e;
#define MEMBERS_6(a, b, c, d, e, f) a; b; c; d; e; f;
#define MEMBERS_7(a, b, c, d, e, f, g) a; b; c; d; e; f; g;
#define MEMBERS_8(a, b, c, d, e, f, g, h) a; b; c; d; e; f; g; h;
#define APPEND_TRUE(...) VARARG_8(APPEND_TRUE_, __VA_ARGS__)
#define APPEND_TRUE_0()
#define APPEND_TRUE_1(a) a, TRUE
#define APPEND_TRUE_2(a, b) a, TRUE, b, TRUE
#define APPEND_TRUE_3(a, b, c) a, TRUE, b, TRUE, c, TRUE
#define APPEND_TRUE_4(a, b, c, d) a, TRUE, b, TRUE, c, TRUE, d, TRUE
#define APPEND_TRUE_5(a, b, c, d, e) a, TRUE, b, TRUE, c, TRUE, d, TRUE, e, TRUE
#define APPEND_TRUE_6(a, b, c, d, e, f) a, TRUE, b, TRUE, c, TRUE, d, TRUE, e, TRUE, f, TRUE
#define APPEND_TRUE_7(a, b, c, d, e, f, g) a, TRUE, b, TRUE, c, TRUE, d, TRUE, e, TRUE, f, TRUE, g, TRUE
#define APPEND_TRUE_8(a, b, c, d, e, f, g, h) a, TRUE, b, TRUE, c, TRUE, d, TRUE, e, TRUE, f, TRUE, g, TRUE, h, TRUE
/* Test */
#define SINGLE_BATTLE_TEST(_name, ...) \
struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *); \
__attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \
{ \
.name = _name, \
.filename = __FILE__, \
.runner = &gBattleTestRunner, \
.data = (void *)&(const struct BattleTest) \
{ \
.type = BATTLE_TEST_SINGLES, \
.sourceLine = __LINE__, \
.function = { .singles = (SingleBattleTestFunction)CAT(Test, __LINE__) }, \
.resultsSize = sizeof(struct CAT(Result, __LINE__)), \
}, \
}; \
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *player, struct BattlePokemon *opponent)
#define DOUBLE_BATTLE_TEST(_name, ...) \
struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *); \
__attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \
{ \
.name = _name, \
.filename = __FILE__, \
.runner = &gBattleTestRunner, \
.data = (void *)&(const struct BattleTest) \
{ \
.type = BATTLE_TEST_DOUBLES, \
.sourceLine = __LINE__, \
.function = { .doubles = (DoubleBattleTestFunction)CAT(Test, __LINE__) }, \
.resultsSize = sizeof(struct CAT(Result, __LINE__)), \
}, \
}; \
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *playerLeft, struct BattlePokemon *opponentLeft, struct BattlePokemon *playerRight, struct BattlePokemon *opponentRight)
/* Parametrize */
#define PARAMETRIZE if (gBattleTestRunnerState->parametersCount++ == i)
/* Randomly */
#define PASSES_RANDOMLY(passes, trials) for (; gBattleTestRunnerState->runRandomly; gBattleTestRunnerState->runRandomly = FALSE) Randomly(__LINE__, passes, trials)
void Randomly(u32 sourceLine, u32 passes, u32 trials);
/* Given */
#define GIVEN for (; gBattleTestRunnerState->runGiven; gBattleTestRunnerState->runGiven = FALSE)
#define RNGSeed(seed) RNGSeed_(__LINE__, seed)
#define PLAYER(species) for (OpenPokemon(__LINE__, B_SIDE_PLAYER, species); gBattleTestRunnerState->data.currentMon; ClosePokemon(__LINE__))
#define OPPONENT(species) for (OpenPokemon(__LINE__, B_SIDE_OPPONENT, species); gBattleTestRunnerState->data.currentMon; ClosePokemon(__LINE__))
#define Gender(gender) Gender_(__LINE__, gender)
#define Nature(nature) Nature_(__LINE__, nature)
#define Ability(ability) Ability_(__LINE__, ability)
#define Level(level) Level_(__LINE__, level)
#define MaxHP(maxHP) MaxHP_(__LINE__, maxHP)
#define HP(hp) HP_(__LINE__, hp)
#define Attack(attack) Attack_(__LINE__, attack)
#define Defense(defense) Defense_(__LINE__, defense)
#define SpAttack(spAttack) SpAttack_(__LINE__, spAttack)
#define SpDefense(spDefense) SpDefense_(__LINE__, spDefense)
#define Speed(speed) Speed_(__LINE__, speed)
#define Item(item) Item_(__LINE__, item)
#define Moves(move1, ...) Moves_(__LINE__, (const u16 [MAX_MON_MOVES]) { move1, __VA_ARGS__ })
#define Friendship(friendship) Friendship_(__LINE__, friendship)
#define Status1(status1) Status1_(__LINE__, status1)
void OpenPokemon(u32 sourceLine, u32 side, u32 species);
void ClosePokemon(u32 sourceLine);
void RNGSeed_(u32 sourceLine, u32 seed);
void Gender_(u32 sourceLine, u32 gender);
void Nature_(u32 sourceLine, u32 nature);
void Ability_(u32 sourceLine, u32 ability);
void Level_(u32 sourceLine, u32 level);
void MaxHP_(u32 sourceLine, u32 maxHP);
void HP_(u32 sourceLine, u32 hp);
void Attack_(u32 sourceLine, u32 attack);
void Defense_(u32 sourceLine, u32 defense);
void SpAttack_(u32 sourceLine, u32 spAttack);
void SpDefense_(u32 sourceLine, u32 spDefense);
void Speed_(u32 sourceLine, u32 speed);
void Item_(u32 sourceLine, u32 item);
void Moves_(u32 sourceLine, const u16 moves[MAX_MON_MOVES]);
void Friendship_(u32 sourceLine, u32 friendship);
void Status1_(u32 sourceLine, u32 status1);
#define PLAYER_PARTY (gBattleTestRunnerState->data.recordedBattle.playerParty)
#define OPPONENT_PARTY (gBattleTestRunnerState->data.recordedBattle.opponentParty)
/* When */
#define WHEN for (; gBattleTestRunnerState->runWhen; gBattleTestRunnerState->runWhen = FALSE)
enum { TURN_CLOSED, TURN_OPEN, TURN_CLOSING };
#define TURN for (OpenTurn(__LINE__); gBattleTestRunnerState->data.turnState == TURN_OPEN; CloseTurn(__LINE__))
#define MOVE(battler, ...) Move(__LINE__, battler, (struct MoveContext) { APPEND_TRUE(__VA_ARGS__) })
#define FORCED_MOVE(battler) ForcedMove(__LINE__, battler)
#define SWITCH(battler, partyIndex) Switch(__LINE__, battler, partyIndex)
#define SKIP_TURN(battler) SkipTurn(__LINE__, battler)
#define SEND_OUT(battler, partyIndex) SendOut(__LINE__, battler, partyIndex)
struct MoveContext
{
u16 move;
u16 explicitMove:1;
u16 moveSlot:2;
u16 explicitMoveSlot:1;
u16 hit:1;
u16 explicitHit:1;
u16 criticalHit:1;
u16 explicitCriticalHit:1;
u16 megaEvolve:1;
u16 explicitMegaEvolve:1;
// TODO: u8 zMove:1;
u16 allowed:1;
u16 explicitAllowed:1;
struct BattlePokemon *target;
bool8 explicitTarget;
};
void OpenTurn(u32 sourceLine);
void CloseTurn(u32 sourceLine);
void Move(u32 sourceLine, struct BattlePokemon *, struct MoveContext);
void ForcedMove(u32 sourceLine, struct BattlePokemon *);
void Switch(u32 sourceLine, struct BattlePokemon *, u32 partyIndex);
void SkipTurn(u32 sourceLine, struct BattlePokemon *);
void SendOut(u32 sourceLine, struct BattlePokemon *, u32 partyIndex);
/* Scene */
#define SCENE for (; gBattleTestRunnerState->runScene; gBattleTestRunnerState->runScene = FALSE)
#define ONE_OF for (OpenQueueGroup(__LINE__, QUEUE_GROUP_ONE_OF); gBattleTestRunnerState->data.queueGroupType != QUEUE_GROUP_NONE; CloseQueueGroup(__LINE__))
#define NONE_OF for (OpenQueueGroup(__LINE__, QUEUE_GROUP_NONE_OF); gBattleTestRunnerState->data.queueGroupType != QUEUE_GROUP_NONE; CloseQueueGroup(__LINE__))
#define NOT NONE_OF
#define ABILITY_POPUP(battler, ...) QueueAbility(__LINE__, battler, (struct AbilityEventContext) { __VA_ARGS__ })
#define ANIMATION(type, id, ...) QueueAnimation(__LINE__, type, id, (struct AnimationEventContext) { __VA_ARGS__ })
#define HP_BAR(battler, ...) QueueHP(__LINE__, battler, (struct HPEventContext) { APPEND_TRUE(__VA_ARGS__) })
#define MESSAGE(pattern) QueueMessage(__LINE__, (const u8 []) _(pattern))
#define STATUS_ICON(battler, status) QueueStatus(__LINE__, battler, (struct StatusEventContext) { status })
enum QueueGroupType
{
QUEUE_GROUP_NONE,
QUEUE_GROUP_ONE_OF,
QUEUE_GROUP_NONE_OF,
};
struct AbilityEventContext
{
u16 ability;
};
struct AnimationEventContext
{
struct BattlePokemon *attacker;
struct BattlePokemon *target;
};
struct HPEventContext
{
u8 _;
u16 hp;
bool8 explicitHP;
s16 damage;
bool8 explicitDamage;
u16 *captureHP;
bool8 explicitCaptureHP;
s16 *captureDamage;
bool8 explicitCaptureDamage;
};
struct StatusEventContext
{
u8 status1;
bool8 none:1;
bool8 sleep:1;
bool8 poison:1;
bool8 burn:1;
bool8 freeze:1;
bool8 paralysis:1;
bool8 badPoison:1;
};
void OpenQueueGroup(u32 sourceLine, enum QueueGroupType);
void CloseQueueGroup(u32 sourceLine);
void QueueAbility(u32 sourceLine, struct BattlePokemon *battler, struct AbilityEventContext);
void QueueAnimation(u32 sourceLine, u32 type, u32 id, struct AnimationEventContext);
void QueueHP(u32 sourceLine, struct BattlePokemon *battler, struct HPEventContext);
void QueueMessage(u32 sourceLine, const u8 *pattern);
void QueueStatus(u32 sourceLine, struct BattlePokemon *battler, struct StatusEventContext);
/* Then */
#define THEN for (; gBattleTestRunnerState->runThen; gBattleTestRunnerState->runThen = FALSE)
/* Finally */
#define FINALLY for (; gBattleTestRunnerState->runFinally; gBattleTestRunnerState->runFinally = FALSE) if ((gBattleTestRunnerState->runningFinally = TRUE))
/* Expect */
#define EXPECT_MUL_EQ(a, m, b) \
do \
{ \
s32 _a = (a), _m = (m), _b = (b); \
s32 _am = Q_4_12_TO_INT(_a * _m); \
s32 _t = Q_4_12_TO_INT(abs(_m) + Q_4_12_ROUND); \
if (abs(_am-_b) > _t) \
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_MUL_EQ(%d, %q, %d) failed: %d not in [%d..%d]", gTestRunnerState.test->filename, __LINE__, _a, _m, _b, _am, _b-_t, _b+_t); \
} while (0)
#endif

439
test/test_runner.c Normal file
View File

@ -0,0 +1,439 @@
#include <stdarg.h>
#include "global.h"
#include "characters.h"
#include "gpu_regs.h"
#include "main.h"
#include "malloc.h"
#include "test.h"
#include "test_runner.h"
#define TIMEOUT_SECONDS 30
void CB2_TestRunner(void);
EWRAM_DATA struct TestRunnerState gTestRunnerState;
void TestRunner_Battle(const struct Test *);
static bool32 MgbaOpen_(void);
static void MgbaExit_(u8 exitCode);
static s32 MgbaPuts_(const char *s);
static s32 MgbaVPrintf_(const char *fmt, va_list va);
static void Intr_Timer2(void);
extern const struct Test __start_tests[];
extern const struct Test __stop_tests[];
static bool32 PrefixMatch(const char *pattern, const char *string)
{
if (string == NULL)
return TRUE;
while (TRUE)
{
if (!*pattern)
return TRUE;
if (*pattern != *string)
return FALSE;
pattern++;
string++;
}
}
enum { STATE_INIT, STATE_NEXT_TEST, STATE_REPORT_RESULT, STATE_EXIT };
void CB2_TestRunner(void)
{
switch (gTestRunnerState.state)
{
case STATE_INIT:
if (!MgbaOpen_())
{
gTestRunnerState.state = STATE_EXIT;
gTestRunnerState.exitCode = 2;
return;
}
gIntrTable[7] = Intr_Timer2;
gTestRunnerState.state = STATE_NEXT_TEST;
gTestRunnerState.exitCode = 0;
gTestRunnerState.tests = 0;
gTestRunnerState.passes = 0;
gTestRunnerState.skips = 0;
gTestRunnerState.skipFilename = NULL;
gTestRunnerState.test = __start_tests - 1;
break;
case STATE_NEXT_TEST:
gTestRunnerState.test++;
if (gTestRunnerState.test == __stop_tests)
{
MgbaPrintf_("%s%d/%d PASSED\e[0m", gTestRunnerState.exitCode == 0 ? "\e[32m" : "\e[31m", gTestRunnerState.passes, gTestRunnerState.tests);
if (gTestRunnerState.skips)
{
if (gTestRunnerSkipIsFail)
MgbaPrintf_("\e[31m%d SKIPPED\e[0m", gTestRunnerState.skips);
else
MgbaPrintf_("%d SKIPPED", gTestRunnerState.skips);
}
gTestRunnerState.state = STATE_EXIT;
return;
}
if (!PrefixMatch(gTestRunnerArgv, gTestRunnerState.test->name))
return;
// Greedily assign tests to processes based on estimated cost.
// TODO: Make processCosts a min heap.
if (gTestRunnerState.test->runner != &gAssumptionsRunner)
{
u32 i;
u32 minCost, minCostProcess;
minCost = gTestRunnerState.processCosts[0];
minCostProcess = 0;
for (i = 1; i < gTestRunnerN; i++)
{
if (gTestRunnerState.processCosts[i] < minCost)
{
minCost = gTestRunnerState.processCosts[i];
minCostProcess = i;
}
}
if (gTestRunnerState.test->runner->estimateCost)
gTestRunnerState.processCosts[minCostProcess] += gTestRunnerState.test->runner->estimateCost(gTestRunnerState.test->data);
else
gTestRunnerState.processCosts[minCostProcess] += 1;
if (minCostProcess != gTestRunnerI)
return;
}
gTestRunnerState.state = STATE_REPORT_RESULT;
gTestRunnerState.result = TEST_RESULT_PASS;
gTestRunnerState.expectedResult = TEST_RESULT_PASS;
if (gTestRunnerHeadless)
gTestRunnerState.timeoutSeconds = TIMEOUT_SECONDS;
else
gTestRunnerState.timeoutSeconds = UINT_MAX;
InitHeap(gHeap, HEAP_SIZE);
EnableInterrupts(INTR_FLAG_TIMER2);
REG_TM2CNT_L = UINT16_MAX - (274 * 60); // Approx. 1 second.
REG_TM2CNT_H = TIMER_ENABLE | TIMER_INTR_ENABLE | TIMER_1024CLK;
// NOTE: Assumes that the compiler interns __FILE__.
if (gTestRunnerState.skipFilename == gTestRunnerState.test->filename)
{
gTestRunnerState.result = TEST_RESULT_SKIP;
}
else
{
MgbaPrintf_(":N%s", gTestRunnerState.test->name);
if (gTestRunnerState.test->runner->setUp)
gTestRunnerState.test->runner->setUp(gTestRunnerState.test->data);
gTestRunnerState.test->runner->run(gTestRunnerState.test->data);
}
break;
case STATE_REPORT_RESULT:
REG_TM2CNT_H = 0;
gTestRunnerState.state = STATE_NEXT_TEST;
if (gTestRunnerState.test->runner->tearDown)
gTestRunnerState.test->runner->tearDown(gTestRunnerState.test->data);
if (gTestRunnerState.test->runner == &gAssumptionsRunner)
{
if (gTestRunnerState.result != TEST_RESULT_PASS)
gTestRunnerState.skipFilename = gTestRunnerState.test->filename;
}
else if (gTestRunnerState.result == TEST_RESULT_SKIP)
{
gTestRunnerState.skips++;
if (gTestRunnerSkipIsFail)
gTestRunnerState.exitCode = 1;
}
else
{
const char *color;
const char *result;
gTestRunnerState.tests++;
if (gTestRunnerState.result == gTestRunnerState.expectedResult)
{
gTestRunnerState.passes++;
color = "\e[32m";
MgbaPrintf_(":N%s", gTestRunnerState.test->name);
}
else if (gTestRunnerState.result != TEST_RESULT_SKIP || gTestRunnerSkipIsFail)
{
gTestRunnerState.exitCode = 1;
color = "\e[31m";
}
else
{
color = "";
}
if (gTestRunnerState.result == TEST_RESULT_PASS
&& gTestRunnerState.result != gTestRunnerState.expectedResult)
{
MgbaPuts_("\e[31mPlease remove KNOWN_FAILING if this test intentionally PASSes\e[0m");
}
switch (gTestRunnerState.result)
{
case TEST_RESULT_FAIL: result = "FAIL"; break;
case TEST_RESULT_PASS: result = "PASS"; break;
case TEST_RESULT_SKIP: result = "SKIP"; break;
case TEST_RESULT_INVALID: result = "INVALID"; break;
case TEST_RESULT_ERROR: result = "ERROR"; break;
case TEST_RESULT_TIMEOUT: result = "TIMEOUT"; break;
default: result = "UNKNOWN"; break;
}
MgbaPrintf_(":R%s%s\e[0m", color, result);
}
break;
case STATE_EXIT:
MgbaExit_(gTestRunnerState.exitCode);
break;
}
}
void Test_ExpectedResult(enum TestResult result)
{
gTestRunnerState.expectedResult = result;
}
static void Assumptions_Run(void *data)
{
void (*function)(void) = data;
function();
}
const struct TestRunner gAssumptionsRunner =
{
.run = Assumptions_Run,
};
#define IRQ_LR (*(vu32 *)0x3007F9C)
/* Returns to AgbMainLoop.
* Similar to a longjmp except that we only restore sp (and cpsr via
* overwriting the value of lr_irq on the stack).
*
* WARNING: This could potentially be flaky because other global state
* will not be cleaned up, we may decide to Exit on a timeout instead. */
static NAKED void JumpToAgbMainLoop(void)
{
asm(".arm\n\
.word 0xe3104778\n\
ldr r0, =gAgbMainLoop_sp\n\
ldr sp, [r0]\n\
ldr r0, =AgbMainLoop\n\
bx r0\n\
.pool");
}
void ReinitCallbacks(void)
{
gMain.callback1 = NULL;
SetMainCallback2(CB2_TestRunner);
gMain.vblankCallback = NULL;
gMain.hblankCallback = NULL;
}
static void Intr_Timer2(void)
{
if (--gTestRunnerState.timeoutSeconds == 0)
{
if (gTestRunnerState.test->runner->checkProgress
&& gTestRunnerState.test->runner->checkProgress(gTestRunnerState.test->data))
{
gTestRunnerState.timeoutSeconds = TIMEOUT_SECONDS;
}
else
{
gTestRunnerState.result = TEST_RESULT_TIMEOUT;
ReinitCallbacks();
IRQ_LR = ((uintptr_t)JumpToAgbMainLoop & ~1) + 4;
}
}
}
void Test_ExitWithResult(enum TestResult result, const char *fmt, ...)
{
gTestRunnerState.result = result;
ReinitCallbacks();
if (gTestRunnerState.test->runner->handleExitWithResult
&& !gTestRunnerState.test->runner->handleExitWithResult(gTestRunnerState.test->data, result)
&& gTestRunnerState.result != gTestRunnerState.expectedResult)
{
va_list va;
va_start(va, fmt);
MgbaVPrintf_(fmt, va);
}
JumpToAgbMainLoop();
}
#define REG_DEBUG_ENABLE (*(vu16 *)0x4FFF780)
#define REG_DEBUG_FLAGS (*(vu16 *)0x4FFF700)
#define REG_DEBUG_STRING ((char *)0x4FFF600)
static bool32 MgbaOpen_(void)
{
REG_DEBUG_ENABLE = 0xC0DE;
return REG_DEBUG_ENABLE == 0x1DEA;
}
static void MgbaExit_(u8 exitCode)
{
register u32 _exitCode asm("r0") = exitCode;
asm("swi 0x3" :: "r" (_exitCode));
}
static s32 MgbaPuts_(const char *s)
{
return MgbaPrintf_("%s", s);
}
s32 MgbaPrintf_(const char *fmt, ...)
{
va_list va;
va_start(va, fmt);
return MgbaVPrintf_(fmt, va);
}
static s32 MgbaPutchar_(s32 i, s32 c)
{
REG_DEBUG_STRING[i++] = c;
if (i == 255)
{
REG_DEBUG_STRING[i] = '\0';
REG_DEBUG_FLAGS = MGBA_LOG_INFO | 0x100;
i = 0;
}
return i;
}
extern const u8 gWireless_RSEtoASCIITable[];
// Bare-bones, only supports plain %s, %S, and %d.
static s32 MgbaVPrintf_(const char *fmt, va_list va)
{
s32 i = 0;
s32 c, d;
const char *s;
while (*fmt)
{
switch ((c = *fmt++))
{
case '%':
switch (*fmt++)
{
case '%':
i = MgbaPutchar_(i, '%');
break;
case 'd':
d = va_arg(va, int);
if (d == 0)
{
i = MgbaPutchar_(i, '0');
}
else
{
char buffer[10];
s32 n = 0;
u32 u = abs(d);
if (d < 0)
i = MgbaPutchar_(i, '-');
while (u > 0)
{
buffer[n++] = '0' + (u % 10);
u /= 10;
}
while (n > 0)
i = MgbaPutchar_(i, buffer[--n]);
}
break;
case 'q':
d = va_arg(va, int);
{
char buffer[10];
s32 n = 0;
u32 u = abs(d) >> 12;
if (u == 0)
{
i = MgbaPutchar_(i, '0');
}
else
{
if (d < 0)
i = MgbaPutchar_(i, '-');
while (u > 0)
{
buffer[n++] = '0' + (u % 10);
u /= 10;
}
while (n > 0)
i = MgbaPutchar_(i, buffer[--n]);
}
n = 0;
i = MgbaPutchar_(i, '.');
u = d & 0xFFF;
while (TRUE)
{
u *= 10;
i = MgbaPutchar_(i, '0' + (u >> 12));
u &= 0xFFF;
if (u == 0)
break;
if (++n == 2)
{
u *= 10;
i = MgbaPutchar_(i, '0' + ((u + UQ_4_12_ROUND) >> 12));
break;
}
}
}
break;
case 's':
s = va_arg(va, const char *);
while ((c = *s++) != '\0')
i = MgbaPutchar_(i, c);
break;
case 'S':
s = va_arg(va, const u8 *);
while ((c = *s++) != EOS)
{
if ((c = gWireless_RSEtoASCIITable[c]) != '\0')
i = MgbaPutchar_(i, c);
else
i = MgbaPutchar_(i, '?');
}
break;
}
break;
case '\n':
i = 254;
i = MgbaPutchar_(i, '\0');
break;
default:
i = MgbaPutchar_(i, c);
break;
}
}
if (i != 0)
{
REG_DEBUG_FLAGS = MGBA_LOG_INFO | 0x100;
}
return i;
}

8
test/test_runner_args.c Normal file
View File

@ -0,0 +1,8 @@
#include "global.h"
// These values are patched by patchelf. Therefore we have put them in
// their own TU so that the optimizer cannot inline them.
const bool8 gTestRunnerEnabled = TRUE;
const u8 gTestRunnerN = 0;
const u8 gTestRunnerI = 0;
const char gTestRunnerArgv[256] = {'\0'};

1545
test/test_runner_battle.c Normal file

File diff suppressed because it is too large Load Diff

1
tools/mgba-rom-test-hydra/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
mgba-rom-test-hydra

View File

@ -0,0 +1,18 @@
.PHONY: all clean
SRCS = main.c
ifeq ($(OS),Windows_NT)
EXE := .exe
else
EXE :=
endif
all: mgba-rom-test-hydra$(EXE)
@:
mgba-rom-test-hydra$(EXE): $(SRCS)
$(CC) $(SRCS) -o $@ $(LDFLAGS)
clean:
$(RM) mgba-rom-test-hydra$(EXE)

View File

@ -0,0 +1,421 @@
/* mgba-rom-test-hydra. Runs multiple mgba-rom-test processes and
* parses the output to display human-readable progress.
*
* Output lines starting with "GBA Debug: :" are parsed as commands to
* Hydra, other output lines starting with "GBA Debug: " are parsed as
* output from the current test, and any other lines are parsed as
* output from the mgba-rom-test process itself.
*
* COMMANDS
* N: Sets the test name to the remainder of the line.
* R: Sets the result to the remainder of the line, and flushes any
* output buffered since the previous R. */
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAX_PROCESSES 32 // See also test/test.h
struct Runner
{
pid_t pid;
int outfd;
char rom_path[L_tmpnam];
char test_name[256];
size_t input_buffer_size;
size_t input_buffer_capacity;
char *input_buffer;
size_t output_buffer_size;
size_t output_buffer_capacity;
char *output_buffer;
};
static unsigned nrunners = 0;
static struct Runner *runners = NULL;
static void handle_read(struct Runner *runner)
{
char *sol = runner->input_buffer;
char *eol;
size_t consumed = 0;
size_t remaining = runner->input_buffer_size;
while ((eol = memchr(sol, '\n', remaining)))
{
eol++;
size_t n = eol - sol;
if (runner->input_buffer_size >= strlen("GBA Debug: ")
&& !strncmp(sol, "GBA Debug: ", strlen("GBA Debug: ")))
{
char *soc = sol + strlen("GBA Debug: ");
if (soc[0] == ':')
{
switch (soc[1])
{
case 'N':
soc += 2;
if (sizeof(runner->test_name) <= eol - soc - 1)
{
fprintf(stderr, "test_name too long\n");
exit(2);
}
strncpy(runner->test_name, soc, eol - soc - 1);
runner->test_name[eol - soc - 1] = '\0';
break;
case 'R':
soc += 2;
fprintf(stdout, "%s: ", runner->test_name);
fwrite(soc, 1, eol - soc, stdout);
fwrite(runner->output_buffer, 1, runner->output_buffer_size, stdout);
strcpy(runner->test_name, "WAITING...");
runner->output_buffer_size = 0;
break;
default:
goto buffer_output;
}
}
else
{
buffer_output:
if (runner->output_buffer_size + eol - soc >= runner->output_buffer_capacity)
{
runner->output_buffer_capacity *= 2;
if (runner->output_buffer_capacity < runner->output_buffer_size + eol - soc)
runner->output_buffer_capacity = runner->output_buffer_size + eol - soc;
runner->output_buffer = realloc(runner->output_buffer, runner->output_buffer_capacity);
if (!runner->output_buffer)
{
perror("realloc output_buffer failed");
exit(2);
}
}
memcpy(runner->output_buffer + runner->output_buffer_size, soc, eol - soc);
runner->output_buffer_size += eol - soc;
}
}
else
{
if (write(STDOUT_FILENO, sol, eol - sol) == -1)
{
perror("write failed");
exit(2);
}
}
sol += n;
consumed += n;
remaining -= n;
}
memcpy(runner->input_buffer, sol, remaining);
runner->input_buffer_size -= consumed;
if (runner->input_buffer_size == runner->input_buffer_capacity)
{
runner->input_buffer_capacity *= 2;
runner->input_buffer = realloc(runner->input_buffer, runner->input_buffer_capacity);
if (!runner->input_buffer)
{
perror("realloc input_buffer failed");
exit(2);
}
}
}
static void unlink_roms(void)
{
for (int i = 0; i < nrunners; i++)
{
if (runners[i].rom_path[0])
{
if (unlink(runners[i].rom_path) == -1)
perror("unlink rom_path failed");
}
}
}
static void exit2(int _)
{
exit(2);
}
int main(int argc, char *argv[])
{
if (argc < 3)
{
fprintf(stderr, "usage %s mgba-rom-test rom\n", argv[0]);
exit(2);
}
bool tty = isatty(STDOUT_FILENO);
if (!tty)
{
const char *v = getenv("MAKE_TERMOUT");
tty = v && v[0] == '\0';
}
if (tty)
{
char *stdout_buffer = malloc(64 * 1024);
if (!stdout_buffer)
{
perror("malloc stdout_buffer failed");
exit(2);
}
setvbuf(stdout, stdout_buffer, _IOFBF, 64 * 1024);
}
else
{
setvbuf(stdout, NULL, _IONBF, 0);
}
int elffd;
if ((elffd = open(argv[2], O_RDONLY)) == -1)
{
perror("open elffd failed");
exit(2);
}
struct stat elfst;
if (fstat(elffd, &elfst) == -1)
{
perror("stat elffd failed");
exit(2);
}
void *elf;
if ((elf = mmap(NULL, elfst.st_size, PROT_READ, MAP_PRIVATE, elffd, 0)) == MAP_FAILED)
{
perror("mmap elffd failed");
exit(2);
}
nrunners = sysconf(_SC_NPROCESSORS_ONLN);
if (nrunners > MAX_PROCESSES)
nrunners = MAX_PROCESSES;
runners = calloc(nrunners, sizeof(*runners));
if (!runners)
{
perror("calloc runners failed");
exit(2);
}
for (int i = 0; i < nrunners; i++)
{
runners[i].input_buffer_capacity = 4096;
runners[i].input_buffer = malloc(runners[i].input_buffer_capacity);
runners[i].output_buffer_capacity = 4096;
runners[i].output_buffer = malloc(runners[i].output_buffer_capacity);
strcpy(runners[i].test_name, "WAITING...");
if (tty)
fprintf(stdout, "%s\n", runners[i].test_name);
}
fflush(stdout);
atexit(unlink_roms);
signal(SIGINT, exit2);
signal(SIGTERM, exit2);
// Start test runners.
pid_t parent_pid = getpid();
for (int i = 0; i < nrunners; i++)
{
int pipefds[2];
if (pipe(pipefds) == -1)
{
perror("pipe failed");
exit(2);
}
if (!tmpnam(runners[i].rom_path))
{
perror("tmpnam rom_path failed");
exit(2);
}
int tmpfd;
if ((tmpfd = open(runners[i].rom_path, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR)) == -1)
{
perror("open tmpfd failed");
_exit(2);
}
if ((write(tmpfd, elf, elfst.st_size)) == -1)
{
perror("write tmpfd failed");
_exit(2);
}
pid_t patchelfpid = fork();
if (patchelfpid == -1)
{
perror("fork patchelf failed");
_exit(2);
}
else if (patchelfpid == 0)
{
char n_arg[5], i_arg[5];
snprintf(n_arg, sizeof(n_arg), "\\x%02x", nrunners);
snprintf(i_arg, sizeof(i_arg), "\\x%02x", i);
if (execlp("tools/patchelf/patchelf", "tools/patchelf/patchelf", runners[i].rom_path, "gTestRunnerN", n_arg, "gTestRunnerI", i_arg, NULL) == -1)
{
perror("execlp patchelf failed");
_exit(2);
}
}
else
{
int wstatus;
if (waitpid(patchelfpid, &wstatus, 0) == -1)
{
perror("waitpid patchelfpid failed");
_exit(2);
}
if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0)
{
fprintf(stderr, "patchelf exited with an error\n");
_exit(2);
}
}
pid_t pid = fork();
if (pid == -1) {
perror("fork mgba-rom-test failed");
exit(2);
} else if (pid == 0) {
if (prctl(PR_SET_PDEATHSIG, SIGTERM) == -1)
{
perror("prctl failed");
_exit(2);
}
if (getppid() != parent_pid) // Parent died.
{
_exit(2);
}
if (close(pipefds[0]) == -1)
{
perror("close pipefds[0] failed");
_exit(2);
}
if (dup2(pipefds[1], STDOUT_FILENO) == -1)
{
perror("dup2 stdout failed");
_exit(2);
}
if (close(pipefds[1]) == -1)
{
perror("close pipefds[1] failed");
_exit(2);
}
// stdbuf is required because otherwise mgba never flushes
// stdout.
if (execlp("stdbuf", "stdbuf", "-oL", argv[1], "-l15", "-ClogLevel.gba.dma=16", "-Rr0", runners[i].rom_path, NULL) == -1)
{
perror("execl stdbuf mgba-rom-test failed");
_exit(2);
}
} else {
runners[i].pid = pid;
runners[i].outfd = pipefds[0];
if (close(pipefds[1]) == -1)
{
perror("close pipefds[1] failed");
exit(2);
}
}
}
// Process test runner output.
int openfds = nrunners;
struct pollfd *pollfds = calloc(nrunners, sizeof(*pollfds));
if (!pollfds)
{
perror("calloc pollfds failed");
exit(2);
}
for (int i = 0; i < nrunners; i++)
{
pollfds[i].fd = runners[i].outfd;
pollfds[i].events = POLLIN;
}
while (openfds > 0)
{
if (tty)
{
struct winsize winsize;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize) == -1)
{
perror("ioctl TIOCGWINSZ failed");
exit(2);
}
int scrollback = 0;
for (int i = 0; i < nrunners; i++)
{
if (runners[i].outfd >= 0)
scrollback += (strlen(runners[i].test_name) + winsize.ws_col - 1) / winsize.ws_col;
}
if (scrollback > 0)
fprintf(stdout, "\e[%dF\e[J", scrollback);
}
if (poll(pollfds, nrunners, -1) == -1)
{
perror("poll failed");
exit(2);
}
for (int i = 0; i < nrunners; i++)
{
if (pollfds[i].revents & POLLIN)
{
int n;
if ((n = read(pollfds[i].fd, runners[i].input_buffer + runners[i].input_buffer_size, runners[i].input_buffer_capacity - runners[i].input_buffer_size)) == -1)
{
perror("read pollfds[i] failed");
exit(2);
}
runners[i].input_buffer_size += n;
handle_read(&runners[i]);
}
if (pollfds[i].revents & (POLLERR | POLLHUP))
{
if (close(pollfds[i].fd) == -1)
{
perror("close pollfds[i] failed");
exit(2);
}
runners[i].outfd = pollfds[i].fd = -pollfds[i].fd;
openfds--;
}
}
if (tty)
{
for (int i = 0; i < nrunners; i++)
{
if (runners[i].outfd >= 0)
fprintf(stdout, "%s\n", runners[i].test_name);
}
fflush(stdout);
}
}
// Reap test runners and collate exit codes.
int exit_code = 0;
for (int i = 0; i < nrunners; i++)
{
int wstatus;
if (waitpid(runners[i].pid, &wstatus, 0) == -1)
{
perror("waitpid runners[i] failed");
exit(2);
}
if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) > exit_code)
exit_code = WEXITSTATUS(wstatus);
}
return exit_code;
}

7
tools/mgba/README.md Normal file
View File

@ -0,0 +1,7 @@
# mGBA
The binaries in this folder are built from `mGBA`, an emulator for running Game Boy Advance games. The source code is available here: <https://github.com/mgba-emu/mgba>.
The source code for these specific builds is available from:
- Windows: <https://github.com/mgba-emu/mgba/tree/7ee2be6c96222dca12a9a579b747fe5ff1829def>
- Linux: <https://github.com/mgba-emu/mgba/tree/dbffb46c4e7d2e7a2cbed7c3488cece4c2176d4c>

BIN
tools/mgba/mgba-rom-test Executable file

Binary file not shown.

BIN
tools/mgba/mgba-rom-test.exe Executable file

Binary file not shown.

1
tools/patchelf/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
patchelf

18
tools/patchelf/Makefile Normal file
View File

@ -0,0 +1,18 @@
.PHONY: all clean
SRCS = patchelf.c
ifeq ($(OS),Windows_NT)
EXE := .exe
else
EXE :=
endif
all: patchelf$(EXE)
@:
patchelf$(EXE): $(SRCS)
$(CC) $(SRCS) -o $@ $(LDFLAGS)
clean:
$(RM) patchelf$(EXE)

191
tools/patchelf/patchelf.c Normal file
View File

@ -0,0 +1,191 @@
#include <ctype.h>
#include <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
static bool try_patch_value(const char *sym, char *dest, const char *source, size_t size);
int main(int argc, char *argv[])
{
int exit_code = 1;
int fd = -1;
char *f = MAP_FAILED;
if (argc < 2 || argc % 2 != 0)
{
fprintf(stderr, "Usage: %s <filename> [<symbol> <value>]...\n", argv[0]);
goto error;
}
if ((fd = open(argv[1], O_RDWR)) == -1)
{
fprintf(stderr, "open(%s, O_RDWR) failed: %s\n", argv[1], strerror(errno));
goto error;
}
struct stat st;
if (fstat(fd, &st) == -1)
{
perror("stat failed");
goto error;
}
if ((f = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED)
{
perror("mmap failed");
goto error;
}
if (memcmp(f, ELFMAG, 4) != 0)
{
fprintf(stderr, "not an ELF file\n");
goto error;
}
const Elf32_Ehdr *ehdr = (Elf32_Ehdr *)f;
const Elf32_Shdr *shdrs = (Elf32_Shdr *)(f + ehdr->e_shoff);
if (ehdr->e_shstrndx == SHN_UNDEF)
{
fprintf(stderr, "no section name string table\n");
goto error;
}
const Elf32_Shdr *shdr_shstr = &shdrs[ehdr->e_shstrndx];
const char *shstr = (const char *)(f + shdr_shstr->sh_offset);
const Elf32_Shdr *shdr_symtab = NULL;
const Elf32_Shdr *shdr_strtab = NULL;
for (int i = 0; i < ehdr->e_shnum; i++)
{
const char *sh_name = shstr + shdrs[i].sh_name;
if (strcmp(sh_name, ".symtab") == 0)
shdr_symtab = &shdrs[i];
else if (strcmp(sh_name, ".strtab") == 0)
shdr_strtab = &shdrs[i];
}
if (!shdr_symtab)
{
fprintf(stderr, "no .symtab section\n");
goto error;
}
if (!shdr_strtab)
{
fprintf(stderr, "no .strtab section\n");
goto error;
}
const Elf32_Sym *symtab = (Elf32_Sym *)(f + shdr_symtab->sh_offset);
const char *strtab = (const char *)(f + shdr_strtab->sh_offset);
for (int i = 0; i < shdr_symtab->sh_size / shdr_symtab->sh_entsize; i++)
{
if (symtab[i].st_name == 0) continue;
if (symtab[i].st_shndx > ehdr->e_shnum) continue;
const char *st_name = strtab + symtab[i].st_name;
const Elf32_Shdr *shdr = &shdrs[symtab[i].st_shndx];
uint32_t sym_offset = symtab[i].st_value - shdr->sh_addr;
for (int j = 2; j < argc; j += 2)
{
const char *arg_name = argv[j + 0];
const char *arg_value = argv[j + 1];
if (strcmp(st_name, arg_name) == 0)
{
char *value = (char *)(f + shdr->sh_offset + sym_offset);
if (!try_patch_value(st_name, value, arg_value, symtab[i].st_size))
goto error;
}
}
}
exit_code = 0;
error:
if (f != MAP_FAILED)
{
if (msync(f, st.st_size, MS_SYNC) == -1)
{
perror("msync failed");
f = MAP_FAILED;
}
}
if (f != MAP_FAILED)
{
if (munmap(f, st.st_size) == -1)
{
perror("munmap failed");
}
}
if (fd != -1)
{
if (close(fd) == -1)
{
perror("close failed");
}
}
return exit_code;
}
static int parsexdigit(char c)
{
if ('0' <= c && c <= '9')
return c - '0';
else if ('a' <= c && c <= 'f')
return c - 'a' + 10;
else if ('A' <= c && c <= 'F')
return c - 'A' + 10;
else
return -1;
}
static bool try_patch_value(const char *sym, char *dest, const char *source, size_t size)
{
int i = 0;
while (*source)
{
if (i == size)
{
fprintf(stderr, "%s: overflows size (%lu)\n", sym, size);
return false;
}
char c, value;
switch ((c = *source++))
{
case '\\':
switch ((c = *source++))
{
case '0':
value = 0;
break;
case 'x':
if (!isxdigit((c = *source++)))
{
fprintf(stderr, "%s: illegal escape \\x%c\n", sym, c);
return false;
}
value = parsexdigit(c);
if (!isxdigit((c = *source++)))
{
fprintf(stderr, "%s: illegal escape \\x%c%c\n", sym, *(source - 2), c);
return false;
}
value = value * 16 + parsexdigit(c);
break;
default:
fprintf(stderr, "%s: illegal escape \\%c\n", sym, c);
return false;
}
break;
default:
value = c;
break;
}
dest[i++] = value;
}
return true;
}