diff --git a/tests/server/test_elemental_reaction.py b/tests/server/test_elemental_reaction.py new file mode 100644 index 00000000..bcb87bde --- /dev/null +++ b/tests/server/test_elemental_reaction.py @@ -0,0 +1,223 @@ +from agents.interaction_agent import InteractionAgent +from server.match import Match, MatchState +from server.deck import Deck +from tests.utils_for_test import ( + set_16_omni, check_hp, check_name, make_respond +) + + +def test_crystallize(): + """ + agent 0: 1 elec to p1c2, then nothing until r3, and c2 will be defeated, + choose c0. in r3, 3 geo to p1c2 + agent 1: in r1, 1 elec to p1c2, 1 geo to p1c2, 1 elec to p1c2, 1 geo to + p1c2, in r2, 1 geo to p1c2, 1 elec to p1c2, 1 geo to p1c2, p0c2 + defeated, switch to p1c2, end. + result: agent 1 get two crystallize, and agent 0 attack 3 + 1 - 2 = 2hp. + """ + agent_0 = InteractionAgent( + player_id = 0, + verbose_level = 0, + commands = [ + 'sw_card', + 'choose 2', + # 'reroll', + 'skill 0 omni omni omni', + 'end', + 'end', + 'choose 0', + 'skill 1 omni omni omni', + 'end', + ], + random_after_no_command = True + ) + agent_1 = InteractionAgent( + player_id = 1, + verbose_level = 0, + commands = [ + 'sw_card', + 'choose 2', + # 'reroll', + 'skill 0 omni omni omni', + 'sw_char 0 omni', + 'skill 0 omni omni omni', + 'sw_char 2 omni', + 'skill 0 omni omni omni', + 'sw_char 0 omni', + 'skill 0 omni omni omni', + 'end', + 'skill 0 omni omni omni', + 'sw_char 2 omni', + 'skill 0 omni omni omni', + 'sw_char 0 omni', + 'skill 0 omni omni omni', + 'sw_char 2 omni', + 'end', + 'end', + ], + random_after_no_command = True + ) + match = Match() + # match.enable_history = True + deck = { + 'name': 'Deck', + 'charactors': [ + { + 'name': 'GeoMobMage', + 'element': 'GEO', + }, + { + 'name': 'GeoMobMage', + 'element': 'GEO', + }, + { + 'name': 'ElectroMobMage', + 'element': 'ELECTRO', + }, + ], + 'cards': [ + { + 'name': 'Strategize', + } + ] * 30, + } + deck = Deck(**deck) + match.set_deck([deck, deck]) + match.match_config.max_same_card_number = 30 + set_16_omni(match) + assert match.start() + match.step() + + while True: + if match.need_respond(0): + make_respond(agent_0, match) + elif match.need_respond(1): + make_respond(agent_1, match) + if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: + break + + assert len(agent_0.commands) == 0 + assert len(agent_1.commands) == 0 + assert match.round_number == 4 + check_hp(match, [[10, 10, 0], [10, 10, 7]]) + + assert match.match_state != MatchState.ERROR + + +def test_frozen(): + """ + agent 0 goes first, sw 2, 1 cryo to p1c2, sw 0, 1 hydro to p1c2, sw 1, + 2 physical to p1c2, sw2, end, sw0, sw2, (cannot skill), end, + (can skill) 1 cryo to p0c2. + agent 1 sw0, sw2, (cannot skill), end, (can skill) 3 cryo to p0c2, sw 0, + 1 hydro to p0c2, end. + """ + agent_0 = InteractionAgent( + player_id = 0, + verbose_level = 0, + commands = [ + 'sw_card', + 'choose 2', + 'skill 0 omni omni omni', + 'sw_char 0 omni', + 'skill 0 omni omni omni', + 'sw_char 1 omni', + 'skill 0 omni omni omni', + 'sw_char 2 omni', + 'end', + 'sw_char 0 omni', + 'sw_char 2 omni', + 'end', + 'skill 0 omni omni omni', + ], + random_after_no_command = True + ) + agent_1 = InteractionAgent( + player_id = 1, + verbose_level = 0, + commands = [ + 'sw_card', + 'choose 2', + 'sw_char 0 omni', + 'sw_char 2 omni', + 'end', + 'skill 1 omni omni omni', + 'sw_char 0 omni', + 'skill 0 omni omni omni', + 'end', + ], + random_after_no_command = True + ) + match = Match() + # match.enable_history = True + deck = { + 'name': 'Deck', + 'charactors': [ + { + 'name': 'HydroMobMage', + 'element': 'HYDRO', + }, + { + 'name': 'HydroMob', + 'element': 'HYDRO', + }, + { + 'name': 'CryoMobMage', + 'element': 'CRYO', + }, + ], + 'cards': [ + { + 'name': 'Strategize', + } + ] * 30, + } + deck = Deck(**deck) + match.set_deck([deck, deck]) + match.match_config.max_same_card_number = 30 + match.match_config.random_first_player = False + set_16_omni(match) + match.enable_history = True + assert match.start() + match.step() + + while True: + if match.need_respond(0): + if len(agent_0.commands) == 2: + # p0c2 should be frozen, and cannot use skill + check_name('Frozen', + match.player_tables[0].charactors[2].status) + check_name('UseSkillRequest', match.requests, exist = False) + elif len(agent_0.commands) == 1: + # now not frozen and can use skill + check_name('Frozen', + match.player_tables[0].charactors[2].status, + exist = False) + check_name('UseSkillRequest', match.requests, exist = True) + make_respond(agent_0, match) + elif match.need_respond(1): + if len(agent_1.commands) == 5: + # p1c2 should be frozen, and cannot use skill + check_name('Frozen', + match.player_tables[1].charactors[2].status) + check_name('UseSkillRequest', match.requests, exist = False) + elif len(agent_1.commands) == 4: + # now not frozen and can use skill + check_name('Frozen', + match.player_tables[1].charactors[2].status, + exist = False) + check_name('UseSkillRequest', match.requests, exist = True) + make_respond(agent_1, match) + if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: + break + + assert len(agent_0.commands) == 0 + assert len(agent_1.commands) == 0 + assert match.round_number == 3 + check_hp(match, [[10, 10, 5], [9, 10, 3]]) + + assert match.match_state != MatchState.ERROR + + +if __name__ == '__main__': + test_frozen() diff --git a/tests/server/test_pipeline.py b/tests/server/test_pipeline.py new file mode 100644 index 00000000..df36e0a9 --- /dev/null +++ b/tests/server/test_pipeline.py @@ -0,0 +1,227 @@ +import time +import json +from agents.nothing_agent import NothingAgent +from server.match import Match, MatchState +from server.deck import Deck +from agents.random_agent import RandomAgent +from agents.interaction_agent import InteractionAgent +from tests.utils_for_test import ( + set_16_omni, make_respond, + remove_ids +) + + +TEST_DECK = { + 'name': 'Deck', + 'charactors': [ + { + 'name': 'DendroMobMage', + 'element': 'DENDRO', + 'hp': 99, + 'max_hp': 99, + }, + { + 'name': 'DendroMobMage', + 'element': 'DENDRO', + }, + { + 'name': 'ElectroMobMage', + 'element': 'ELECTRO', + }, + ], + 'cards': [ + { + 'name': 'Strategize', + } + ] * 30, +} + + +def test_match_pipeline(): + agent_0 = NothingAgent(player_id = 0) + agent_1 = RandomAgent(player_id = 1) + match = Match() + deck = TEST_DECK + deck = Deck(**deck) + match.set_deck([deck, deck]) + match.match_config.max_same_card_number = 30 + set_16_omni(match) + # main.match.event_handlers[0].version = '3.3' + assert match.start() + match.step() # switch card + + while match.round_number < 100 and not match.is_game_end(): + make_respond(agent_0, match, assertion = False) + make_respond(agent_1, match, assertion = False) + + assert match.match_state != MatchState.ERROR + + +def test_save_load(): + """ + use v3.3 Catalyzing Field to test save & load. + when save on field generated, load should be same. otherwise, load should + be different only on id. + """ + agent_0 = NothingAgent(player_id = 0) + agent_1 = InteractionAgent( + player_id = 1, + verbose_level = 0, + commands = [ + 'sw_card', + 'choose 2', + # 'reroll', + 'skill 0 omni omni omni', + 'sw_char 0 omni', + 'skill 0 omni omni omni', # quiken activated + 'skill 0 omni omni omni', + 'sw_char 2 omni', + 'skill 0 omni omni omni', # quiken activated, renew in v3.3 + 'end', + 'skill 0 omni omni omni', + 'skill 0 omni omni omni', + 'skill 0 omni omni omni', # run out of field + ], + random_after_no_command = True + ) + match = Match() + deck = TEST_DECK + deck = Deck(**deck) + match.set_deck([deck, deck]) + match.match_config.max_same_card_number = 30 + set_16_omni(match) + match.event_handlers[0].version = '3.3' + assert match.start() + match.step() # switch card + + saves = {} + agent_saves = {} + + while True: + make_respond(agent_0, match, assertion = False) + make_respond(agent_1, match, assertion = False) + if len(agent_1.commands) not in saves: + length = len(agent_1.commands) + saves[length] = match.copy(deep = True) + agent_saves[length] = agent_1.copy(deep = True) + if len(agent_1.commands) == 0: + break + + def check_range(saves, agent_saves, start, end, + same = True, ignore_id = False): + """ + load start and run until end, then check if they are same. + if ignore_id, before compare will remove ids from objects. + """ + match = saves[start].copy(deep = True) + agent_1 = agent_saves[start].copy(deep = True) + while True: + make_respond(agent_0, match, assertion = False) + make_respond(agent_1, match, assertion = False) + if len(agent_1.commands) == end: + break + saves_end = saves[end] + if ignore_id: + match = match.copy(deep = True) + saves_end = saves_end.copy(deep = True) + remove_ids(match) + remove_ids(saves_end) + is_same = match == saves_end + assert is_same == same + + # from any position, run reuslts should same + for _ in range(1, 11): + check_range(saves, agent_saves, _, 0, same = True, ignore_id = True) + # generate status, id should not change + check_range(saves, agent_saves, 8, 7, same = False, ignore_id = False) + # ignore id, should same + check_range(saves, agent_saves, 8, 7, same = True, ignore_id = True) + # renew status, id should not change + check_range(saves, agent_saves, 6, 4, same = True, ignore_id = False) + + assert len(agent_1.commands) == 0 + + assert match.match_state != MatchState.ERROR + + +def test_copy_speed(): + run_time = 100 + match = Match() + deck = TEST_DECK + deck = Deck(**deck) + match.set_deck([deck, deck]) + match.match_config.max_same_card_number = 30 + assert match.start() + starttime = time.time() + for i in range(run_time): + _ = match.copy(deep = True) + endtime = time.time() + print('not json speed', (endtime - starttime) / run_time) + + match = Match() + match.match_config.max_same_card_number = 30 + match.set_deck([deck, deck]) + mainjson = match.json() + starttime = time.time() + assert match.start() + for i in range(run_time): + mainjson = match.json() + _ = Match(**json.loads(mainjson)) + endtime = time.time() + print('json speed', (endtime - starttime) / run_time) + assert True + + +def test_random_same_after_load(): + agent_0 = NothingAgent(player_id = 0) + agent_1 = RandomAgent(player_id = 1, random_seed = 19260817) + match = Match() + deck = TEST_DECK + deck = Deck(**deck) + match.set_deck([deck, deck]) + match.match_config.max_same_card_number = 30 + initial_match = match.copy(deep = True) + initial_agent_1 = agent_1.copy(deep = True) + results_1 = [] + test_step = 100 + assert match.start() + match.step() + for _ in range(test_step): + make_respond(agent_0, match, assertion = False) + make_respond(agent_1, match, assertion = False) + results_1.append(match.copy(deep = True)) + assert match.match_state != MatchState.ERROR + + # use deepcopy to rerun + match = initial_match.copy(deep = True) + agent_1 = initial_agent_1.copy(deep = True) + assert match.start() + match.step() + for i in range(test_step): + make_respond(agent_0, match, assertion = False) + make_respond(agent_1, match, assertion = False) + remove_ids(results_1[i]) + now = match.copy(deep = True) + remove_ids(now) + assert now == results_1[i] + assert match.match_state != MatchState.ERROR + + # use json to rerun + match = Match(**json.loads(initial_match.json())) + agent_1 = RandomAgent(**json.loads(initial_agent_1.json())) + assert match.start() + match.step() + for i in range(test_step): + make_respond(agent_0, match, assertion = False) + make_respond(agent_1, match, assertion = False) + remove_ids(results_1[i]) + now = match.copy(deep = True) + remove_ids(now) + assert now == results_1[i] + assert match.match_state != MatchState.ERROR + + +if __name__ == '__main__': + test_match_pipeline() + test_save_load() + test_random_same_after_load() diff --git a/tests/utils_for_test.py b/tests/utils_for_test.py new file mode 100644 index 00000000..e8534431 --- /dev/null +++ b/tests/utils_for_test.py @@ -0,0 +1,63 @@ +from typing import List, Any +from server.match import Match +from server.event_handler import OmnipotentGuideEventHandler +from agents.agent_base import AgentBase +from utils import BaseModel + + +def set_16_omni(match: Match): + match.match_config.initial_dice_number = 16 + match.event_handlers.append(OmnipotentGuideEventHandler()) + + +def make_respond(agent: AgentBase, match: Match, assertion: bool = True): + """ + Make response once, and step the match until new request or + game end, regardless of the target of requests. + If not requesting this agent and nees assetion, raise AssertionError. + Otherwise, do nothing. + """ + resp = agent.generate_response(match) + if assertion: + assert resp is not None + if resp is None: + return + match.respond(resp) + match.step() + + +def check_hp(match: Match, hp: List[List[int]]): + """ + input hps for each player and check + """ + assert len(match.player_tables) == len(hp) + for h, t in zip(hp, match.player_tables): + c = t.charactors + assert len(c) == len(h) + for onec, oneh in zip(c, h): + assert onec.hp == oneh + + +def check_name(name: str, lists: List[Any], exist: bool = True): + """ + check the existence of name in lists + """ + real_exist = name in [x.name for x in lists] + assert real_exist == exist + + +def remove_ids(model: BaseModel) -> BaseModel: + """ + remove ids of all objects in-place. + """ + for key in model.__fields__.keys(): + value = getattr(model, key) + if isinstance(value, BaseModel): + remove_ids(value) + elif isinstance(value, list) or isinstance(value, tuple): + for v in value: + if isinstance(v, BaseModel): + remove_ids(v) + if 'id' in model.__fields__.keys(): + model.id = 0 + return model