├── .coveragerc ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── build-test.yml │ ├── pre-commit.yaml │ ├── publish.yml │ ├── pytest.yml │ └── remove-closed-issue-label.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.en.md ├── README.md ├── docs └── images │ └── wechat.png ├── http_log_replay.py ├── http_room_serve.py ├── http_test_serve.py ├── ipynb ├── get_status_icon_type.ipynb ├── message.json ├── pack_default_patch.ipynb ├── parse_name_descs.ipynb ├── parse_name_type.ipynb └── result.json ├── main.py ├── pyproject.toml ├── pytest.ini ├── src └── lpsim │ ├── __init__.py │ ├── agents │ ├── __init__.py │ ├── agent_base.py │ ├── interaction_agent.py │ ├── nothing_agent.py │ └── random_agent.py │ ├── consts.py │ ├── network │ ├── __init__.py │ ├── __version__.py │ ├── http_room_server.py │ ├── http_server.py │ └── utils.py │ ├── resources │ └── consts.py │ ├── responses.py │ ├── server │ ├── __init__.py │ ├── action.py │ ├── card │ │ ├── __init__.py │ │ ├── equipment │ │ │ ├── __init__.py │ │ │ ├── artifact │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── element_artifacts.py │ │ │ │ ├── emblem_of_severed_fate.py │ │ │ │ ├── exile.py │ │ │ │ ├── gamblers.py │ │ │ │ ├── gilded_dreams.py │ │ │ │ ├── heal_by_skill.py │ │ │ │ ├── instructors_cap.py │ │ │ │ ├── millelith.py │ │ │ │ ├── ocean_hued.py │ │ │ │ └── vermillion_shimenawa.py │ │ │ └── weapon │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── favonius.py │ │ │ │ ├── other_bow.py │ │ │ │ ├── other_catalyst.py │ │ │ │ ├── other_claymore.py │ │ │ │ ├── other_polearm.py │ │ │ │ ├── other_sword.py │ │ │ │ ├── sacrificial.py │ │ │ │ ├── skyward.py │ │ │ │ ├── sumeru_forge_weapon.py │ │ │ │ └── vanilla.py │ │ ├── event │ │ │ ├── __init__.py │ │ │ ├── arcane_legend.py │ │ │ ├── foods.py │ │ │ ├── others.py │ │ │ └── resonance.py │ │ └── support │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── companions.py │ │ │ ├── items.py │ │ │ └── locations.py │ ├── character │ │ ├── __init__.py │ │ ├── anemo │ │ │ ├── __init__.py │ │ │ ├── jean_3_3.py │ │ │ ├── jean_4_2.py │ │ │ ├── kaedehara_kazuha_3_8.py │ │ │ ├── maguu_kenki_3_3.py │ │ │ ├── maguu_kenki_3_4.py │ │ │ ├── sucrose_3_3.py │ │ │ ├── venti_3_7.py │ │ │ ├── wanderer_4_1.py │ │ │ └── xiao_3_7.py │ │ ├── character_base.py │ │ ├── cryo │ │ │ ├── __init__.py │ │ │ ├── chongyun_3_3.py │ │ │ ├── diona_3_3.py │ │ │ ├── eula_3_5.py │ │ │ ├── eula_3_8.py │ │ │ ├── fatui_cryo_cicin_mage_3_7.py │ │ │ ├── fatui_cryo_cicin_mage_4_1.py │ │ │ ├── ganyu_3_3.py │ │ │ ├── ganyu_3_7.py │ │ │ ├── kaeya_3_3.py │ │ │ ├── kamisato_ayaka_3_3.py │ │ │ ├── qiqi_4_0.py │ │ │ ├── shenhe_3_7.py │ │ │ └── shenhe_4_2.py │ │ ├── custom │ │ │ ├── __init__.py │ │ │ ├── mob_1_0.py │ │ │ ├── mob_mage_1_0.py │ │ │ └── physical_mob_1_0.py │ │ ├── dendro │ │ │ ├── __init__.py │ │ │ ├── baizhu_4_2.py │ │ │ ├── collei_3_3.py │ │ │ ├── jadeplume_terrorshroom_3_3.py │ │ │ ├── nahida_3_7.py │ │ │ ├── tighnari_3_6.py │ │ │ └── yaoyao_4_1.py │ │ ├── electro │ │ │ ├── __init__.py │ │ │ ├── beidou_3_4.py │ │ │ ├── beidou_3_8.py │ │ │ ├── cyno_3_3.py │ │ │ ├── dori_4_2.py │ │ │ ├── electro_hypostasis_3_7.py │ │ │ ├── fischl_3_3.py │ │ │ ├── keqing_3_3.py │ │ │ ├── kujou_sara_3_5.py │ │ │ ├── lisa_4_0.py │ │ │ ├── raiden_shogun_3_7.py │ │ │ ├── razor_3_3.py │ │ │ ├── razor_3_8.py │ │ │ └── yae_miko_3_7.py │ │ ├── geo │ │ │ ├── __init__.py │ │ │ ├── albedo_4_0.py │ │ │ ├── arataki_itto_3_6.py │ │ │ ├── arataki_itto_4_2.py │ │ │ ├── ningguang_3_3.py │ │ │ ├── noelle_3_3.py │ │ │ ├── stonehide_lawachurl_3_3.py │ │ │ └── zhongli_3_7.py │ │ ├── hydro │ │ │ ├── __init__.py │ │ │ ├── barbara_3_3.py │ │ │ ├── candace_3_8.py │ │ │ ├── kamisato_ayato_3_6.py │ │ │ ├── kamisato_ayato_4_1.py │ │ │ ├── mirror_maiden_3_3.py │ │ │ ├── mirror_maiden_3_7.py │ │ │ ├── mona_3_3.py │ │ │ ├── nilou_4_2.py │ │ │ ├── rhodeia_3_3.py │ │ │ ├── rhodeia_4_2.py │ │ │ ├── sangonomiya_kokomi_3_5.py │ │ │ ├── sangonomiya_kokomi_3_6.py │ │ │ ├── tartaglia_3_7.py │ │ │ ├── tartaglia_4_1.py │ │ │ ├── xingqiu_3_3.py │ │ │ ├── xingqiu_3_6.py │ │ │ └── xingqiu_4_1.py │ │ └── pyro │ │ │ ├── __init__.py │ │ │ ├── abyss_lector_fathomless_flames_3_7.py │ │ │ ├── amber_3_7.py │ │ │ ├── bennett_3_3.py │ │ │ ├── dehya_4_1.py │ │ │ ├── diluc_3_3.py │ │ │ ├── fatui_pyro_agent_3_3.py │ │ │ ├── hu_tao_3_7.py │ │ │ ├── klee_3_4.py │ │ │ ├── xiangling_3_3.py │ │ │ ├── xiangling_3_8.py │ │ │ ├── yanfei_3_8.py │ │ │ ├── yanfei_4_2.py │ │ │ ├── yoimiya_3_3.py │ │ │ ├── yoimiya_3_4.py │ │ │ └── yoimiya_3_8.py │ ├── consts.py │ ├── deck.py │ ├── dice.py │ ├── elemental_reaction.py │ ├── event.py │ ├── event_controller.py │ ├── event_handler.py │ ├── interaction.py │ ├── match.py │ ├── modifiable_values.py │ ├── object_base.py │ ├── patch │ │ ├── __init__.py │ │ ├── v43 │ │ │ ├── __init__.py │ │ │ ├── balance │ │ │ │ ├── __init__.py │ │ │ │ ├── fatui_pyro_agent_4_3.py │ │ │ │ ├── joyous_celebration_4_3.py │ │ │ │ ├── ocean_huled_4_3.py │ │ │ │ ├── rhodeia_of_loch_4_3.py │ │ │ │ ├── stone_and_contracts_4_3.py │ │ │ │ ├── timaus_wagner_4_3.py │ │ │ │ └── wind_and_freedom_4_3.py │ │ │ ├── cards │ │ │ │ ├── __init__.py │ │ │ │ ├── echoes_of_an_offering_4_3.py │ │ │ │ ├── falls_and_fortune_4_3.py │ │ │ │ ├── fish_and_chips_4_3.py │ │ │ │ ├── flickering_four_leaf_sigil_4_3.py │ │ │ │ ├── gilded_dreams_4_3.py │ │ │ │ ├── mamere_4_3.py │ │ │ │ ├── memento_lens_4_3.py │ │ │ │ ├── new_weapons_4_3.py │ │ │ │ ├── opera_epiclese_4_3.py │ │ │ │ ├── passing_of_judgment_4_3.py │ │ │ │ ├── seed_dispensary_4_3.py │ │ │ │ ├── the_boar_princess_4_3.py │ │ │ │ ├── vourukashas_glow_4_3.py │ │ │ │ └── weeping_willow_4_3.py │ │ │ └── characters │ │ │ │ ├── __init__.py │ │ │ │ ├── alhaitham_4_3.py │ │ │ │ ├── azhdaha_4_3.py │ │ │ │ ├── dvalin_4_3.py │ │ │ │ ├── eremite_scorching_loremaster_4_3.py │ │ │ │ ├── gorou_4_3.py │ │ │ │ ├── layla_4_3.py │ │ │ │ ├── lynette_4_3.py │ │ │ │ ├── lyney_4_3.py │ │ │ │ ├── signora_4_3.py │ │ │ │ ├── thunder_manifestation_4_3.py │ │ │ │ └── yelan_4_3.py │ │ ├── v44 │ │ │ ├── __init__.py │ │ │ ├── balance │ │ │ │ ├── __init__.py │ │ │ │ ├── in_every_house_a_stove_4_4.py │ │ │ │ ├── thunder_manifestation_4_4.py │ │ │ │ └── vourukashas_glow_4_4.py │ │ │ ├── cards │ │ │ │ ├── __init__.py │ │ │ │ ├── jeht_4_4.py │ │ │ │ ├── machine_assembly_line_4_4.py │ │ │ │ ├── matsutake_meat_rools_4_4.py │ │ │ │ ├── sapwood_blade_4_4.py │ │ │ │ ├── silver_and_melus_4_4.py │ │ │ │ ├── sunyata_flower_4_4.py │ │ │ │ └── veterans_visage_4_4.py │ │ │ └── characters │ │ │ │ ├── __init__.py │ │ │ │ ├── cryo_hypostasis_4_4.py │ │ │ │ ├── millennial_pearl_seahorse_4_4.py │ │ │ │ ├── sayu_4_4.py │ │ │ │ └── thoma_4_4.py │ │ └── v45 │ │ │ ├── __init__.py │ │ │ ├── balance │ │ │ ├── __init__.py │ │ │ ├── gilded_dreams_4_5.py │ │ │ ├── jade_chamber_4_5.py │ │ │ └── knights_of_favonius_library_4_5.py │ │ │ ├── cards │ │ │ ├── __init__.py │ │ │ ├── controlled_directional_blast_4_5.py │ │ │ ├── day_of_resistance_moment_of_hattered_dreams_4_5.py │ │ │ ├── fortress_of_meropide_4_5.py │ │ │ ├── golden_troupes_reward_4_5.py │ │ │ ├── lumenstone_adjuvant_4_5.py │ │ │ └── tome_of_the_eternal_flow_4_5.py │ │ │ └── characters │ │ │ ├── __init__.py │ │ │ ├── charlotte_4_5.py │ │ │ ├── fatui_electro_cicin_mage_4_5.py │ │ │ ├── kirara_4_5.py │ │ │ └── neuvillette_4_5.py │ ├── player_table.py │ ├── query.py │ ├── status │ │ ├── __init__.py │ │ ├── base.py │ │ ├── character_status │ │ │ ├── __init__.py │ │ │ ├── anemo_characters.py │ │ │ ├── artifacts.py │ │ │ ├── base.py │ │ │ ├── cryo_characters.py │ │ │ ├── dendro_characters.py │ │ │ ├── electro_characters.py │ │ │ ├── event_cards.py │ │ │ ├── foods.py │ │ │ ├── geo_characters.py │ │ │ ├── hydro_characters.py │ │ │ ├── pyro_characters.py │ │ │ ├── system.py │ │ │ └── weapons.py │ │ └── team_status │ │ │ ├── __init__.py │ │ │ ├── anemo_characters.py │ │ │ ├── base.py │ │ │ ├── cryo_characters.py │ │ │ ├── dendro_characters.py │ │ │ ├── electro_characters.py │ │ │ ├── event_cards.py │ │ │ ├── geo_characters.py │ │ │ ├── hydro_characters.py │ │ │ ├── pyro_characters.py │ │ │ ├── system.py │ │ │ └── weapons.py │ ├── struct.py │ └── summon │ │ ├── __init__.py │ │ ├── base.py │ │ ├── events.py │ │ └── system.py │ ├── tools.py │ └── utils │ ├── __init__.py │ ├── class_registry.py │ ├── deck_code.py │ ├── deck_code_data.json │ ├── default_desc.json │ ├── desc_registry.py │ └── instance_factory.py ├── templates ├── card.py ├── character.py └── test.py └── tests ├── __init__.py ├── default_random_state.py ├── server ├── bugfix │ ├── jsons │ │ ├── test_boar_talent.json │ │ ├── test_counter_reset_when_revive.json │ │ ├── test_dunyarzad_no_draw.json │ │ ├── test_dvalin_talent.json │ │ ├── test_dvalin_talent_2.json │ │ ├── test_issue_106.json │ │ ├── test_issue_82.json │ │ ├── test_kazuha_attack_by_baizhu_q.json │ │ ├── test_nilou_dendro_core.json │ │ └── test_trigger_order_in_character.json │ ├── test_boar_talent.py │ ├── test_counter_reset_when_revive.py │ ├── test_dunyarzad_no_draw.py │ ├── test_dvalin_talent.py │ ├── test_dvalin_talent_2.py │ ├── test_json_bugfix.py │ ├── test_kazuha_attack_by_baizhu_q.py │ ├── test_nilou_dendro_core.py │ └── test_trigger_order_in_character.py ├── cards │ ├── test_arcane_legend.py │ ├── test_artifacts.py │ ├── test_element_resonance.py │ ├── test_event_cards.py │ ├── test_food.py │ ├── test_nation_resonance.py │ ├── test_vermillion_shimenawa.py │ └── test_weapons.py ├── characters │ ├── anemo │ │ ├── test_jean.py │ │ ├── test_kaedehara_kazuha.py │ │ ├── test_maguu_kenki.py │ │ ├── test_sucrose.py │ │ ├── test_venti.py │ │ ├── test_wanderer.py │ │ └── test_xiao.py │ ├── cryo │ │ ├── test_ayaka.py │ │ ├── test_chongyun.py │ │ ├── test_diona.py │ │ ├── test_eula.py │ │ ├── test_ganyu.py │ │ ├── test_kaeya.py │ │ ├── test_qiqi.py │ │ └── test_shenhe.py │ ├── dendro │ │ ├── test_baizhu.py │ │ ├── test_collei.py │ │ ├── test_grass_chicken.py │ │ ├── test_nahida.py │ │ ├── test_tighnari.py │ │ └── test_yaoyao.py │ ├── electro │ │ ├── test_beidou.py │ │ ├── test_cyno.py │ │ ├── test_dori.py │ │ ├── test_electro_hypostasis.py │ │ ├── test_fischl.py │ │ ├── test_keqing.py │ │ ├── test_kujou_sara.py │ │ ├── test_lisa.py │ │ ├── test_miko.py │ │ ├── test_raiden_shogun.py │ │ └── test_razor.py │ ├── geo │ │ ├── test_albedo.py │ │ ├── test_arataki_itto.py │ │ ├── test_ningguang.py │ │ ├── test_noelle.py │ │ ├── test_stonehide.py │ │ └── test_zhongli.py │ ├── hydro │ │ ├── test_barbara.py │ │ ├── test_candace.py │ │ ├── test_kamisato_ayato_and_cryo_cicin.py │ │ ├── test_kokomi.py │ │ ├── test_mirror_maiden.py │ │ ├── test_mona.py │ │ ├── test_nilou.py │ │ ├── test_rhodeia.py │ │ ├── test_tartaglia.py │ │ └── test_xingqiu.py │ ├── pyro │ │ ├── test_abyss_fire.py │ │ ├── test_amber.py │ │ ├── test_bennett.py │ │ ├── test_dead_agent.py │ │ ├── test_dehya.py │ │ ├── test_diluc.py │ │ ├── test_hutao.py │ │ ├── test_klee.py │ │ ├── test_xiangling.py │ │ ├── test_yanfei.py │ │ └── test_yoimiya.py │ └── test_mobs.py ├── patch │ ├── v43 │ │ ├── jsons │ │ │ ├── test_11card.json │ │ │ ├── test_alhaitham.json │ │ │ ├── test_azhdaha.json │ │ │ ├── test_dvalin.json │ │ │ ├── test_eremite.json │ │ │ ├── test_gorou.json │ │ │ ├── test_layla_yelan.json │ │ │ ├── test_lynette.json │ │ │ ├── test_lyney.json │ │ │ ├── test_new_4_transform.json │ │ │ ├── test_seed_dispensary.json │ │ │ ├── test_signora.json │ │ │ ├── test_thunder_manifestation.json │ │ │ └── test_timaeus_wagner.json │ │ ├── test_alhaitham.py │ │ ├── test_azhdaha.py │ │ ├── test_dvalin.py │ │ ├── test_eremite.py │ │ ├── test_fishchip_gilded.py │ │ ├── test_four_leaf.py │ │ ├── test_gorou.py │ │ ├── test_layla_yelan.py │ │ ├── test_lynette.py │ │ ├── test_lyney.py │ │ ├── test_mamere_judgment.py │ │ ├── test_memento_lens.py │ │ ├── test_new_4_transform.py │ │ ├── test_opera_weeping.py │ │ ├── test_others_v43.py │ │ ├── test_rhodeia_ocean_stone_v43.py │ │ ├── test_seed_dispensary.py │ │ ├── test_signora.py │ │ ├── test_thunder_manifestation.py │ │ ├── test_timaeus_wagner_v43.py │ │ └── test_v43equips_boar_fall.py │ ├── v44 │ │ ├── jsons │ │ │ ├── balance_thuncer_vourukashas_stove.json │ │ │ ├── cryo_hypostasis.json │ │ │ ├── jeht_sunyata.json │ │ │ ├── machine_2.json │ │ │ ├── millennial_pearl_seahorse.json │ │ │ ├── millennial_pearl_seahorse2.json │ │ │ ├── sapwood_machine_veteran.json │ │ │ ├── sayu.json │ │ │ ├── silver.json │ │ │ ├── thoma.json │ │ │ └── veteran_2.json │ │ ├── test_json_4_4.py │ │ └── test_sunyata_flower.py │ └── v45 │ │ ├── jsons │ │ ├── arcane_blast_v45.json │ │ ├── balance_v45.json │ │ ├── charlotte_v45.json │ │ ├── coverage_improve_v45.json │ │ ├── electro_cicin_mage_2_v45.json │ │ ├── electro_cicin_mage_v45.json │ │ ├── kirara_v45.json │ │ ├── lumenstone_enternalflow_v45.json │ │ ├── meropide_golden_v45.json │ │ └── neuvillette_v45.json │ │ ├── template.py │ │ └── test_json_4_5.py ├── scenario │ ├── test_maguu_kenki_10_10_10.py │ └── test_wanderer_icyquill.py ├── supports │ ├── kujirai_log.json │ ├── test_companions.py │ ├── test_items.py │ └── test_locations.py ├── test_deck.py ├── test_deck_code.py ├── test_draw_card.py ├── test_elemental_reaction.py ├── test_interaction.py ├── test_others.py ├── test_pipeline.py ├── test_query.py ├── test_recreate_mode.py ├── test_registry.py └── version_update │ ├── test_version_4_1.py │ └── test_version_4_2.py └── utils_for_test.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | 4 | [report] 5 | include = 6 | ./src/lpsim/server/* 7 | ./src/lpsim/utils/* 8 | ./tests/* 9 | ; Regexes for lines to exclude from consideration 10 | exclude_also = 11 | ; Don't complain about missing debug-only code: 12 | def __repr__ 13 | def __str__ 14 | if self\.debug 15 | 16 | ; Don't complain if tests don't hit defensive assertion code: 17 | raise AssertionError 18 | raise NotImplementedError 19 | 20 | ; Don't complain if non-runnable code isn't run: 21 | if 0: 22 | if __name__ == .__main__.: 23 | 24 | ; Don't complain about abstract methods, they aren't run: 25 | @(abc\.)?abstractmethod 26 | 27 | ; Don't complain about ...: 28 | 29 | ignore_errors = True 30 | 31 | [html] 32 | directory = htmlcov 33 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @zyr17 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | # test build CI, all the steps are the same as publish.yml, except will not publish 2 | # to pypi or doing tests. 3 | name: Test Build 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | 9 | jobs: 10 | 11 | test: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 # Shallow clones should be disabled for setuptools_scm 19 | fetch-tags: true # Fetch all tags for setuptools_scm 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: '3.10' 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install ".[dev]" 30 | 31 | - name: Fetch tags and print 32 | run: | 33 | git tag 34 | python -m setuptools_scm 35 | 36 | - name: Create distributions 37 | run: | 38 | python -m build 39 | 40 | - name: Upload artifacts 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: package 44 | path: dist/* 45 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yaml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | pre-commit: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-python@v5 14 | with: 15 | python-version: "3.12" 16 | - uses: pre-commit/action@v3.0.1 17 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish to PyPI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | test: 13 | uses: ./.github/workflows/pytest.yml 14 | 15 | upload-to-pypi: 16 | needs: test 17 | runs-on: ubuntu-latest 18 | if: github.ref_type == 'tag' 19 | environment: 20 | name: release 21 | url: https://pypi.org/p/lpsim/ 22 | permissions: 23 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 # Shallow clones should be disabled for setuptools_scm 28 | fetch-tags: true # Fetch all tags for setuptools_scm 29 | 30 | - name: Set up Python 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: '3.10' 34 | 35 | - name: Install dependencies 36 | run: | 37 | python -m pip install --upgrade pip 38 | pip install ".[dev]" 39 | 40 | - name: Fetch tags and print 41 | run: | 42 | git tag 43 | python -m setuptools_scm 44 | 45 | - name: Create distributions 46 | run: | 47 | python -m build 48 | 49 | - name: Upload artifacts 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: package 53 | path: dist/* 54 | 55 | - name: Publish package distributions to TestPyPI 56 | uses: pypa/gh-action-pypi-publish@release/v1 57 | 58 | upload-to-test-pypi: 59 | needs: test 60 | if: github.ref_type != 'tag' 61 | runs-on: ubuntu-latest 62 | environment: 63 | name: test-release 64 | url: https://test.pypi.org/p/lpsim/ 65 | permissions: 66 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 67 | steps: 68 | - uses: actions/checkout@v4 69 | with: 70 | fetch-depth: 0 # Shallow clones should be disabled for setuptools_scm 71 | fetch-tags: true # Fetch all tags for setuptools_scm 72 | 73 | - name: Set up Python 74 | uses: actions/setup-python@v5 75 | with: 76 | python-version: '3.10' 77 | 78 | - name: Install dependencies 79 | run: | 80 | python -m pip install --upgrade pip 81 | pip install ".[dev]" 82 | 83 | - name: Fetch tags and print 84 | run: | 85 | git tag 86 | python -m setuptools_scm 87 | 88 | - name: Create distributions 89 | run: | 90 | python -m build 91 | 92 | - name: Upload artifacts 93 | uses: actions/upload-artifact@v4 94 | with: 95 | name: package 96 | path: dist/* 97 | 98 | - name: Publish package distributions to TestPyPI 99 | uses: pypa/gh-action-pypi-publish@release/v1 100 | with: 101 | repository-url: https://test.pypi.org/legacy/ 102 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: pytest 2 | 3 | on: 4 | push: 5 | paths: 6 | - "src/**" 7 | - "tests/**" 8 | - "**/*.py" 9 | - "!src/lpsim/env/**" 10 | - "!src/lpsim/network/**" 11 | pull_request: 12 | paths: 13 | - "src/**" 14 | - "tests/**" 15 | - "**/*.py" 16 | - "!src/lpsim/env/**" 17 | - "!src/lpsim/network/**" 18 | workflow_dispatch: 19 | workflow_call: 20 | 21 | jobs: 22 | test: 23 | runs-on: ubuntu-latest 24 | strategy: 25 | matrix: 26 | python-version: ['3.10', '3.11', '3.12'] 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Set up Python 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | cache: 'pip' 35 | 36 | - name: Install dependencies 37 | run: | 38 | pip install -e ".[dev]" 39 | 40 | - name: Create pyrightconfig.json 41 | run: | 42 | echo '{ 43 | "exclude": [ 44 | "src/lpsim/env/**" 45 | ], 46 | "typeCheckingMode": "basic" 47 | }' > pyrightconfig.json 48 | 49 | - name: Run type check 50 | run: | 51 | pyright --version 52 | pyright . 53 | 54 | - name: Run tests with coverage 55 | run: | 56 | python -m setuptools_scm 57 | python -m pytest --cov 58 | coverage xml 59 | 60 | - name: Upload coverage to Coveralls 61 | if: github.ref == 'refs/heads/master' 62 | uses: coverallsapp/github-action@v2 63 | -------------------------------------------------------------------------------- /.github/workflows/remove-closed-issue-label.yml: -------------------------------------------------------------------------------- 1 | name: Remove Workon Label 2 | on: 3 | issues: 4 | types: [closed] 5 | 6 | permissions: 7 | issues: write 8 | 9 | jobs: 10 | remove_label: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Remove label 14 | uses: actions/github-script@v7 15 | with: 16 | github-token: ${{secrets.GITHUB_TOKEN}} 17 | script: | 18 | let req = { 19 | issue_number: context.issue.number, 20 | owner: context.repo.owner, 21 | repo: context.repo.repo, 22 | }; 23 | const labelToRemove = 'workon'; 24 | const issue = await github.rest.issues.get(req); 25 | const labels = issue.data.labels; 26 | const hasLabel = labels.some(label => label.name === labelToRemove); 27 | if (hasLabel) { 28 | await github.rest.issues.removeLabel({...req, name: labelToRemove}); 29 | } 30 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "frontend"] 2 | path = frontend 3 | url = git@github.com:LPSim/frontend.git 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/crate-ci/typos 3 | rev: v1.17.2 4 | hooks: 5 | - id: typos 6 | 7 | - repo: https://github.com/astral-sh/ruff-pre-commit 8 | rev: v0.1.14 9 | hooks: 10 | - id: ruff 11 | args: [--fix] 12 | - id: ruff-format 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python", 4 | "charliermarsh.ruff" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "CurrentFile", 9 | "type": "debugpy", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "cwd": "${workspaceFolder}", 14 | "env": { 15 | // NEED TO INSTALL Command Variable plugin! 16 | "PYTHONPATH": "${workspaceFolder}${command:extension.commandvariable.envListSep}${workspaceFolder}/src", 17 | "LPSIM_DEBUG_LEVEL": "DEBUG", 18 | }, 19 | "justMyCode": true 20 | }, 21 | { 22 | "name": "Uvicorn", 23 | "type": "debugpy", 24 | "request": "launch", 25 | "program": "http_test_serve.py", 26 | "args": [ 27 | ], 28 | "console": "integratedTerminal", 29 | "cwd": "${workspaceFolder}", 30 | "env": { 31 | "PYTHONPATH": "${workspaceFolder}${command:extension.commandvariable.envListSep}${workspaceFolder}/src", 32 | // "PYTHONPATH": "${workspaceFolder}/src", 33 | }, 34 | "justMyCode": true 35 | }, 36 | ] 37 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "charliermarsh.ruff" 4 | }, 5 | "python.testing.pytestArgs": [ 6 | "tests" 7 | ], 8 | "python.testing.unittestEnabled": false, 9 | "python.testing.pytestEnabled": true 10 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to _LPSim_ 2 | 3 | Thanks for you interest in contributing to _lpsim_! 4 | 5 | ## Prerequisites 6 | 7 | _lpsim_ is written in [Python](https://www.python.org/), requires Python>=3.10 to run, so please make sure you have Python>=3.10 installed. 8 | 9 | To confirm your Python version, simply run `python --version` or `python3 --version` in terminal ("Terminal" app on macOS or "Windows Terminal" on Windows). 10 | 11 | To develop _lpsim_, you'll need to install [git](https://git-scm.com/) for version control, and run: 12 | 13 | ``` 14 | git clone https://github.com/LPSim/backend 15 | cd backend 16 | ``` 17 | 18 | to enter the project folder. 19 | 20 | Then, you should create a virtual environment for this project. It's better to keep the dependencies environment isolated. 21 | 22 | ``` 23 | python -m venv venv 24 | source venv/bin/activate 25 | ``` 26 | 27 | Each time you open a new terminal, you should run `source venv/bin/activate` to enter the venv. 28 | 29 | ## Install _lpsim_ for develpement 30 | 31 | > [!WARNING] 32 | > Running `pip install lpsim` or `pip install .` in `backend` folder does not install development dependencies. 33 | 34 | Activate venv, enter the `backend` folder you have cloned before, and run `pip install -e ".[dev]"`. This command will install pytest and its plugins. The option `-e` helps you to install an editable version of `lpsim` so that you can import `lpsim` from the `backend` folder locally. 35 | 36 | Optionally, you can also run `pip install pre-commit` or `pipx install pre-commit` to install pre-commit for linting and formatting, and run `pre-commit install` to install a pre-commit hook in your **local** repository. After that, each time you commit your changes, pre-commit will run hooks to find typos, do lints and format your codes. 37 | 38 | ## Running tests 39 | 40 | Since @zyr17 writes some test codes in `tests` that refer to each other, we can not simply run `pytest` to run tests. Instead, we need to run `python -m pytest` in `backend` folder, which will add the current directory (`backend` folder) to `sys.path`. More details can be found at [pytest's documentation](https://docs.pytest.org/en/7.2.x/how-to/usage.html#calling-pytest-through-python-m-pytest). 41 | -------------------------------------------------------------------------------- /docs/images/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LPSim/backend/0866b9b2a0a6a696eeb8291652df7fb36eeec3c9/docs/images/wechat.png -------------------------------------------------------------------------------- /http_log_replay.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from lpsim.tools import interact_match_with_agents, read_log 3 | from lpsim.network import HTTPServer 4 | from lpsim.server.match import MatchConfig 5 | import logging 6 | 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | 11 | if __name__ == "__main__": 12 | log_path = "logs.json" 13 | if len(sys.argv) > 1: 14 | log_path = sys.argv[1] 15 | log_str = open(log_path).read() 16 | agents, match = read_log(log_str, use_16_omni=None) 17 | # for i in range(2): 18 | # characters = match.player_tables[i].player_deck_information.characters # noqa: E501 19 | # for c in characters: 20 | # c.hp = c.max_hp = 30 21 | match.config.history_level = 10 22 | try: 23 | interact_match_with_agents(agents[0], agents[1], match) 24 | except Exception: 25 | print("*************************************************************") 26 | print("!!!!! ERROR: play log FAILED, play to last success log. !!!!!") 27 | print("*************************************************************") 28 | server = HTTPServer( 29 | decks=["", ""], 30 | match_config=MatchConfig( 31 | check_deck_restriction=False, 32 | card_number=None, 33 | max_same_card_number=None, 34 | character_number=None, 35 | max_round_number=999, 36 | random_first_player=False, 37 | history_level=10, 38 | ), 39 | ) 40 | server.match = match 41 | 42 | server.run() 43 | -------------------------------------------------------------------------------- /http_room_serve.py: -------------------------------------------------------------------------------- 1 | """ 2 | Serve a room server on localhost:7999. Note you should wrap the server with 3 | `if __name__ == '__main__':` to avoid error. 4 | """ 5 | 6 | 7 | from lpsim.network.http_room_server import HTTPRoomServer 8 | 9 | 10 | if __name__ == "__main__": 11 | room_server = HTTPRoomServer( 12 | max_rooms=3, 13 | port=7999, 14 | room_port_range=[8001, 8010], 15 | room_timeout=300, 16 | admin_password="foobar", 17 | run_args={}, 18 | ) 19 | room_server.run() 20 | print("server closed") 21 | -------------------------------------------------------------------------------- /http_test_serve.py: -------------------------------------------------------------------------------- 1 | from lpsim.server.event_handler import OmnipotentGuideEventHandler_3_3 2 | from lpsim.network import HTTPServer 3 | from lpsim.server.match import MatchConfig, Match 4 | from tests.utils_for_test import get_random_state 5 | import logging 6 | 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | 11 | if __name__ == "__main__": 12 | deck_str_1 = """ 13 | default_version:4.4 14 | character:AnemoMobMage 15 | character:CryoMob 16 | character:PyroMobMage 17 | character:Sangonomiya Kokomi 18 | Mushroom Pizza*10 19 | Matsutake Meat Rolls*10 20 | Silver and Melus*10 21 | """ 22 | deck_str_2 = """ 23 | character:Nahida@3.7 24 | character:Rhodeia of Loch@3.3 25 | character:Fischl@3.3 26 | Gambler's Earrings@3.8 27 | Paimon@3.3 28 | Chef Mao@4.1 29 | Dunyarzad@4.1 30 | The Bestest Travel Companion!@3.3 31 | Paimon@3.3 32 | Liben@3.3 33 | Liben@3.3 34 | Send Off@3.7 35 | Teyvat Fried Egg@4.1 36 | Dunyarzad@4.1 37 | Sweet Madame@3.3 38 | I Haven't Lost Yet!@4.0 39 | Send Off@3.7 40 | Lotus Flower Crisp@3.3 41 | Magic Guide@3.3*15 42 | """ 43 | server = HTTPServer( 44 | decks=[deck_str_1, deck_str_1], 45 | match_config=MatchConfig( 46 | check_deck_restriction=False, 47 | card_number=None, 48 | max_same_card_number=None, 49 | character_number=None, 50 | max_round_number=999, 51 | random_first_player=False, 52 | # max_hand_size = 999, 53 | # recreate_mode = True, 54 | history_level=10, 55 | # make_skill_prediction = True, 56 | # random_object_information = { 57 | # 'rhodeia': [ 58 | # 'squirrel', 'raptor', 'frog', 'squirrel', 'raptor', 59 | # 'frog', 'frog', 'squirrel', 'squirrel' 60 | # ], 61 | # } 62 | ), 63 | ) 64 | 65 | # # modify hp 66 | # for deck in server.decks: 67 | # for character in deck.characters: 68 | # character.hp = 40 69 | # character.max_hp = 40 70 | 71 | # fix random seed 72 | match = Match(random_state=get_random_state()) 73 | match.set_deck(server.decks) 74 | match.config = server.match.config 75 | 76 | # rich mode 77 | match.event_handlers.append(OmnipotentGuideEventHandler_3_3()) 78 | match.config.initial_dice_number = 16 79 | 80 | server.match = match 81 | server.match_random_state = get_random_state() 82 | server.match.start() 83 | server.match._save_history() 84 | server.match.step() 85 | server.run() 86 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Literal 3 | from lpsim.server.match import Match 4 | from lpsim.utils import BaseModel 5 | from lpsim.server.deck import Deck 6 | from lpsim.agents import RandomAgent 7 | 8 | 9 | class Main(BaseModel): 10 | """ """ 11 | 12 | version: str = "1.0.0" 13 | name: Literal["GITCG"] = "GITCG" 14 | match: Match = Match() 15 | 16 | 17 | if __name__ == "__main__": 18 | logging.basicConfig(level=logging.WARNING) 19 | agent_0 = RandomAgent(player_idx=0) 20 | agent_1 = RandomAgent(player_idx=1) 21 | main = Main() 22 | deck = Deck.from_str( 23 | """ 24 | default_version:4.1 25 | character:Rhodeia of Loch 26 | character:Kamisato Ayaka 27 | character:Yaoyao 28 | Traveler's Handy Sword*5 29 | Gambler's Earrings*5 30 | Kanten Senmyou Blessing*5 31 | Sweet Madame*5 32 | Abyssal Summons*5 33 | Fatui Conspiracy*5 34 | Timmie*5 35 | """ 36 | ) 37 | main.match.set_deck([deck, deck]) 38 | main.match.config.max_same_card_number = 30 39 | main.match.config.history_level = 10 40 | main.match.config.check_deck_restriction = False 41 | main.match.config.initial_hand_size = 20 42 | main.match.config.max_hand_size = 30 43 | main.match.config.card_number = None 44 | assert main.match.start()[0] 45 | main.match.step() 46 | 47 | while main.match.round_number < 100 and not main.match.is_game_end(): 48 | if main.match.need_respond(0): 49 | current_agent = agent_0 50 | elif main.match.need_respond(1): 51 | current_agent = agent_1 52 | else: 53 | raise RuntimeError("no agent need to respond") 54 | resp = current_agent.generate_response(main.match) 55 | assert resp is not None 56 | main.match.respond(resp) 57 | main.match.step() 58 | 59 | main.match.get_history_json(filename="logs.txt") 60 | print("game end, save to logs.txt") 61 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=68.2.2", "setuptools-scm>=7.1.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "lpsim" 7 | dynamic = ["version"] 8 | authors = [{ name = "Zyr17", email = "jzjqz17@gmail.com" }] 9 | description = "Lochfolk Prinzessin Simulator, which simulates Genius Invokation TCG" 10 | readme = "README.md" 11 | requires-python = ">=3.10" 12 | classifiers = [ 13 | "Programming Language :: Python :: 3", 14 | "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python :: 3.10", 17 | ] 18 | dependencies = [ 19 | "pydantic==1.10.14", 20 | "typing_extensions", 21 | "dictdiffer", 22 | "fastapi", 23 | "uvicorn[standard]", 24 | ] 25 | 26 | [project.optional-dependencies] 27 | dev = [ 28 | "build", 29 | "numpy", 30 | "setuptools-scm", 31 | "pytest", 32 | "pytest-cov", 33 | "pytest-xdist", 34 | "pyright", 35 | ] 36 | 37 | [project.urls] 38 | "Homepage" = "https://github.com/LPSim/backend" 39 | "Bug Tracker" = "https://github.com/LPSim/backend/issues" 40 | 41 | [tool.setuptools_scm] 42 | write_to = "src/lpsim/_version.py" 43 | version_scheme = "post-release" 44 | local_scheme = "no-local-version" 45 | 46 | [tool.typos] 47 | files.extend-exclude = ["/src/lpsim/utils/deck_code_data.json"] 48 | 49 | [tool.typos.default.extend-words] 50 | Invokation = "Invokation" 51 | 52 | [tool.ruff] 53 | exclude = ["templates"] 54 | 55 | [tool.pyright] 56 | typeCheckingMode = "basic" 57 | include = ["src", "tests"] 58 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | # This is coming from https://github.com/pytest-dev/pytest-xdist/issues/825 and it's caused from pytest-cov 4 | # Remove once fixed: https://github.com/pytest-dev/pytest-cov/issues/557 5 | ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning 6 | 7 | # Build in coverage and pytest-xdist multiproc testing. 8 | addopts = --cov --durations=30 -n auto --dist worksteal --import-mode=importlib 9 | 10 | # Our pytest units are located in the ./test/ directory. 11 | testpaths = tests 12 | 13 | markers = 14 | slowtest: marks tests as slow (deselect with '-m "not slow"') -------------------------------------------------------------------------------- /src/lpsim/__init__.py: -------------------------------------------------------------------------------- 1 | from .server.deck import Deck # noqa: F401 2 | from .server.match import Match, MatchState # noqa: F401 3 | from .utils import ( # noqa: F401 4 | register_class, 5 | DescDictType, 6 | update_desc, 7 | deck_str_to_deck_code, 8 | deck_code_to_deck_str, 9 | ) 10 | 11 | 12 | try: 13 | from ._version import __version__, __version_tuple__ # type: ignore 14 | except ModuleNotFoundError: 15 | __version__ = "unknown" 16 | __version_tuple__ = (0, 0, 0) 17 | -------------------------------------------------------------------------------- /src/lpsim/agents/__init__.py: -------------------------------------------------------------------------------- 1 | from .interaction_agent import InteractionAgent 2 | from .nothing_agent import NothingAgent 3 | from .random_agent import RandomAgent 4 | from .agent_base import AgentBase # noqa: F401 5 | 6 | 7 | Agents = InteractionAgent | NothingAgent | RandomAgent 8 | -------------------------------------------------------------------------------- /src/lpsim/agents/agent_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base class of all agents. 3 | """ 4 | from ..server.interaction import Responses 5 | from ..server.match import Match 6 | from ..utils import BaseModel 7 | 8 | 9 | class AgentBase(BaseModel): 10 | """ 11 | Base class of all agents. 12 | """ 13 | 14 | player_idx: int 15 | 16 | def generate_response(self, match: Match) -> Responses | None: 17 | """ 18 | generate response based on the match. Return None if no response is 19 | generated. 20 | """ 21 | raise NotImplementedError() 22 | -------------------------------------------------------------------------------- /src/lpsim/agents/nothing_agent.py: -------------------------------------------------------------------------------- 1 | from .agent_base import AgentBase 2 | from ..server.match import Match 3 | from ..server.interaction import ( 4 | Responses, 5 | SwitchCardResponse, 6 | ChooseCharacterResponse, 7 | RerollDiceResponse, 8 | DeclareRoundEndResponse, 9 | ) 10 | 11 | 12 | class NothingAgent(AgentBase): 13 | """ 14 | Agent that do nothing, only response essential requests. 15 | """ 16 | 17 | def generate_response(self, match: Match) -> Responses | None: 18 | for req in match.requests: 19 | if req.player_idx == self.player_idx: 20 | if req.name == "SwitchCardRequest": 21 | return SwitchCardResponse(request=req, card_idxs=[]) 22 | elif req.name == "ChooseCharacterRequest": 23 | return ChooseCharacterResponse( 24 | request=req, character_idx=req.available_character_idxs[0] 25 | ) 26 | elif req.name == "RerollDiceRequest": 27 | return RerollDiceResponse(request=req, reroll_dice_idxs=[]) 28 | elif req.name == "DeclareRoundEndRequest": 29 | return DeclareRoundEndResponse(request=req) 30 | -------------------------------------------------------------------------------- /src/lpsim/consts.py: -------------------------------------------------------------------------------- 1 | from .server.consts import * # noqa: F401, F403 2 | -------------------------------------------------------------------------------- /src/lpsim/network/__init__.py: -------------------------------------------------------------------------------- 1 | from .http_server import HTTPServer # noqa: F401 2 | from .http_room_server import HTTPRoomServer # noqa: F401 3 | -------------------------------------------------------------------------------- /src/lpsim/network/__version__.py: -------------------------------------------------------------------------------- 1 | # This file will record minimum supported version of frontend and current version. 2 | # When frontend is updated, modify the version here. 3 | from .. import __version_tuple__, __version__ 4 | 5 | __frontend_version__ = "0.4.5.0" 6 | 7 | 8 | __all__ = [ 9 | "__frontend_version__", 10 | "__version__", 11 | "__version_tuple__", 12 | ] 13 | -------------------------------------------------------------------------------- /src/lpsim/network/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, List, Tuple 3 | from ..server.event_handler import OmnipotentGuideEventHandler_3_3 4 | from ..server.match import Match, MatchConfig 5 | from ..server.deck import Deck 6 | 7 | 8 | def get_new_match( 9 | decks: List[Deck], 10 | seed: Any = None, 11 | rich_mode: bool = False, 12 | match_config: MatchConfig | None = None, 13 | history_level: int = 10, 14 | make_skill_prediction: bool = True, 15 | auto_step: bool = True, 16 | ) -> Tuple[Match, Any]: 17 | """ 18 | Generate new match with given conditions. 19 | 20 | Args: 21 | decks: The decks of players. If its length is zero, will not set decks 22 | or start the match. 23 | seed: The random seed. It should follow the format of 24 | numpy.RandomState.get_state(legacy=True) or random.Random.getstate(). 25 | rich_mode: If True, use rich mode, at round start, players is given 26 | 16 omni dice. Mainly used in code testing. 27 | match_config: The config of the match. If None, use default config. 28 | Mainly used to set special rules, accept illegal decks, use 29 | recreate mode, etc. 30 | history_level: The level of history. Default is 10, to record important 31 | actions. Will have no effect when match_config is not None. 32 | make_skill_prediction: If True, make skill prediction. Will have no 33 | effect when match_config is not None. 34 | auto_step: If True, auto step the match once. 35 | Returns: 36 | The generated match and its initial random state. 37 | If generate failed or error occurred, raise error. 38 | """ 39 | if seed: 40 | match: Match = Match(random_state=seed) 41 | else: 42 | match: Match = Match() 43 | 44 | if match_config: 45 | match.config = match_config 46 | else: 47 | match.config.history_level = history_level 48 | match.config.make_skill_prediction = make_skill_prediction 49 | 50 | if rich_mode: 51 | match.config.initial_dice_number = 16 52 | match.event_handlers.append(OmnipotentGuideEventHandler_3_3()) 53 | 54 | random_state = match.random_state 55 | 56 | if len(decks) > 0: 57 | match.set_deck(decks) 58 | start_result = match.start() 59 | if not start_result[0]: 60 | raise RuntimeError(f"Match start failed. {start_result[1]}") 61 | 62 | if auto_step: 63 | if len(decks) == 0: 64 | logging.warning("No deck is set, match will not auto_step.") 65 | else: 66 | match._save_history() 67 | match.step() 68 | 69 | return match, random_state 70 | -------------------------------------------------------------------------------- /src/lpsim/resources/consts.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class CharacterIcons(str, Enum): 5 | NAHIDA = "nahida" 6 | QIQI = "qiqi" 7 | KLEE = "klee" 8 | YAOYAO = "yaoyao" 9 | SAYU = "sayu" 10 | DORI = "dori" 11 | DIONA = "diona" 12 | -------------------------------------------------------------------------------- /src/lpsim/responses.py: -------------------------------------------------------------------------------- 1 | from .server.interaction import ( # noqa: F401 2 | Responses, 3 | SwitchCardResponse, 4 | ChooseCharacterResponse, 5 | RerollDiceResponse, 6 | SwitchCharacterResponse, 7 | ElementalTuningResponse, 8 | DeclareRoundEndResponse, 9 | UseSkillResponse, 10 | UseCardResponse, 11 | ) 12 | -------------------------------------------------------------------------------- /src/lpsim/server/__init__.py: -------------------------------------------------------------------------------- 1 | from .card import ( 2 | LocationBase, 3 | ItemBase, 4 | WeaponBase, 5 | SupportBase, 6 | ArtifactBase, 7 | CompanionBase, 8 | ) 9 | from .character import CharacterBase, TalentBase, SkillBase 10 | from .status import StatusBase, CharacterStatusBase, TeamStatusBase 11 | from .summon import SummonBase 12 | from .match import Match, MatchState 13 | from .object_base import ObjectBase, CardBase 14 | from .deck import Deck 15 | 16 | # import patch to automatically register classes 17 | from .patch import * # noqa: F401, F403 18 | 19 | __all__ = ( 20 | "LocationBase", 21 | "ItemBase", 22 | "WeaponBase", 23 | "SupportBase", 24 | "ArtifactBase", 25 | "CompanionBase", 26 | "CharacterBase", 27 | "TalentBase", 28 | "SkillBase", 29 | "StatusBase", 30 | "CharacterStatusBase", 31 | "TeamStatusBase", 32 | "SummonBase", 33 | "Match", 34 | "MatchState", 35 | "ObjectBase", 36 | "CardBase", 37 | "Deck", 38 | ) 39 | -------------------------------------------------------------------------------- /src/lpsim/server/card/__init__.py: -------------------------------------------------------------------------------- 1 | from ...utils import import_all_modules 2 | from .equipment import WeaponBase, ArtifactBase 3 | from .support import SupportBase, LocationBase, ItemBase, CompanionBase 4 | 5 | 6 | import_all_modules(__file__, __name__) 7 | __all__ = ( 8 | "WeaponBase", 9 | "ArtifactBase", 10 | "SupportBase", 11 | "LocationBase", 12 | "ItemBase", 13 | "CompanionBase", 14 | ) 15 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | from .artifact import ArtifactBase 3 | from .weapon import WeaponBase 4 | 5 | 6 | import_all_modules(__file__, __name__) 7 | __all__ = ("ArtifactBase", "WeaponBase") 8 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/artifact/__init__.py: -------------------------------------------------------------------------------- 1 | from .....utils import import_all_modules 2 | from .base import ArtifactBase 3 | 4 | 5 | import_all_modules(__file__, __name__) 6 | __all__ = ("ArtifactBase",) 7 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/artifact/emblem_of_severed_fate.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from ....modifiable_values import DamageIncreaseValue 6 | 7 | from ....event import SkillEndEventArguments 8 | 9 | from ....consts import ObjectPositionType, SkillType 10 | 11 | from ....action import ChargeAction 12 | 13 | from ....struct import Cost 14 | from .base import ArtifactBase, RoundEffectArtifactBase 15 | 16 | 17 | class OrnateKabuto_4_0(ArtifactBase): 18 | name: Literal["Ornate Kabuto"] 19 | version: Literal["4.0"] = "4.0" 20 | cost: Cost = Cost(same_dice_number=1) 21 | usage: int = 0 22 | 23 | def event_handler_SKILL_END( 24 | self, event: SkillEndEventArguments, match: Any 25 | ) -> List[ChargeAction]: 26 | if not ( 27 | self.position.area == ObjectPositionType.CHARACTER 28 | and event.action.position.player_idx == self.position.player_idx 29 | and event.action.position.character_idx != self.position.character_idx 30 | and event.action.skill_type == SkillType.ELEMENTAL_BURST 31 | ): 32 | # not equipped or not our other use burst 33 | return [] 34 | # charge self by 1 35 | return [ 36 | ChargeAction( 37 | player_idx=self.position.player_idx, 38 | character_idx=self.position.character_idx, 39 | charge=1, 40 | ) 41 | ] 42 | 43 | 44 | class OrnateKabuto_3_5(OrnateKabuto_4_0): 45 | version: Literal["3.5"] = "3.5" 46 | cost: Cost = Cost(any_dice_number=2) 47 | 48 | 49 | class EmblemOfSeveredFate_4_1(OrnateKabuto_4_0, RoundEffectArtifactBase): 50 | name: Literal["Emblem of Severed Fate"] 51 | version: Literal["4.1"] = "4.1" 52 | cost: Cost = Cost(same_dice_number=2) 53 | max_usage_per_round: int = 1 54 | 55 | def value_modifier_DAMAGE_INCREASE( 56 | self, value: DamageIncreaseValue, match: Any, mode: Literal["TEST", "REAL"] 57 | ) -> DamageIncreaseValue: 58 | if self.position.area != ObjectPositionType.CHARACTER: 59 | # not equipped 60 | return value 61 | if not value.is_corresponding_character_use_damage_skill( 62 | self.position, match, SkillType.ELEMENTAL_BURST 63 | ): 64 | # not self use elemental burst 65 | return value 66 | if self.usage <= 0: 67 | # no usage left 68 | return value 69 | # modify damage 70 | assert mode == "REAL" 71 | self.usage -= 1 72 | value.damage += 2 73 | return value 74 | 75 | 76 | class EmblemOfSeveredFate_4_0(EmblemOfSeveredFate_4_1): 77 | version: Literal["4.0"] = "4.0" 78 | max_usage_per_round: int = 999 79 | 80 | 81 | class EmblemOfSeveredFate_3_7(EmblemOfSeveredFate_4_0): 82 | version: Literal["3.7"] = "3.7" 83 | cost: Cost = Cost(any_dice_number=3) 84 | 85 | 86 | register_class( 87 | OrnateKabuto_4_0 88 | | EmblemOfSeveredFate_4_1 89 | | EmblemOfSeveredFate_4_0 90 | | EmblemOfSeveredFate_3_7 91 | | OrnateKabuto_3_5 92 | ) 93 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/artifact/exile.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | from ....consts import SkillType 5 | from ....action import ChargeAction 6 | from ....event import SkillEndEventArguments 7 | from ....struct import Cost 8 | from .base import RoundEffectArtifactBase 9 | 10 | 11 | class ExilesCirclet_3_3(RoundEffectArtifactBase): 12 | name: Literal["Exile's Circlet"] 13 | version: Literal["3.3"] = "3.3" 14 | cost: Cost = Cost(any_dice_number=2) 15 | max_usage_per_round: int = 1 16 | 17 | def event_handler_SKILL_END( 18 | self, event: SkillEndEventArguments, match: Any 19 | ) -> List[ChargeAction]: 20 | """ 21 | If self use elemental burst, charge standby characters. 22 | """ 23 | if self.position.not_satisfy( 24 | "both pidx=same cidx=same and source area=character and target area=skill", 25 | event.action.position, 26 | ): 27 | # not equipped, or not this character use skill 28 | return [] 29 | if self.usage <= 0: 30 | # no usage left 31 | return [] 32 | if event.action.skill_type != SkillType.ELEMENTAL_BURST: 33 | # not elemental burst 34 | return [] 35 | # charge 36 | self.usage -= 1 37 | ret: List[ChargeAction] = [] 38 | for cid, character in enumerate( 39 | match.player_tables[self.position.player_idx].characters 40 | ): 41 | if cid == self.position.character_idx or character.is_defeated: 42 | # skip self and defeated characters 43 | continue 44 | ret.append( 45 | ChargeAction( 46 | player_idx=self.position.player_idx, 47 | character_idx=cid, 48 | charge=1, 49 | ) 50 | ) 51 | return ret 52 | 53 | 54 | register_class(ExilesCirclet_3_3) 55 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/artifact/gamblers.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from .base import ArtifactBase 6 | 7 | from ....consts import DieColor, ObjectPositionType 8 | 9 | from ....struct import Cost, ObjectPosition 10 | 11 | from ....event import CharacterDefeatedEventArguments 12 | from ....action import CreateDiceAction, Actions 13 | 14 | 15 | class GamblersEarrings_3_8(ArtifactBase): 16 | name: Literal["Gambler's Earrings"] 17 | version: Literal["3.8"] = "3.8" 18 | cost: Cost = Cost(same_dice_number=1) 19 | usage: int = 3 20 | 21 | def equip(self, match: Any) -> List[Actions]: 22 | """ 23 | Equip this artifact. Reset usage. 24 | """ 25 | self.usage = 3 26 | return [] 27 | 28 | def event_handler_CHARACTER_DEFEATED( 29 | self, event: CharacterDefeatedEventArguments, match: Any 30 | ) -> List[CreateDiceAction]: 31 | """ 32 | When an opposing character is defeated, check if the character this 33 | card is attached to is the active character. If so, create Omni 34 | Element x2. 35 | """ 36 | target_position = ObjectPosition( 37 | player_idx=event.action.player_idx, 38 | # all the following are not used to check, no need to set correctly 39 | area=ObjectPositionType.INVALID, 40 | id=-1, 41 | ) 42 | if self.position.not_satisfy( 43 | "both pidx=diff and source area=character active=true", 44 | target_position, 45 | match, 46 | ): 47 | # not opponent character defeated, or self not active, or self not equipped 48 | return [] 49 | if self.usage <= 0: 50 | # no usage left 51 | return [] 52 | self.usage -= 1 53 | return [ 54 | CreateDiceAction( 55 | player_idx=self.position.player_idx, number=2, color=DieColor.OMNI 56 | ) 57 | ] 58 | 59 | 60 | class GamblersEarrings_3_3(GamblersEarrings_3_8): 61 | version: Literal["3.3"] = "3.3" 62 | usage: int = 999 63 | 64 | def equip(self, match: Any) -> List[Actions]: 65 | """ 66 | Equip this artifact. Reset usage. 67 | """ 68 | self.usage = 999 69 | return [] 70 | 71 | 72 | register_class(GamblersEarrings_3_8 | GamblersEarrings_3_3) 73 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/artifact/gilded_dreams.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from ....consts import DamageType, ElementalReactionType, ObjectPositionType 6 | 7 | from ....action import Actions, DrawCardAction 8 | 9 | from ....event import ReceiveDamageEventArguments 10 | 11 | from ....struct import Cost 12 | from .base import RoundEffectArtifactBase 13 | 14 | 15 | class ShadowOfTheSandKing_4_2(RoundEffectArtifactBase): 16 | name: Literal["Shadow of the Sand King"] 17 | version: Literal["4.2"] = "4.2" 18 | cost: Cost = Cost(same_dice_number=1) 19 | max_usage_per_round: int = 1 20 | 21 | def equip(self, match: Any) -> List[Actions]: 22 | return super().equip(match) + [ 23 | DrawCardAction( 24 | player_idx=self.position.player_idx, 25 | number=1, 26 | draw_if_filtered_not_enough=True, 27 | ) 28 | ] 29 | 30 | def event_handler_RECEIVE_DAMAGE( 31 | self, event: ReceiveDamageEventArguments, match: Any 32 | ) -> List[Actions]: 33 | """ 34 | When opponent character take elemental reaction, draw a card. 35 | """ 36 | if ( 37 | self.position.area != ObjectPositionType.CHARACTER 38 | or self.position.player_idx == event.final_damage.target_position.player_idx 39 | or event.final_damage.element_reaction == ElementalReactionType.NONE 40 | or event.final_damage.damage_type != DamageType.DAMAGE 41 | or self.usage <= 0 42 | or self.position.character_idx 43 | != match.player_tables[self.position.player_idx].active_character_idx 44 | ): 45 | # not equipped, not opponent character, or not elemental reaction, 46 | # or not damage, or no usage, or self not active character 47 | return [] 48 | # draw card 49 | self.usage -= 1 50 | return [ 51 | DrawCardAction( 52 | player_idx=self.position.player_idx, 53 | number=1, 54 | draw_if_filtered_not_enough=True, 55 | ) 56 | ] 57 | 58 | 59 | register_class(ShadowOfTheSandKing_4_2) 60 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/artifact/instructors_cap.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from ....consts import ELEMENT_TO_DIE_COLOR, ElementalReactionType 6 | from ....action import CreateDiceAction 7 | from ....event import ReceiveDamageEventArguments, SkillEndEventArguments 8 | from ....struct import Cost 9 | from .base import RoundEffectArtifactBase 10 | 11 | 12 | class InstructorsCap_3_3(RoundEffectArtifactBase): 13 | # TODO electro hypostasis triggers reaction for second punch, 14 | # may generate dice for the next attack. Same as Chang and parametric? 15 | name: Literal["Instructor's Cap"] 16 | version: Literal["3.3"] = "3.3" 17 | cost: Cost = Cost(any_dice_number=2) 18 | max_usage_per_round: int = 3 19 | element_reaction_triggered: bool = False 20 | 21 | def event_handler_RECEIVE_DAMAGE( 22 | self, event: ReceiveDamageEventArguments, match: Any 23 | ) -> List[CreateDiceAction]: 24 | """ 25 | Record if elemental reaction triggered 26 | """ 27 | if event.elemental_reaction != ElementalReactionType.NONE: 28 | # has elemental reaction 29 | self.element_reaction_triggered = True 30 | return [] 31 | 32 | def event_handler_SKILL_END( 33 | self, event: SkillEndEventArguments, match: Any 34 | ) -> List[CreateDiceAction]: 35 | """ 36 | if elemental reaction reiggered, and self use skill, and have usage, 37 | create one die with same element type as self. 38 | """ 39 | if not self.element_reaction_triggered: 40 | # no elemental reaction triggered 41 | return [] 42 | self.element_reaction_triggered = False 43 | if self.usage <= 0: 44 | # no usage 45 | return [] 46 | if self.position.not_satisfy( 47 | "both pidx=same cidx=same and source area=character and target area=skill", 48 | event.action.position, 49 | ): 50 | # not self player use skill or not equipped 51 | return [] 52 | # create die 53 | self.usage -= 1 54 | character = match.player_tables[self.position.player_idx].characters[ 55 | self.position.character_idx 56 | ] 57 | return [ 58 | CreateDiceAction( 59 | player_idx=self.position.player_idx, 60 | number=1, 61 | color=ELEMENT_TO_DIE_COLOR[character.element], 62 | ) 63 | ] 64 | 65 | 66 | register_class(InstructorsCap_3_3) 67 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/weapon/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import WeaponBase 2 | from .....utils import import_all_modules 3 | 4 | 5 | import_all_modules(__file__, __name__) 6 | __all__ = ("WeaponBase",) 7 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/weapon/favonius.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from ....consts import SkillType, WeaponType 6 | 7 | from ....action import ChargeAction 8 | from ....event import SkillEndEventArguments 9 | from ....struct import Cost 10 | from .base import RoundEffectWeaponBase 11 | 12 | 13 | class FavoniusBase(RoundEffectWeaponBase): 14 | name: str 15 | cost: Cost = Cost(same_dice_number=3) 16 | version: str 17 | weapon_type: WeaponType 18 | max_usage_per_round: int = 1 19 | 20 | def event_handler_SKILL_END( 21 | self, event: SkillEndEventArguments, match: Any 22 | ) -> List[ChargeAction]: 23 | """ 24 | if self character use elemental skill, charge one more 25 | """ 26 | if self.position.not_satisfy( 27 | "both pidx=same cidx=same and source area=character and target area=skill", 28 | event.action.position, 29 | ): 30 | # not self character or not equipped 31 | return [] 32 | if event.action.skill_type != SkillType.ELEMENTAL_SKILL: 33 | # not elemental skill 34 | return [] 35 | if self.usage <= 0: 36 | # no usage 37 | return [] 38 | self.usage -= 1 39 | return [ 40 | ChargeAction( 41 | player_idx=self.position.player_idx, 42 | character_idx=self.position.character_idx, 43 | charge=1, 44 | ) 45 | ] 46 | 47 | 48 | class FavoniusSword_3_7(FavoniusBase): 49 | name: Literal["Favonius Sword"] 50 | version: Literal["3.7"] = "3.7" 51 | weapon_type: Literal[WeaponType.SWORD] = WeaponType.SWORD 52 | 53 | 54 | register_class(FavoniusSword_3_7) 55 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/weapon/other_bow.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from ....event import SkillEndEventArguments 6 | 7 | from ....modifiable_values import DamageIncreaseValue 8 | 9 | from ....action import CreateObjectAction 10 | 11 | from ....consts import ObjectPositionType, SkillType, WeaponType 12 | 13 | from ....struct import Cost, ObjectPosition 14 | from .base import RoundEffectWeaponBase, WeaponBase 15 | 16 | 17 | class AmosBow_3_7(RoundEffectWeaponBase): 18 | name: Literal["Amos' Bow"] 19 | cost: Cost = Cost(same_dice_number=3) 20 | version: Literal["3.7"] = "3.7" 21 | weapon_type: WeaponType = WeaponType.BOW 22 | max_usage_per_round: int = 1 23 | 24 | def value_modifier_DAMAGE_INCREASE( 25 | self, value: DamageIncreaseValue, match: Any, mode: Literal["TEST", "REAL"] 26 | ) -> DamageIncreaseValue: 27 | condition_satisfied = False 28 | if value.cost.total_dice_cost + value.cost.charge >= 5 and self.usage > 0: 29 | # have usage and costs satisfied 30 | self.damage_increase = 3 31 | condition_satisfied = True 32 | current_damage = value.damage 33 | super().value_modifier_DAMAGE_INCREASE(value, match, mode) 34 | if value.damage > current_damage: 35 | # value modified 36 | if condition_satisfied: 37 | # decrease usage 38 | assert mode == "REAL" 39 | self.usage -= 1 40 | self.damage_increase = 1 41 | return value 42 | 43 | 44 | class ElegyForTheEnd_3_7(WeaponBase): 45 | name: Literal["Elegy for the End"] 46 | cost: Cost = Cost(same_dice_number=3) 47 | version: Literal["3.7"] = "3.7" 48 | weapon_type: WeaponType = WeaponType.BOW 49 | 50 | def event_handler_SKILL_END( 51 | self, event: SkillEndEventArguments, match: Any 52 | ) -> List[CreateObjectAction]: 53 | """ 54 | If equipped and self use elemental burst, create a status 55 | """ 56 | if not self.position.check_position_valid( 57 | event.action.position, 58 | match, 59 | player_idx_same=True, 60 | character_idx_same=True, 61 | source_area=ObjectPositionType.CHARACTER, 62 | ): 63 | # not equipped or not self use skill 64 | return [] 65 | if event.action.skill_type != SkillType.ELEMENTAL_BURST: 66 | # not elemental burst 67 | return [] 68 | return [ 69 | CreateObjectAction( 70 | object_name="Millennial Movement: Farewell Song", 71 | object_position=ObjectPosition( 72 | player_idx=self.position.player_idx, 73 | area=ObjectPositionType.TEAM_STATUS, 74 | id=0, 75 | ), 76 | object_arguments={}, 77 | ) 78 | ] 79 | 80 | 81 | register_class(AmosBow_3_7 | ElegyForTheEnd_3_7) 82 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/weapon/other_catalyst.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from ....modifiable_values import DamageIncreaseValue 6 | 7 | from ....struct import Cost 8 | 9 | from ....consts import ElementalReactionType, ObjectPositionType, ObjectType, WeaponType 10 | from .base import RoundEffectWeaponBase 11 | 12 | 13 | class AThousandFloatingDreams_3_7(RoundEffectWeaponBase): 14 | name: Literal["A Thousand Floating Dreams"] 15 | type: Literal[ObjectType.WEAPON] = ObjectType.WEAPON 16 | version: Literal["3.7"] = "3.7" 17 | weapon_type: WeaponType = WeaponType.CATALYST 18 | 19 | cost: Cost = Cost(same_dice_number=3) 20 | max_usage_per_round: int = 2 21 | 22 | def value_modifier_DAMAGE_INCREASE( 23 | self, value: DamageIncreaseValue, match: Any, mode: Literal["TEST", "REAL"] 24 | ) -> DamageIncreaseValue: 25 | """ 26 | First +1 DMG if self character use skill. Then if this damage is 27 | our character use skill, and trigger element reaction, +1 DMG. 28 | """ 29 | # first +1 DMG 30 | super().value_modifier_DAMAGE_INCREASE(value, match, mode) 31 | 32 | # second element reaction +1 DMG 33 | if self.usage == 0: 34 | # no usage left 35 | return value 36 | if self.position.area != ObjectPositionType.CHARACTER: 37 | # not equipped 38 | return value 39 | if value.element_reaction == ElementalReactionType.NONE: 40 | # no elemental reaction 41 | return value 42 | if value.damage_from_element_reaction: 43 | # from elemental reaction 44 | return value 45 | if not self.position.check_position_valid( 46 | value.position, 47 | match, 48 | player_idx_same=True, 49 | target_area=ObjectPositionType.SKILL, 50 | ): 51 | # not self player use skill 52 | return value 53 | # modify damage 54 | assert mode == "REAL" 55 | value.damage += 1 56 | self.usage -= 1 57 | return value 58 | 59 | 60 | register_class(AThousandFloatingDreams_3_7) 61 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/weapon/other_claymore.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from ....modifiable_values import DamageIncreaseValue 6 | 7 | from ....action import CreateObjectAction 8 | 9 | from ....event import SkillEndEventArguments 10 | 11 | from .base import RoundEffectWeaponBase, WeaponBase 12 | 13 | from ....struct import Cost, ObjectPosition 14 | 15 | from ....consts import ObjectPositionType, ObjectType, WeaponType 16 | 17 | 18 | class WolfsGravestone_3_3(WeaponBase): 19 | name: Literal["Wolf's Gravestone"] 20 | cost: Cost = Cost(same_dice_number=3) 21 | version: Literal["3.3"] = "3.3" 22 | weapon_type: WeaponType = WeaponType.CLAYMORE 23 | 24 | def value_modifier_DAMAGE_INCREASE( 25 | self, value: DamageIncreaseValue, match: Any, mode: Literal["TEST", "REAL"] 26 | ) -> DamageIncreaseValue: 27 | assert value.target_position.area == ObjectPositionType.CHARACTER 28 | character = match.get_object(value.target_position) 29 | if character.hp <= 6: 30 | self.damage_increase = 3 31 | super().value_modifier_DAMAGE_INCREASE(value, match, mode) 32 | self.damage_increase = 1 33 | return value 34 | 35 | 36 | class TheBell_3_7(RoundEffectWeaponBase): 37 | name: Literal["The Bell"] 38 | type: Literal[ObjectType.WEAPON] = ObjectType.WEAPON 39 | version: Literal["3.7"] = "3.7" 40 | weapon_type: WeaponType = WeaponType.CLAYMORE 41 | 42 | cost: Cost = Cost(same_dice_number=3) 43 | max_usage_per_round: int = 1 44 | 45 | def event_handler_SKILL_END( 46 | self, event: SkillEndEventArguments, match: Any 47 | ) -> List[CreateObjectAction]: 48 | """ 49 | If self character use any skill, and have usage, create Rebellious 50 | Shield. 51 | """ 52 | if self.position.not_satisfy( 53 | "both pidx=same cidx=same and source area=character and target area=skill", 54 | event.action.position, 55 | ): 56 | # not self character use skill or not equipped 57 | return [] 58 | if self.usage == 0: 59 | # no usage 60 | return [] 61 | self.usage -= 1 62 | return [ 63 | CreateObjectAction( 64 | object_name="Rebellious Shield", 65 | object_position=ObjectPosition( 66 | player_idx=self.position.player_idx, 67 | area=ObjectPositionType.TEAM_STATUS, 68 | id=-1, 69 | ), 70 | object_arguments={}, 71 | ) 72 | ] 73 | 74 | 75 | register_class(WolfsGravestone_3_3 | TheBell_3_7) 76 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/weapon/other_sword.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from ....modifiable_values import DamageValue 6 | 7 | from ....action import MakeDamageAction 8 | 9 | from ....event import SkillEndEventArguments 10 | 11 | from ....consts import DamageElementalType, DamageType, ObjectPositionType, WeaponType 12 | 13 | from ....struct import Cost 14 | from .base import RoundEffectWeaponBase 15 | 16 | 17 | class AquilaFavonia_3_3(RoundEffectWeaponBase): 18 | name: Literal["Aquila Favonia"] 19 | cost: Cost = Cost(same_dice_number=3) 20 | version: Literal["3.3"] = "3.3" 21 | weapon_type: WeaponType = WeaponType.SWORD 22 | max_usage_per_round: int = 2 23 | 24 | def event_handler_SKILL_END( 25 | self, event: SkillEndEventArguments, match: Any 26 | ) -> List[MakeDamageAction]: 27 | """ 28 | If self is active and opposite use skill and has usage, heal 1 HP 29 | """ 30 | if not self.position.check_position_valid( 31 | event.action.position, 32 | match, 33 | player_idx_same=False, 34 | source_area=ObjectPositionType.CHARACTER, 35 | target_area=ObjectPositionType.SKILL, 36 | ): 37 | # not equipped or not opponent use skill 38 | return [] 39 | if ( 40 | self.position.character_idx 41 | != match.player_tables[self.position.player_idx].active_character_idx 42 | ): 43 | # not active 44 | return [] 45 | if self.usage == 0: 46 | # no usage 47 | return [] 48 | self.usage -= 1 49 | character = match.player_tables[self.position.player_idx].characters[ 50 | self.position.character_idx 51 | ] 52 | return [ 53 | MakeDamageAction( 54 | damage_value_list=[ 55 | DamageValue( 56 | position=self.position, 57 | damage_type=DamageType.HEAL, 58 | target_position=character.position, 59 | damage=-1, 60 | damage_elemental_type=DamageElementalType.HEAL, 61 | cost=self.cost.copy(), 62 | ) 63 | ], 64 | ) 65 | ] 66 | 67 | 68 | register_class(AquilaFavonia_3_3) 69 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/weapon/sacrificial.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from ....action import CreateDiceAction 6 | 7 | from ....event import SkillEndEventArguments 8 | from ....consts import ( 9 | ELEMENT_TO_DIE_COLOR, 10 | ElementType, 11 | ObjectPositionType, 12 | SkillType, 13 | WeaponType, 14 | ) 15 | from ....struct import Cost 16 | from .base import RoundEffectWeaponBase 17 | 18 | 19 | class SacrificialWeapons_3_3(RoundEffectWeaponBase): 20 | name: Literal[ 21 | "Sacrificial Fragments", 22 | "Sacrificial Greatsword", 23 | "Sacrificial Sword", 24 | "Sacrificial Bow", 25 | ] 26 | cost: Cost = Cost(same_dice_number=3) 27 | version: Literal["3.3"] = "3.3" 28 | weapon_type: WeaponType = WeaponType.OTHER 29 | max_usage_per_round: int = 1 30 | 31 | def __init__(self, *argv, **kwargs): 32 | super().__init__(*argv, **kwargs) 33 | if self.name == "Sacrificial Fragments": 34 | self.weapon_type = WeaponType.CATALYST 35 | elif self.name == "Sacrificial Bow": 36 | self.weapon_type = WeaponType.BOW 37 | elif self.name == "Sacrificial Sword": 38 | self.weapon_type = WeaponType.SWORD 39 | else: 40 | assert self.name == "Sacrificial Greatsword" 41 | self.weapon_type = WeaponType.CLAYMORE 42 | 43 | def event_handler_SKILL_END( 44 | self, event: SkillEndEventArguments, match: Any 45 | ) -> List[CreateDiceAction]: 46 | """ 47 | If self use elemental skill, create one corresponding dice 48 | """ 49 | if not ( 50 | self.position.area == ObjectPositionType.CHARACTER 51 | and event.action.position.player_idx == self.position.player_idx 52 | and event.action.position.character_idx == self.position.character_idx 53 | and event.action.skill_type == SkillType.ELEMENTAL_SKILL 54 | and self.usage > 0 55 | ): 56 | # not equipped or not self use elemental skill or no usage 57 | return [] 58 | self.usage -= 1 59 | character = match.player_tables[self.position.player_idx].characters[ 60 | self.position.character_idx 61 | ] 62 | ele_type: ElementType = character.element 63 | die_color = ELEMENT_TO_DIE_COLOR[ele_type] 64 | return [ 65 | CreateDiceAction( 66 | player_idx=self.position.player_idx, 67 | number=1, 68 | color=die_color, 69 | ) 70 | ] 71 | 72 | 73 | register_class(SacrificialWeapons_3_3) 74 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/weapon/skyward.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from ....consts import ObjectPositionType, SkillType, WeaponType 6 | from ....modifiable_values import DamageIncreaseValue 7 | from ....struct import Cost 8 | from .base import RoundEffectWeaponBase 9 | 10 | 11 | class SkywardBase(RoundEffectWeaponBase): 12 | name: str 13 | cost: Cost = Cost(same_dice_number=3) 14 | version: str 15 | weapon_type: WeaponType 16 | max_usage_per_round: int = 1 17 | 18 | def value_modifier_DAMAGE_INCREASE( 19 | self, value: DamageIncreaseValue, match: Any, mode: Literal["TEST", "REAL"] 20 | ) -> DamageIncreaseValue: 21 | if self.position.area != ObjectPositionType.CHARACTER: 22 | # not equipped 23 | return value 24 | super().value_modifier_DAMAGE_INCREASE(value, match, mode) 25 | if ( 26 | value.is_corresponding_character_use_damage_skill( 27 | self.position, match, SkillType.NORMAL_ATTACK 28 | ) 29 | and self.usage > 0 30 | ): 31 | # have usage and is normal attack 32 | assert mode == "REAL" 33 | self.usage -= 1 34 | value.damage += 1 35 | return value 36 | 37 | 38 | class SkywardAtlas_3_3(SkywardBase): 39 | name: Literal["Skyward Atlas"] 40 | version: Literal["3.3"] = "3.3" 41 | weapon_type: Literal[WeaponType.CATALYST] = WeaponType.CATALYST 42 | 43 | 44 | class SkywardHarp_3_3(SkywardBase): 45 | name: Literal["Skyward Harp"] 46 | version: Literal["3.3"] = "3.3" 47 | weapon_type: Literal[WeaponType.BOW] = WeaponType.BOW 48 | 49 | 50 | class SkywardSpine_3_3(SkywardBase): 51 | name: Literal["Skyward Spine"] 52 | version: Literal["3.3"] = "3.3" 53 | weapon_type: Literal[WeaponType.POLEARM] = WeaponType.POLEARM 54 | 55 | 56 | class SkywardBlade_3_7(SkywardBase): 57 | name: Literal["Skyward Blade"] 58 | version: Literal["3.7"] = "3.7" 59 | weapon_type: Literal[WeaponType.SWORD] = WeaponType.SWORD 60 | 61 | 62 | class SkywardPride_3_7(SkywardBase): 63 | name: Literal["Skyward Pride"] 64 | version: Literal["3.7"] = "3.7" 65 | weapon_type: Literal[WeaponType.CLAYMORE] = WeaponType.CLAYMORE 66 | 67 | 68 | register_class( 69 | SkywardAtlas_3_3 70 | | SkywardHarp_3_3 71 | | SkywardPride_3_7 72 | | SkywardSpine_3_3 73 | | SkywardBlade_3_7 74 | ) 75 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/weapon/sumeru_forge_weapon.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from ....action import CreateObjectAction, DrawCardAction 6 | 7 | from ....struct import Cost 8 | from .base import WeaponBase 9 | from ....consts import ObjectPositionType, WeaponType 10 | 11 | 12 | class FruitOfFulfillment_3_8(WeaponBase): 13 | name: Literal["Fruit of Fulfillment"] 14 | cost: Cost = Cost(any_dice_number=3) 15 | version: Literal["3.8"] = "3.8" 16 | weapon_type: WeaponType = WeaponType.CATALYST 17 | 18 | def equip(self, match: Any) -> List[DrawCardAction]: 19 | """ 20 | draw 2 cards 21 | """ 22 | return [ 23 | DrawCardAction( 24 | player_idx=self.position.player_idx, 25 | number=2, 26 | draw_if_filtered_not_enough=True, 27 | ) 28 | ] 29 | 30 | 31 | class KingsSquire_4_0(WeaponBase): 32 | name: Literal["King's Squire"] 33 | cost: Cost = Cost(same_dice_number=3) 34 | version: Literal["4.0"] = "4.0" 35 | weapon_type: WeaponType = WeaponType.BOW 36 | 37 | def equip(self, match: Any) -> List[CreateObjectAction]: 38 | """ 39 | attach status 40 | """ 41 | return [ 42 | CreateObjectAction( 43 | object_position=self.position.set_area( 44 | ObjectPositionType.CHARACTER_STATUS 45 | ), 46 | object_name=self.name, 47 | object_arguments={}, 48 | ) 49 | ] 50 | 51 | 52 | class Moonpiercer_4_1(KingsSquire_4_0): 53 | name: Literal["Moonpiercer"] 54 | version: Literal["4.1"] = "4.1" 55 | weapon_type: WeaponType = WeaponType.POLEARM 56 | 57 | 58 | register_class(FruitOfFulfillment_3_8 | KingsSquire_4_0 | Moonpiercer_4_1) 59 | -------------------------------------------------------------------------------- /src/lpsim/server/card/equipment/weapon/vanilla.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from ....consts import WeaponType 6 | from .base import WeaponBase 7 | from ....struct import Cost 8 | 9 | 10 | class VanillaWeapon_3_3(WeaponBase): 11 | name: Literal[ 12 | "Magic Guide", 13 | "Raven Bow", 14 | "Traveler's Handy Sword", 15 | "White Iron Greatsword", 16 | "White Tassel", 17 | ] 18 | version: Literal["3.3"] = "3.3" 19 | weapon_type: WeaponType = WeaponType.OTHER 20 | 21 | cost: Cost = Cost(same_dice_number=2) 22 | 23 | def __init__(self, *argv, **kwargs): 24 | super().__init__(*argv, **kwargs) 25 | if self.name == "Magic Guide": 26 | self.weapon_type = WeaponType.CATALYST 27 | elif self.name == "Raven Bow": 28 | self.weapon_type = WeaponType.BOW 29 | elif self.name == "Traveler's Handy Sword": 30 | self.weapon_type = WeaponType.SWORD 31 | elif self.name == "White Iron Greatsword": 32 | self.weapon_type = WeaponType.CLAYMORE 33 | else: 34 | assert self.name == "White Tassel" 35 | self.weapon_type = WeaponType.POLEARM 36 | 37 | 38 | register_class(VanillaWeapon_3_3) 39 | -------------------------------------------------------------------------------- /src/lpsim/server/card/event/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/card/support/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | from .base import SupportBase 3 | from .companions import CompanionBase 4 | from .items import ItemBase 5 | from .locations import LocationBase 6 | 7 | 8 | import_all_modules(__file__, __name__) 9 | __all__ = ("SupportBase", "CompanionBase", "ItemBase", "LocationBase") 10 | -------------------------------------------------------------------------------- /src/lpsim/server/character/__init__.py: -------------------------------------------------------------------------------- 1 | from ...utils import import_all_modules 2 | from .character_base import CharacterBase, TalentBase, SkillBase 3 | 4 | 5 | import_all_modules(__file__, __name__, exceptions={"template"}) 6 | __all__ = ("CharacterBase", "TalentBase", "SkillBase") 7 | -------------------------------------------------------------------------------- /src/lpsim/server/character/anemo/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/character/anemo/jean_4_2.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ..character_base import PhysicalNormalAttackBase 6 | 7 | from ...consts import DieColor 8 | 9 | from ...struct import Cost 10 | from .jean_3_3 import ( 11 | Jean_3_3, 12 | DandelionField_3_3, 13 | LandsOfDandelion_3_3, 14 | GaleBlade, 15 | DandelionBreeze as DB_3_3, 16 | ) 17 | 18 | 19 | class DandelionField_4_2(DandelionField_3_3): 20 | version: Literal["4.2"] = "4.2" 21 | damage: int = 1 22 | 23 | 24 | class LandsOfDandelion_4_2(LandsOfDandelion_3_3): 25 | version: Literal["4.2"] = "4.2" 26 | cost: Cost = Cost( 27 | elemental_dice_color=DieColor.ANEMO, elemental_dice_number=4, charge=2 28 | ) 29 | 30 | 31 | class DandelionBreeze(DB_3_3): 32 | version: Literal["4.2"] = "4.2" 33 | cost: Cost = Cost( 34 | elemental_dice_color=DieColor.ANEMO, elemental_dice_number=4, charge=2 35 | ) 36 | 37 | 38 | class Jean_4_2(Jean_3_3): 39 | version: Literal["4.2"] = "4.2" 40 | max_charge: int = 2 41 | skills: List[PhysicalNormalAttackBase | GaleBlade | DandelionBreeze] = [] 42 | 43 | def _init_skills(self) -> None: 44 | self.skills = [ 45 | PhysicalNormalAttackBase( 46 | name="Favonius Bladework", 47 | cost=PhysicalNormalAttackBase.get_cost(self.element), 48 | ), 49 | GaleBlade(), 50 | DandelionBreeze(), 51 | ] 52 | 53 | 54 | register_class(Jean_4_2 | DandelionField_4_2 | LandsOfDandelion_4_2) 55 | -------------------------------------------------------------------------------- /src/lpsim/server/character/anemo/maguu_kenki_3_3.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ..character_base import PhysicalNormalAttackBase 6 | 7 | from ...action import Actions 8 | 9 | from .maguu_kenki_3_4 import MaguuKenki_3_4 as MK_3_4 10 | from .maguu_kenki_3_4 import BlusteringBlade as BB_3_4 11 | from .maguu_kenki_3_4 import FrostyAssault as FA_3_4 12 | from .maguu_kenki_3_4 import PseudoTenguSweeper 13 | 14 | 15 | class BlusteringBlade(BB_3_4): 16 | damage: int = 1 17 | 18 | def get_actions(self, match: Any) -> List[Actions]: 19 | """ 20 | gather two actions 21 | """ 22 | return super(BB_3_4, self).get_actions(match) + [ 23 | self.create_summon("Shadowsword: Lone Gale"), 24 | ] 25 | 26 | 27 | class FrostyAssault(FA_3_4): 28 | damage: int = 1 29 | 30 | def get_actions(self, match: Any) -> List[Actions]: 31 | """ 32 | gather two actions 33 | """ 34 | return super(FA_3_4, self).get_actions(match) + [ 35 | self.create_summon("Shadowsword: Galloping Frost"), 36 | ] 37 | 38 | 39 | class MaguuKenki_3_3(MK_3_4): 40 | version: Literal["3.3"] = "3.3" 41 | skills: List[ 42 | PhysicalNormalAttackBase | BlusteringBlade | FrostyAssault | PseudoTenguSweeper 43 | ] = [] 44 | 45 | def _init_skills(self) -> None: 46 | self.skills = [ 47 | PhysicalNormalAttackBase( 48 | name="Ichimonji", 49 | cost=PhysicalNormalAttackBase.get_cost(self.element), 50 | ), 51 | BlusteringBlade(), 52 | FrostyAssault(), 53 | PseudoTenguSweeper(), 54 | ] 55 | 56 | 57 | register_class(MaguuKenki_3_3) 58 | -------------------------------------------------------------------------------- /src/lpsim/server/character/cryo/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/character/cryo/eula_3_5.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from .eula_3_8 import Eula_3_8 as E_3_8 6 | from .eula_3_8 import IcetideVortex as IV_3_8 7 | from .eula_3_8 import GlacialIllumination as GI_3_8 8 | from .eula_3_8 import LightfallSword_3_8 as LS_3_8 9 | from .eula_3_8 import FavoniusBladeworkEdel 10 | 11 | 12 | class LightfallSword_3_5(LS_3_8): 13 | version: Literal["3.5"] = "3.5" 14 | damage: int = 2 15 | 16 | 17 | class IcetideVortex(IV_3_8): 18 | version: Literal["3.5"] = "3.5" 19 | 20 | 21 | class GlacialIllumination(GI_3_8): 22 | version: Literal["3.5"] = "3.5" 23 | 24 | 25 | class Eula_3_5(E_3_8): 26 | version: Literal["3.5"] = "3.5" 27 | skills: List[FavoniusBladeworkEdel | IcetideVortex | GlacialIllumination] = [] 28 | 29 | def _init_skills(self) -> None: 30 | self.skills = [FavoniusBladeworkEdel(), IcetideVortex(), GlacialIllumination()] 31 | 32 | 33 | register_class(Eula_3_5 | LightfallSword_3_5) 34 | -------------------------------------------------------------------------------- /src/lpsim/server/character/cryo/fatui_cryo_cicin_mage_3_7.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | from .fatui_cryo_cicin_mage_4_1 import FatuiCryoCicinMage_4_1 as FCC_4_1 5 | from .fatui_cryo_cicin_mage_4_1 import CryoCicins_4_1 as CC_4_1 6 | from .fatui_cryo_cicin_mage_4_1 import MistySummons as MS_4_1 7 | from .fatui_cryo_cicin_mage_4_1 import CicinIcicle, BlizzardBranchBlossom 8 | 9 | 10 | class CryoCicins_3_7(CC_4_1): 11 | version: Literal["3.7"] = "3.7" 12 | decrease_only_self_damage: bool = False 13 | 14 | 15 | class MistySummons(MS_4_1): 16 | version: Literal["3.7"] = "3.7" 17 | 18 | 19 | class FatuiCryoCicinMage_3_7(FCC_4_1): 20 | version: Literal["3.7"] = "3.7" 21 | skills: List[CicinIcicle | MistySummons | BlizzardBranchBlossom] = [] 22 | 23 | def _init_skills(self) -> None: 24 | self.skills = [ 25 | CicinIcicle(), 26 | MistySummons(), 27 | BlizzardBranchBlossom(), 28 | ] 29 | 30 | 31 | register_class(FatuiCryoCicinMage_3_7 | CryoCicins_3_7) 32 | -------------------------------------------------------------------------------- /src/lpsim/server/character/cryo/ganyu_3_3.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, List 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ..character_base import PhysicalNormalAttackBase 6 | from ...consts import DieColor 7 | from ...struct import Cost 8 | from .ganyu_3_7 import Ganyu_3_7 as G_3_7 9 | from .ganyu_3_7 import UndividedHeart_3_7 as UH_3_7 10 | from .ganyu_3_7 import CelestialShower as CS_3_7 11 | from .ganyu_3_7 import FrostflakeArrow, TrailOftheQilin 12 | 13 | 14 | class CelestialShower(CS_3_7): 15 | damage: int = 1 16 | cost: Cost = Cost( 17 | elemental_dice_color=DieColor.CRYO, elemental_dice_number=3, charge=2 18 | ) 19 | 20 | 21 | class UndividedHeart_3_3(UH_3_7): 22 | version: Literal["3.3"] = "3.3" 23 | 24 | 25 | class Ganyu_3_3(G_3_7): 26 | version: Literal["3.3"] = "3.3" 27 | max_charge: int = 2 28 | skills: List[ 29 | PhysicalNormalAttackBase | TrailOftheQilin | FrostflakeArrow | CelestialShower 30 | ] = [] 31 | 32 | def _init_skills(self) -> None: 33 | self.skills = [ 34 | PhysicalNormalAttackBase( 35 | name="Liutian Archery", 36 | cost=PhysicalNormalAttackBase.get_cost(self.element), 37 | ), 38 | TrailOftheQilin(), 39 | FrostflakeArrow(), 40 | CelestialShower(), 41 | ] 42 | 43 | 44 | register_class(Ganyu_3_3 | UndividedHeart_3_3) 45 | -------------------------------------------------------------------------------- /src/lpsim/server/character/cryo/shenhe_4_2.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ..character_base import PhysicalNormalAttackBase 6 | from .shenhe_3_7 import ( 7 | Shenhe_3_7, 8 | SpringSpiritSummoning as SS_3_7, 9 | DivineMaidensDeliverance, 10 | ) 11 | 12 | 13 | class SpringSpiritSummoning(SS_3_7): 14 | max_usage: int = 2 15 | 16 | 17 | class Shenhe_4_2(Shenhe_3_7): 18 | version: Literal["4.2"] = "4.2" 19 | skills: List[ 20 | PhysicalNormalAttackBase | SpringSpiritSummoning | DivineMaidensDeliverance 21 | ] = [] 22 | 23 | def _init_skills(self) -> None: 24 | self.skills = [ 25 | PhysicalNormalAttackBase( 26 | name="Dawnstar Piercer", 27 | cost=PhysicalNormalAttackBase.get_cost(self.element), 28 | ), 29 | SpringSpiritSummoning(), 30 | DivineMaidensDeliverance(), 31 | ] 32 | 33 | 34 | register_class(Shenhe_4_2) 35 | -------------------------------------------------------------------------------- /src/lpsim/server/character/custom/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/character/custom/mob_1_0.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | from pydantic import validator 3 | 4 | from ....utils.class_registry import register_class 5 | from ...consts import ( 6 | ElementType, 7 | FactionType, 8 | WeaponType, 9 | DamageElementalType, 10 | ELEMENT_TO_DAMAGE_TYPE, 11 | ) 12 | from ..character_base import ( 13 | PhysicalNormalAttackBase, 14 | ElementalSkillBase, 15 | ElementalBurstBase, 16 | CharacterBase, 17 | ) 18 | 19 | 20 | class Mob_1_0(CharacterBase): 21 | """ 22 | Mobs. They cannot carry weapons in default. Their normal 23 | attacks are 2 physical DMG, elemental skills are 3 element DMG, 24 | elemental bursts are 2 charges, 3 cost, and 5 element DMG. 25 | Their names should be BlablaMob, here Blabla should be one of elemental 26 | types. 27 | """ 28 | 29 | name: Literal[ 30 | "CryoMob", 31 | "HydroMob", 32 | "PyroMob", 33 | "ElectroMob", 34 | "GeoMob", 35 | "DendroMob", 36 | "AnemoMob", 37 | ] 38 | version: Literal["1.0"] = "1.0" 39 | element: ElementType = ElementType.NONE 40 | max_hp: int = 10 41 | max_charge: int = 2 42 | skills: List[ 43 | PhysicalNormalAttackBase | ElementalSkillBase | ElementalBurstBase 44 | ] = [] 45 | faction: List[FactionType] = [] 46 | weapon_type: WeaponType = WeaponType.OTHER 47 | 48 | @validator("element") 49 | def element_fits_name(cls, v: ElementType, values, **kwargs): 50 | """ 51 | Check if element type fits name. 52 | """ 53 | if "name" not in values: 54 | raise AssertionError("Name not found.") 55 | type_in_name: str = values["name"][:-3].upper() 56 | element_type: str = v.value 57 | if type_in_name != element_type: 58 | raise ValueError( 59 | f"Element type {element_type} does not fit " f"name {type_in_name}." 60 | ) 61 | return v 62 | 63 | def _init_skills(self): 64 | if self.element == ElementType.NONE: 65 | # set element by name 66 | element_name = self.name[:-3].upper() 67 | self.element = ElementType(element_name) 68 | element_name = self.element.value.lower() 69 | element_name = element_name[0].upper() + element_name[1:] 70 | normal_attack = PhysicalNormalAttackBase( 71 | name="Physical Normal Attack", 72 | damage_type=DamageElementalType.PHYSICAL, 73 | cost=PhysicalNormalAttackBase.get_cost(self.element), 74 | ) 75 | elemental_skill = ElementalSkillBase( 76 | name=f"{element_name} Elemental Skill", 77 | damage_type=ELEMENT_TO_DAMAGE_TYPE[self.element], 78 | cost=ElementalSkillBase.get_cost(self.element), 79 | ) 80 | elemental_burst = ElementalBurstBase( 81 | name=f"{element_name} Elemental Burst", 82 | damage_type=ELEMENT_TO_DAMAGE_TYPE[self.element], 83 | cost=ElementalBurstBase.get_cost(self.element, 3, 2), 84 | damage=5, 85 | ) 86 | self.skills = [normal_attack, elemental_skill, elemental_burst] 87 | 88 | 89 | register_class(Mob_1_0) 90 | -------------------------------------------------------------------------------- /src/lpsim/server/character/custom/mob_mage_1_0.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, List 2 | from pydantic import validator 3 | 4 | from ....utils.class_registry import register_class 5 | from ...consts import ElementType, FactionType, WeaponType, ELEMENT_TO_DAMAGE_TYPE 6 | from .mob_1_0 import Mob_1_0 7 | from ..character_base import ( 8 | ElementalNormalAttackBase, 9 | ElementalSkillBase, 10 | ElementalBurstBase, 11 | ) 12 | 13 | 14 | class MobMage_1_0(Mob_1_0): 15 | """ 16 | Mob mages. They cannot carry weapons in default. Their normal 17 | attacks are 2 physical DMG, elemental skills are 3 element DMG, 18 | elemental bursts are 2 charges, 3 cost, and 5 element DMG. 19 | Their names should be BlablaMob, here Blabla should be one of elemental 20 | types. 21 | """ 22 | 23 | name: Literal[ 24 | "CryoMobMage", 25 | "HydroMobMage", 26 | "PyroMobMage", 27 | "ElectroMobMage", 28 | "GeoMobMage", 29 | "DendroMobMage", 30 | "AnemoMobMage", 31 | ] 32 | element: ElementType = ElementType.NONE 33 | max_hp: int = 10 34 | max_charge: int = 2 35 | skills: List[ 36 | ElementalNormalAttackBase | ElementalSkillBase | ElementalBurstBase 37 | ] = [] 38 | faction: List[FactionType] = [] 39 | weapon_type: WeaponType = WeaponType.CATALYST 40 | 41 | @validator("element") 42 | def element_fits_name(cls, v: ElementType, values, **kwargs): 43 | """ 44 | Check if element type fits name. 45 | """ 46 | if "name" not in values: 47 | raise AssertionError("Name not found.") 48 | type_in_name: str = values["name"][:-7].upper() 49 | element_type: str = v.value 50 | if type_in_name != element_type: 51 | raise ValueError( 52 | f"Element type {element_type} does not fit " f"name {type_in_name}." 53 | ) 54 | return v 55 | 56 | def _init_skills(self): 57 | if self.element == ElementType.NONE: 58 | # set element by name 59 | element_name = self.name[:-7].upper() 60 | self.element = ElementType(element_name) 61 | element_name = self.element.value.lower() 62 | element_name = element_name[0].upper() + element_name[1:] 63 | normal_attack = ElementalNormalAttackBase( 64 | name=f"{element_name} Normal Attack", 65 | damage_type=ELEMENT_TO_DAMAGE_TYPE[self.element], 66 | cost=ElementalNormalAttackBase.get_cost(self.element), 67 | ) 68 | elemental_skill = ElementalSkillBase( 69 | name=f"{element_name} Elemental Skill", 70 | damage_type=ELEMENT_TO_DAMAGE_TYPE[self.element], 71 | cost=ElementalSkillBase.get_cost(self.element), 72 | ) 73 | elemental_burst = ElementalBurstBase( 74 | name=f"{element_name} Elemental Burst", 75 | damage_type=ELEMENT_TO_DAMAGE_TYPE[self.element], 76 | cost=ElementalBurstBase.get_cost(self.element, 3, 2), 77 | damage=5, 78 | ) 79 | self.skills = [normal_attack, elemental_skill, elemental_burst] 80 | 81 | 82 | register_class(MobMage_1_0) 83 | -------------------------------------------------------------------------------- /src/lpsim/server/character/custom/physical_mob_1_0.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | from ...consts import ( 5 | ElementType, 6 | FactionType, 7 | WeaponType, 8 | DamageElementalType, 9 | ) 10 | from ..character_base import ( 11 | PhysicalNormalAttackBase, 12 | ElementalSkillBase, 13 | ElementalBurstBase, 14 | CharacterBase, 15 | ) 16 | 17 | 18 | class PhysicalMob_1_0(CharacterBase): 19 | """ 20 | Physical mobs. They cannot carry weapons in default. Their normal 21 | attacks are 2 physical DMG, elemental skills are 3 physical DMG, 22 | elemental bursts are 2 charges, 3 cost, and 5 physical DMG. 23 | Their elemeny type is only used to decide dice color when using skills. 24 | """ 25 | 26 | name: Literal["PhysicalMob"] 27 | version: Literal["1.0"] = "1.0" 28 | element: ElementType = ElementType.PYRO 29 | max_hp: int = 10 30 | max_charge: int = 2 31 | skills: List[ 32 | PhysicalNormalAttackBase | ElementalSkillBase | ElementalBurstBase 33 | ] = [] 34 | faction: List[FactionType] = [] 35 | weapon_type: WeaponType = WeaponType.OTHER 36 | 37 | def __init__(self, **kwargs): 38 | super().__init__(**kwargs) # type: ignore 39 | 40 | def _init_skills(self): 41 | element_name = self.element.value.lower() 42 | element_name = element_name[0].upper() + element_name[1:] 43 | normal_attack = PhysicalNormalAttackBase( 44 | name="Physical Normal Attack", 45 | damage_type=DamageElementalType.PHYSICAL, 46 | cost=PhysicalNormalAttackBase.get_cost(self.element), 47 | ) 48 | elemental_skill = ElementalSkillBase( 49 | name="Physical Skill", 50 | damage_type=DamageElementalType.PHYSICAL, 51 | cost=ElementalSkillBase.get_cost(self.element), 52 | ) 53 | elemental_burst = ElementalBurstBase( 54 | name="Physical Burst", 55 | damage_type=DamageElementalType.PHYSICAL, 56 | cost=ElementalBurstBase.get_cost(self.element, 3, 2), 57 | damage=5, 58 | ) 59 | self.skills = [normal_attack, elemental_skill, elemental_burst] 60 | 61 | 62 | register_class(PhysicalMob_1_0) 63 | -------------------------------------------------------------------------------- /src/lpsim/server/character/dendro/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/character/electro/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/character/electro/beidou_3_4.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ..character_base import PhysicalNormalAttackBase 6 | from ...struct import Cost 7 | from ...consts import DieColor 8 | from .beidou_3_8 import Beidou_3_8 as Beidou_3_8, LightningStorm_4_2 9 | from .beidou_3_8 import Wavestrider as Wavestrider_3_8 10 | from .beidou_3_8 import Stormbreaker as Stormbreaker_3_8 11 | from .beidou_3_8 import Tidecaller 12 | 13 | 14 | class Wavestrider(Wavestrider_3_8): 15 | damage: int = 2 16 | 17 | 18 | class Stormbreaker(Stormbreaker_3_8): 19 | damage: int = 3 20 | cost: Cost = Cost( 21 | elemental_dice_color=DieColor.ELECTRO, elemental_dice_number=4, charge=3 22 | ) 23 | 24 | 25 | class LightningStorm_3_4(LightningStorm_4_2): 26 | version: Literal["3.4"] = "3.4" 27 | need_to_activate: bool = True 28 | 29 | 30 | class Beidou_3_4(Beidou_3_8): 31 | version: Literal["3.4"] = "3.4" 32 | skills: List[ 33 | PhysicalNormalAttackBase | Tidecaller | Wavestrider | Stormbreaker 34 | ] = [] 35 | 36 | def _init_skills(self) -> None: 37 | self.skills = [ 38 | PhysicalNormalAttackBase( 39 | name="Oceanborne", 40 | cost=PhysicalNormalAttackBase.get_cost(self.element), 41 | ), 42 | Tidecaller(), 43 | Wavestrider(), 44 | Stormbreaker(), 45 | ] 46 | 47 | 48 | register_class(Beidou_3_4 | LightningStorm_3_4) 49 | -------------------------------------------------------------------------------- /src/lpsim/server/character/electro/razor_3_3.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ..character_base import PhysicalNormalAttackBase 4 | 5 | from ....utils.class_registry import register_class 6 | 7 | from ...consts import DieColor 8 | from ...struct import Cost 9 | from .razor_3_8 import Awakening_4_2, Razor_3_8 as R_3_8 10 | from .razor_3_8 import LightningFang as LF_3_8 11 | from .razor_3_8 import ClawAndThunder 12 | 13 | 14 | class LightningFang(LF_3_8): 15 | damage: int = 5 16 | cost: Cost = Cost( 17 | elemental_dice_color=DieColor.ELECTRO, elemental_dice_number=3, charge=3 18 | ) 19 | 20 | 21 | class Awakening_3_3(Awakening_4_2): 22 | version: Literal["3.3"] = "3.3" 23 | cost: Cost = Cost(elemental_dice_color=DieColor.ELECTRO, elemental_dice_number=4) 24 | usage: int = 999 25 | max_usage: int = 999 26 | 27 | 28 | class Razor_3_3(R_3_8): 29 | version: Literal["3.3"] = "3.3" 30 | skills: List[PhysicalNormalAttackBase | ClawAndThunder | LightningFang] = [] 31 | max_charge: int = 3 32 | 33 | def _init_skills(self) -> None: 34 | self.skills = [ 35 | PhysicalNormalAttackBase( 36 | name="Steel Fang", 37 | cost=PhysicalNormalAttackBase.get_cost(self.element), 38 | ), 39 | ClawAndThunder(), 40 | LightningFang(), 41 | ] 42 | 43 | 44 | register_class(Razor_3_3 | Awakening_3_3) 45 | -------------------------------------------------------------------------------- /src/lpsim/server/character/geo/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/character/geo/arataki_itto_4_2.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | from .arataki_itto_3_6 import ( 5 | AratakiItto_3_6, 6 | FightClubLegend, 7 | MasatsuZetsugiAkaushiBurst, 8 | RoyalDescentBeholdIttoTheEvil as RDBITE_3_6, 9 | ) 10 | 11 | 12 | class RoyalDescentBeholdIttoTheEvil(RDBITE_3_6): 13 | damage: int = 4 14 | version: Literal["4.2"] = "4.2" 15 | 16 | 17 | class AratakiItto_4_2(AratakiItto_3_6): 18 | version: Literal["4.2"] = "4.2" 19 | skills: List[ 20 | FightClubLegend | MasatsuZetsugiAkaushiBurst | RoyalDescentBeholdIttoTheEvil 21 | ] = [] 22 | 23 | def _init_skills(self) -> None: 24 | self.skills = [ 25 | FightClubLegend(), 26 | MasatsuZetsugiAkaushiBurst(), 27 | RoyalDescentBeholdIttoTheEvil(), 28 | ] 29 | 30 | 31 | register_class(AratakiItto_4_2) 32 | -------------------------------------------------------------------------------- /src/lpsim/server/character/geo/noelle_3_3.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ...action import Actions 6 | from ...struct import Cost 7 | 8 | from ...consts import ( 9 | DamageElementalType, 10 | DieColor, 11 | ElementType, 12 | FactionType, 13 | WeaponType, 14 | ) 15 | from ..character_base import ( 16 | ElementalBurstBase, 17 | ElementalSkillBase, 18 | CharacterBase, 19 | PhysicalNormalAttackBase, 20 | SkillTalent, 21 | ) 22 | 23 | 24 | class Breastplate(ElementalSkillBase): 25 | name: Literal["Breastplate"] = "Breastplate" 26 | damage: int = 1 27 | damage_type: DamageElementalType = DamageElementalType.GEO 28 | cost: Cost = Cost(elemental_dice_number=3, elemental_dice_color=DieColor.GEO) 29 | 30 | def get_actions(self, match: Any) -> List[Actions]: 31 | return super().get_actions(match) + [self.create_team_status("Full Plate")] 32 | 33 | 34 | class SweepingTime(ElementalBurstBase): 35 | name: Literal["Sweeping Time"] = "Sweeping Time" 36 | damage: int = 4 37 | damage_type: DamageElementalType = DamageElementalType.GEO 38 | cost: Cost = Cost( 39 | elemental_dice_number=4, elemental_dice_color=DieColor.GEO, charge=2 40 | ) 41 | 42 | def get_actions(self, match: Any) -> List[Actions]: 43 | return super().get_actions(match) + [ 44 | self.create_character_status("Sweeping Time") 45 | ] 46 | 47 | 48 | class IGotYourBack_3_3(SkillTalent): 49 | name: Literal["I Got Your Back"] 50 | version: Literal["3.3"] = "3.3" 51 | character_name: Literal["Noelle"] = "Noelle" 52 | cost: Cost = Cost(elemental_dice_number=3, elemental_dice_color=DieColor.GEO) 53 | skill: Literal["Breastplate"] = "Breastplate" 54 | 55 | 56 | class Noelle_3_3(CharacterBase): 57 | name: Literal["Noelle"] 58 | version: Literal["3.3"] = "3.3" 59 | element: ElementType = ElementType.GEO 60 | max_hp: int = 10 61 | max_charge: int = 2 62 | skills: List[PhysicalNormalAttackBase | Breastplate | SweepingTime] = [] 63 | faction: List[FactionType] = [FactionType.MONDSTADT] 64 | weapon_type: WeaponType = WeaponType.CLAYMORE 65 | 66 | def _init_skills(self) -> None: 67 | self.skills = [ 68 | PhysicalNormalAttackBase( 69 | name="Favonius Bladework - Maid", 70 | cost=PhysicalNormalAttackBase.get_cost( 71 | ElementType.GEO, 72 | ), 73 | ), 74 | Breastplate(), 75 | SweepingTime(), 76 | ] 77 | 78 | 79 | register_class(Noelle_3_3 | IGotYourBack_3_3) 80 | -------------------------------------------------------------------------------- /src/lpsim/server/character/hydro/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/character/hydro/kamisato_ayato_3_6.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from ...action import Actions 4 | 5 | from ....utils.class_registry import register_class 6 | 7 | from ..character_base import ElementalSkillBase, PhysicalNormalAttackBase 8 | from ...struct import Cost 9 | from ...consts import DamageElementalType, DieColor 10 | from .kamisato_ayato_4_1 import KamisatoAyato_4_1 as KA_4_1 11 | from .kamisato_ayato_4_1 import KamisatoArtSuiyuu as KAS_4_1 12 | 13 | 14 | class KamisatoArtSuiyuu(KAS_4_1): 15 | damage: int = 3 16 | cost: Cost = Cost( 17 | elemental_dice_color=DieColor.HYDRO, elemental_dice_number=3, charge=3 18 | ) 19 | 20 | 21 | class KamisatoArtKyouka(ElementalSkillBase): 22 | name: Literal["Kamisato Art: Kyouka"] = "Kamisato Art: Kyouka" 23 | damage: int = 2 24 | damage_type: DamageElementalType = DamageElementalType.HYDRO 25 | cost: Cost = Cost(elemental_dice_color=DieColor.HYDRO, elemental_dice_number=3) 26 | 27 | def get_actions(self, match: Any) -> List[Actions]: 28 | """ 29 | Attack and create object 30 | """ 31 | return super().get_actions(match) + [ 32 | self.create_character_status("Takimeguri Kanka"), 33 | ] 34 | 35 | 36 | class KamisatoAyato_3_6(KA_4_1): 37 | version: Literal["3.6"] = "3.6" 38 | max_charge: int = 3 39 | skills: List[PhysicalNormalAttackBase | KamisatoArtKyouka | KamisatoArtSuiyuu] = [] 40 | 41 | def _init_skills(self) -> None: 42 | self.skills = [ 43 | PhysicalNormalAttackBase( 44 | name="Kamisato Art: Marobashi", 45 | cost=PhysicalNormalAttackBase.get_cost(self.element), 46 | ), 47 | KamisatoArtKyouka(), 48 | KamisatoArtSuiyuu(), 49 | ] 50 | 51 | 52 | register_class(KamisatoAyato_3_6) 53 | -------------------------------------------------------------------------------- /src/lpsim/server/character/hydro/mirror_maiden_3_3.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ...consts import ELEMENT_TO_DAMAGE_TYPE 6 | 7 | from ..character_base import ElementalBurstBase, ElementalNormalAttackBase 8 | from .mirror_maiden_3_7 import MirrorMaiden_3_7 as MM_3_7 9 | from .mirror_maiden_3_7 import InfluxBlast as IB_3_7 10 | 11 | 12 | class InfluxBlast(IB_3_7): 13 | damage: int = 3 14 | 15 | 16 | class MirrorMaiden_3_3(MM_3_7): 17 | version: Literal["3.3"] = "3.3" 18 | skills: List[ElementalNormalAttackBase | InfluxBlast | ElementalBurstBase] = [] 19 | 20 | def _init_skills(self) -> None: 21 | self.skills = [ 22 | ElementalNormalAttackBase( 23 | name="Water Ball", 24 | damage_type=ELEMENT_TO_DAMAGE_TYPE[self.element], 25 | cost=ElementalNormalAttackBase.get_cost(self.element), 26 | ), 27 | InfluxBlast(), 28 | ElementalBurstBase( 29 | name="Rippled Reflection", 30 | damage=5, 31 | damage_type=ELEMENT_TO_DAMAGE_TYPE[self.element], 32 | cost=ElementalBurstBase.get_cost(self.element, 3, 2), 33 | ), 34 | ] 35 | 36 | 37 | register_class(MirrorMaiden_3_3) 38 | -------------------------------------------------------------------------------- /src/lpsim/server/character/hydro/rhodeia_4_2.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ...consts import ELEMENT_TO_DAMAGE_TYPE 4 | 5 | from ....utils.class_registry import register_class 6 | 7 | from ..character_base import ElementalNormalAttackBase 8 | from .rhodeia_3_3 import ( 9 | RhodeiaOfLoch_3_3, 10 | RhodeiaElementSkill, 11 | TideAndTorrent as TAT_3_3, 12 | ) 13 | 14 | 15 | class TideAndTorrent(TAT_3_3): 16 | damage: int = 4 17 | damage_per_summon: int = 1 18 | 19 | 20 | class RhodeiaOfLoch_4_2(RhodeiaOfLoch_3_3): 21 | version: Literal["4.2"] = "4.2" 22 | skills: List[ElementalNormalAttackBase | RhodeiaElementSkill | TideAndTorrent] = [] 23 | 24 | def _init_skills(self) -> None: 25 | self.skills = [ 26 | ElementalNormalAttackBase( 27 | name="Surge", 28 | damage_type=ELEMENT_TO_DAMAGE_TYPE[self.element], 29 | cost=ElementalNormalAttackBase.get_cost(self.element), 30 | ), 31 | RhodeiaElementSkill( 32 | name="Oceanid Mimic Summoning", 33 | ), 34 | RhodeiaElementSkill( 35 | name="The Myriad Wilds", 36 | ), 37 | TideAndTorrent(), 38 | ] 39 | 40 | 41 | register_class(RhodeiaOfLoch_4_2) 42 | -------------------------------------------------------------------------------- /src/lpsim/server/character/hydro/sangonomiya_kokomi_3_5.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ...consts import ELEMENT_TO_DAMAGE_TYPE, DieColor 6 | 7 | from ...struct import Cost 8 | from ...action import Actions 9 | from ..character_base import ElementalNormalAttackBase, SkillTalent 10 | from .sangonomiya_kokomi_3_6 import SangonomiyaKokomi_3_6 as SK_3_6 11 | from .sangonomiya_kokomi_3_6 import NereidsAscension as NA_3_6 12 | from .sangonomiya_kokomi_3_6 import KuragesOath 13 | 14 | 15 | class NereidsAscension(NA_3_6): 16 | damage: int = 3 17 | 18 | def get_actions(self, match: Any) -> List[Actions]: 19 | """ 20 | No healing 21 | """ 22 | return super(NA_3_6, self).get_actions(match) + [ 23 | self.create_character_status("Ceremonial Garment"), 24 | ] 25 | 26 | 27 | class TamakushiCasket_3_5(SkillTalent): 28 | name: Literal["Tamakushi Casket"] 29 | version: Literal["3.5"] = "3.5" 30 | character_name: Literal["Sangonomiya Kokomi"] = "Sangonomiya Kokomi" 31 | cost: Cost = Cost( 32 | elemental_dice_color=DieColor.HYDRO, elemental_dice_number=3, charge=2 33 | ) 34 | skill: Literal["Nereid's Ascension"] = "Nereid's Ascension" 35 | 36 | 37 | class SangonomiyaKokomi_3_5(SK_3_6): 38 | version: Literal["3.5"] = "3.5" 39 | skills: List[ElementalNormalAttackBase | KuragesOath | NereidsAscension] = [] 40 | 41 | def _init_skills(self) -> None: 42 | self.skills = [ 43 | ElementalNormalAttackBase( 44 | name="The Shape of Water", 45 | damage_type=ELEMENT_TO_DAMAGE_TYPE[self.element], 46 | cost=ElementalNormalAttackBase.get_cost(self.element), 47 | ), 48 | KuragesOath(), 49 | NereidsAscension(), 50 | ] 51 | 52 | 53 | register_class(SangonomiyaKokomi_3_5 | TamakushiCasket_3_5) 54 | -------------------------------------------------------------------------------- /src/lpsim/server/character/hydro/tartaglia_3_7.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ..character_base import ElementalSkillBase, PhysicalNormalAttackBase 6 | 7 | from ...action import Actions 8 | 9 | from ...consts import DamageElementalType, DieColor 10 | 11 | from ...struct import Cost 12 | 13 | from .tartaglia_4_1 import ( 14 | HavocObliteration as HO_4_1, 15 | TideWithholder as TW_4_1, 16 | Tartaglia_4_1 as Tartaglia_4_1, 17 | AbyssalMayhemHydrospout_4_1 as AMH_4_1, 18 | ) 19 | 20 | 21 | # Skills 22 | 23 | 24 | class FoulLegacyRagingTide(ElementalSkillBase): 25 | name: Literal["Foul Legacy: Raging Tide"] = "Foul Legacy: Raging Tide" 26 | version: Literal["3.7"] = "3.7" 27 | damage: int = 2 28 | damage_type: DamageElementalType = DamageElementalType.HYDRO 29 | cost: Cost = Cost(elemental_dice_color=DieColor.HYDRO, elemental_dice_number=3) 30 | 31 | def get_actions(self, match: Any) -> List[Actions]: 32 | """ 33 | create object then attack 34 | """ 35 | return [ 36 | # Tartaglia need to create the status first, otherwise cannot 37 | # receive the damage increase 38 | self.create_character_status("Melee Stance", {"version": self.version}), 39 | self.attack_opposite_active(match, self.damage, self.damage_type), 40 | self.charge_self(1), 41 | ] 42 | 43 | 44 | class HavocObliteration(HO_4_1): 45 | name: Literal["Havoc: Obliteration"] = "Havoc: Obliteration" 46 | version: Literal["3.7"] = "3.7" 47 | ranged_damage: int = 4 48 | 49 | 50 | class TideWithholder(TW_4_1): 51 | version: Literal["3.7"] = "3.7" 52 | 53 | 54 | # Talents 55 | 56 | 57 | class AbyssalMayhemHydrospout_3_7(AMH_4_1): 58 | version: Literal["3.7"] = "3.7" 59 | only_active_character: bool = False 60 | cost: Cost = Cost( 61 | elemental_dice_color=DieColor.HYDRO, 62 | elemental_dice_number=4, 63 | ) 64 | 65 | 66 | # character base 67 | 68 | 69 | class Tartaglia_3_7(Tartaglia_4_1): 70 | name: Literal["Tartaglia"] 71 | version: Literal["3.7"] = "3.7" 72 | skills: List[ 73 | PhysicalNormalAttackBase 74 | | FoulLegacyRagingTide 75 | | HavocObliteration 76 | | TideWithholder 77 | ] = [] 78 | 79 | def _init_skills(self) -> None: 80 | self.skills = [ 81 | PhysicalNormalAttackBase( 82 | name="Cutting Torrent", 83 | cost=PhysicalNormalAttackBase.get_cost(self.element), 84 | ), 85 | FoulLegacyRagingTide(), 86 | HavocObliteration(), 87 | TideWithholder(), 88 | ] 89 | 90 | 91 | register_class(Tartaglia_3_7 | AbyssalMayhemHydrospout_3_7) 92 | -------------------------------------------------------------------------------- /src/lpsim/server/character/hydro/xingqiu_3_3.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ...action import Actions 6 | from ...consts import DamageElementalType 7 | from .xingqiu_3_6 import Xingqiu_3_6 as X_3_6 8 | from .xingqiu_3_6 import Raincutter as R_3_6 9 | from .xingqiu_4_1 import GuhuaStyle, FatalRainscreen 10 | 11 | 12 | class Raincutter(R_3_6): 13 | name: Literal["Raincutter"] = "Raincutter" 14 | 15 | def get_actions(self, match: Any) -> List[Actions]: 16 | """ 17 | Attack, application and create version 3.3 object 18 | """ 19 | return [ 20 | self.charge_self(-2), 21 | self.attack_opposite_active( 22 | match, 23 | self.damage, 24 | self.damage_type, 25 | ), 26 | self.create_team_status("Rainbow Bladework", {"version": "3.3"}), 27 | self.element_application_self(match, DamageElementalType.HYDRO), 28 | ] 29 | 30 | 31 | class Xingqiu_3_3(X_3_6): 32 | version: Literal["3.3"] = "3.3" 33 | skills: List[GuhuaStyle | FatalRainscreen | Raincutter] = [] 34 | 35 | def _init_skills(self) -> None: 36 | self.skills = [GuhuaStyle(), FatalRainscreen(), Raincutter()] 37 | 38 | 39 | register_class(Xingqiu_3_3) 40 | -------------------------------------------------------------------------------- /src/lpsim/server/character/hydro/xingqiu_3_6.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from .xingqiu_4_1 import Xingqiu_4_1 as X_4_1 6 | from .xingqiu_4_1 import Raincutter as R_4_1 7 | from .xingqiu_4_1 import GuhuaStyle, FatalRainscreen 8 | 9 | 10 | class Raincutter(R_4_1): 11 | name: Literal["Raincutter"] = "Raincutter" 12 | damage: int = 1 13 | 14 | 15 | class Xingqiu_3_6(X_4_1): 16 | version: Literal["3.6"] = "3.6" 17 | skills: List[GuhuaStyle | FatalRainscreen | Raincutter] = [] 18 | 19 | def _init_skills(self) -> None: 20 | self.skills = [GuhuaStyle(), FatalRainscreen(), Raincutter()] 21 | 22 | 23 | register_class(Xingqiu_3_6) 24 | -------------------------------------------------------------------------------- /src/lpsim/server/character/pyro/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/character/pyro/bennett_3_3.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ...action import Actions 6 | from ...struct import Cost 7 | 8 | from ...consts import ( 9 | DamageElementalType, 10 | DieColor, 11 | ElementType, 12 | FactionType, 13 | WeaponType, 14 | ) 15 | from ..character_base import ( 16 | ElementalBurstBase, 17 | ElementalSkillBase, 18 | PhysicalNormalAttackBase, 19 | CharacterBase, 20 | SkillTalent, 21 | ) 22 | 23 | 24 | # Skills 25 | 26 | 27 | class FantasticVoyage(ElementalBurstBase): 28 | name: Literal["Fantastic Voyage"] = "Fantastic Voyage" 29 | damage: int = 2 30 | damage_type: DamageElementalType = DamageElementalType.PYRO 31 | cost: Cost = Cost( 32 | elemental_dice_color=DieColor.PYRO, elemental_dice_number=4, charge=2 33 | ) 34 | 35 | def get_actions(self, match: Any) -> List[Actions]: 36 | """ 37 | Attack and create object 38 | """ 39 | return super().get_actions(match) + [ 40 | self.create_team_status( 41 | "Inspiration Field", 42 | {"talent_activated": self.is_talent_equipped(match)}, 43 | ) 44 | ] 45 | 46 | 47 | # Talents 48 | 49 | 50 | class GrandExpectation_3_3(SkillTalent): 51 | name: Literal["Grand Expectation"] 52 | version: Literal["3.3"] = "3.3" 53 | character_name: Literal["Bennett"] = "Bennett" 54 | cost: Cost = Cost( 55 | elemental_dice_color=DieColor.PYRO, elemental_dice_number=4, charge=2 56 | ) 57 | skill: Literal["Fantastic Voyage"] = "Fantastic Voyage" 58 | 59 | 60 | # character base 61 | 62 | 63 | class Bennett_3_3(CharacterBase): 64 | name: Literal["Bennett"] 65 | version: Literal["3.3"] = "3.3" 66 | element: ElementType = ElementType.PYRO 67 | max_hp: int = 10 68 | max_charge: int = 2 69 | skills: List[PhysicalNormalAttackBase | ElementalSkillBase | FantasticVoyage] = [] 70 | faction: List[FactionType] = [FactionType.MONDSTADT] 71 | weapon_type: WeaponType = WeaponType.SWORD 72 | 73 | def _init_skills(self) -> None: 74 | self.skills = [ 75 | PhysicalNormalAttackBase( 76 | name="Strike of Fortune", 77 | cost=PhysicalNormalAttackBase.get_cost(self.element), 78 | ), 79 | ElementalSkillBase( 80 | name="Passion Overload", 81 | damage_type=DamageElementalType.PYRO, 82 | cost=ElementalSkillBase.get_cost(self.element), 83 | ), 84 | FantasticVoyage(), 85 | ] 86 | 87 | 88 | register_class(Bennett_3_3 | GrandExpectation_3_3) 89 | -------------------------------------------------------------------------------- /src/lpsim/server/character/pyro/xiangling_3_3.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ...consts import DieColor 6 | 7 | from ...struct import Cost 8 | 9 | from .xiangling_3_8 import Crossfire_4_2, Xiangling_3_8 as X_3_8 10 | from .xiangling_3_8 import Pyronado as P_3_8 11 | from .xiangling_3_8 import DoughFu, GuobaAttack 12 | 13 | 14 | class Pyronado(P_3_8): 15 | damage: int = 2 16 | 17 | 18 | class Crossfire_3_3(Crossfire_4_2): 19 | version: Literal["3.3"] = "3.3" 20 | cost: Cost = Cost(elemental_dice_color=DieColor.PYRO, elemental_dice_number=4) 21 | 22 | 23 | class Xiangling_3_3(X_3_8): 24 | version: Literal["3.3"] = "3.3" 25 | skills: List[DoughFu | GuobaAttack | Pyronado] = [] 26 | 27 | def _init_skills(self) -> None: 28 | self.skills = [DoughFu(), GuobaAttack(), Pyronado()] 29 | 30 | 31 | register_class(Xiangling_3_3 | Crossfire_3_3) 32 | -------------------------------------------------------------------------------- /src/lpsim/server/character/pyro/yanfei_4_2.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | from .yanfei_3_8 import ( 5 | DoneDeal, 6 | SealOfApproval, 7 | Yanfei_3_8, 8 | RightOfFinalInterpretation_3_8, 9 | SignedEdict as SE_3_8, 10 | ) 11 | 12 | 13 | class SignedEdict(SE_3_8): 14 | max_usage: int = 2 15 | version: Literal["4.2"] = "4.2" 16 | 17 | 18 | class RightOfFinalInterpretation_4_2(RightOfFinalInterpretation_3_8): 19 | """ 20 | Draw card action is performed by Scarlet Seal. 21 | """ 22 | 23 | version: Literal["4.2"] = "4.2" 24 | draw_card: bool = True 25 | 26 | 27 | class Yanfei_4_2(Yanfei_3_8): 28 | version: Literal["4.2"] = "4.2" 29 | skills: List[SealOfApproval | SignedEdict | DoneDeal] = [] 30 | 31 | def _init_skills(self) -> None: 32 | self.skills = [SealOfApproval(), SignedEdict(), DoneDeal()] 33 | 34 | 35 | register_class(Yanfei_4_2 | RightOfFinalInterpretation_4_2) 36 | -------------------------------------------------------------------------------- /src/lpsim/server/character/pyro/yoimiya_3_3.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ..character_base import SkillTalent 6 | from ...consts import DieColor 7 | from ...struct import Cost 8 | from .yoimiya_3_8 import Yoimiya_3_8 as Y_3_8 9 | from .yoimiya_3_8 import RyuukinSaxifrage as RS_3_8 10 | from .yoimiya_3_8 import FireworkFlareUp, NiwabiFireDance 11 | 12 | 13 | class RyuukinSaxifrage(RS_3_8): 14 | cost: Cost = Cost( 15 | elemental_dice_color=DieColor.PYRO, elemental_dice_number=3, charge=2 16 | ) 17 | 18 | 19 | class NaganoharaMeteorSwarm_3_3(SkillTalent): 20 | name: Literal["Naganohara Meteor Swarm"] 21 | version: Literal["3.3"] = "3.3" 22 | character_name: Literal["Yoimiya"] = "Yoimiya" 23 | cost: Cost = Cost( 24 | elemental_dice_color=DieColor.PYRO, 25 | elemental_dice_number=2, 26 | ) 27 | skill: Literal["Niwabi Fire-Dance"] = "Niwabi Fire-Dance" 28 | status_max_usage: int = 2 29 | 30 | 31 | class Yoimiya_3_3(Y_3_8): 32 | version: Literal["3.3"] = "3.3" 33 | max_charge: int = 2 34 | skills: List[FireworkFlareUp | NiwabiFireDance | RyuukinSaxifrage] = [] 35 | 36 | def _init_skills(self) -> None: 37 | self.skills = [FireworkFlareUp(), NiwabiFireDance(), RyuukinSaxifrage()] 38 | 39 | 40 | register_class(Yoimiya_3_3 | NaganoharaMeteorSwarm_3_3) 41 | -------------------------------------------------------------------------------- /src/lpsim/server/character/pyro/yoimiya_3_4.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | from ...consts import DieColor 5 | from ...struct import Cost 6 | from .yoimiya_3_8 import Yoimiya_3_8 as Y_3_8 7 | from .yoimiya_3_8 import RyuukinSaxifrage as RS_3_8 8 | from .yoimiya_3_8 import FireworkFlareUp, NiwabiFireDance 9 | 10 | 11 | class RyuukinSaxifrage(RS_3_8): 12 | damage: int = 4 13 | cost: Cost = Cost( 14 | elemental_dice_color=DieColor.PYRO, elemental_dice_number=4, charge=3 15 | ) 16 | 17 | 18 | class Yoimiya_3_4(Y_3_8): 19 | version: Literal["3.4"] = "3.4" 20 | skills: List[FireworkFlareUp | NiwabiFireDance | RyuukinSaxifrage] = [] 21 | 22 | def _init_skills(self) -> None: 23 | self.skills = [FireworkFlareUp(), NiwabiFireDance(), RyuukinSaxifrage()] 24 | 25 | 26 | register_class(Yoimiya_3_4) 27 | -------------------------------------------------------------------------------- /src/lpsim/server/dice.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | from .struct import ObjectPosition 4 | from .consts import DieColor, ObjectPositionType 5 | from .object_base import ObjectBase, ObjectType 6 | 7 | 8 | class Dice(ObjectBase): 9 | """ 10 | Class representing dice. 11 | 12 | Attributes: 13 | colors: list of colors of dice. 14 | """ 15 | 16 | name: Literal["Dice"] = "Dice" 17 | position: ObjectPosition = ObjectPosition( 18 | player_idx=-1, 19 | area=ObjectPositionType.INVALID, 20 | id=-1, 21 | ) 22 | type: Literal[ObjectType.DICE] = ObjectType.DICE 23 | colors: List[DieColor] = [] 24 | 25 | def colors_to_idx(self, colors: List[DieColor]) -> List[int]: 26 | """ 27 | Convert colors to idx. 28 | """ 29 | res: List[int] = [] 30 | all_c: List[DieColor | None] = list(self.colors) 31 | for x in colors: 32 | res.append(all_c.index(x)) 33 | all_c[all_c.index(x)] = None 34 | return res 35 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | With class registry, after version 4.3, new cards and characters do not need to 3 | be defined in corresponding folders, and they will be defined in the patch. 4 | """ 5 | 6 | 7 | from ...utils import import_all_modules 8 | 9 | 10 | import_all_modules(__file__, __name__) 11 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v43/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__, set(["tests"])) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v43/balance/__init__.py: -------------------------------------------------------------------------------- 1 | from .....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v43/balance/fatui_pyro_agent_4_3.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from .....utils.desc_registry import DescDictType 6 | from ....character.pyro.fatui_pyro_agent_3_3 import FatuiPyroAgent_3_3 7 | 8 | 9 | class FatuiPyroAgent_4_3(FatuiPyroAgent_3_3): 10 | name: Literal["Fatui Pyro Agent"] 11 | version: Literal["4.3"] = "4.3" 12 | max_hp: int = 9 13 | 14 | 15 | desc: Dict[str, DescDictType] = { 16 | "CHARACTER/Fatui Pyro Agent": { 17 | "descs": {"4.3": {"zh-CN": "", "en-US": ""}}, 18 | }, 19 | "SKILL_Fatui Pyro Agent_ELEMENTAL_BURST/Blade Ablaze": { 20 | "descs": { 21 | "4.3": { 22 | "zh-CN": "$SKILL_Fatui Pyro Agent_ELEMENTAL_BURST/Blade Ablaze|descs|3.3|zh-CN", # noqa: E501 23 | "en-US": "$SKILL_Fatui Pyro Agent_ELEMENTAL_BURST/Blade Ablaze|descs|3.3|en-US", # noqa: E501 24 | } 25 | } 26 | }, 27 | "SKILL_Fatui Pyro Agent_ELEMENTAL_SKILL/Prowl": { 28 | "descs": { 29 | "4.3": { 30 | "zh-CN": "$SKILL_Fatui Pyro Agent_ELEMENTAL_SKILL/Prowl|descs|3.3|zh-CN", # noqa: E501 31 | "en-US": "$SKILL_Fatui Pyro Agent_ELEMENTAL_SKILL/Prowl|descs|3.3|en-US", # noqa: E501 32 | } 33 | } 34 | }, 35 | "SKILL_Fatui Pyro Agent_NORMAL_ATTACK/Thrust": { 36 | "descs": { 37 | "4.3": { 38 | "zh-CN": "$SKILL_Fatui Pyro Agent_NORMAL_ATTACK/Thrust|descs|3.3|zh-CN", # noqa: E501 39 | "en-US": "$SKILL_Fatui Pyro Agent_NORMAL_ATTACK/Thrust|descs|3.3|en-US", # noqa: E501 40 | } 41 | } 42 | }, 43 | "SKILL_Fatui Pyro Agent_PASSIVE/Stealth Master": { 44 | "descs": { 45 | "4.3": { 46 | "zh-CN": "$SKILL_Fatui Pyro Agent_PASSIVE/Stealth Master|descs|3.3|zh-CN", # noqa: E501 47 | "en-US": "$SKILL_Fatui Pyro Agent_PASSIVE/Stealth Master|descs|3.3|en-US", # noqa: E501 48 | } 49 | } 50 | }, 51 | } 52 | 53 | 54 | register_class(FatuiPyroAgent_4_3, desc) 55 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v43/balance/joyous_celebration_4_3.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from .....utils.desc_registry import DescDictType 6 | from ....struct import Cost 7 | from ....card.event.arcane_legend import JoyousCelebration_4_2 8 | 9 | 10 | class JoyousCelebration_4_3(JoyousCelebration_4_2): 11 | version: Literal["4.3"] = "4.3" 12 | cost: Cost = Cost(arcane_legend=True) 13 | 14 | 15 | desc: Dict[str, DescDictType] = { 16 | "ARCANE/Joyous Celebration": { 17 | "descs": { 18 | "4.3": { 19 | "zh-CN": "$ARCANE/Joyous Celebration|descs|4.2|zh-CN", 20 | "en-US": "$ARCANE/Joyous Celebration|descs|4.2|en-US", 21 | } 22 | }, 23 | }, 24 | } 25 | 26 | 27 | register_class(JoyousCelebration_4_3, desc) 28 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v43/balance/ocean_huled_4_3.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Literal 2 | from pydantic import PrivateAttr 3 | 4 | from .....utils.class_registry import register_class 5 | 6 | from .....utils.desc_registry import DescDictType 7 | 8 | from ....consts import DamageElementalType, DamageType 9 | 10 | from ....modifiable_values import DamageValue 11 | 12 | from ....action import Actions, MakeDamageAction 13 | 14 | from ....struct import Cost 15 | 16 | from ....card.equipment.artifact.ocean_hued import OceanHuedClam_4_2 17 | 18 | from ....match import Match 19 | 20 | 21 | class OceanHuedClam_4_3(OceanHuedClam_4_2): 22 | name: Literal["Ocean-Hued Clam"] 23 | version: Literal["4.3"] = "4.3" 24 | cost: Cost = Cost(any_dice_number=3) 25 | _heal: int = PrivateAttr(2) 26 | 27 | def equip(self, match: Match) -> List[Actions]: 28 | super().equip(match) 29 | character = match.player_tables[self.position.player_idx].characters[ 30 | self.position.character_idx 31 | ] 32 | # heal self 33 | return [ 34 | MakeDamageAction( 35 | damage_value_list=[ 36 | DamageValue( 37 | position=self.position, 38 | target_position=character.position, 39 | damage_type=DamageType.HEAL, 40 | damage=-self._heal, 41 | damage_elemental_type=DamageElementalType.HEAL, 42 | cost=Cost(), 43 | ) 44 | ] 45 | ) 46 | ] 47 | 48 | 49 | desc: Dict[str, DescDictType] = { 50 | "ARTIFACT/Ocean-Hued Clam": { 51 | "descs": { 52 | "4.3": { 53 | "zh-CN": "入场时:治疗所附属角色2点。我方角色每受到3点治疗,此牌就累积1个「海染泡沫」。(最多累积2个)角色造成伤害时:消耗所有「海染泡沫」,每消耗1个都使造成的伤害+1。(角色最多装备1件「圣遗物」)", # noqa: E501 54 | "en-US": "When played: Heal this character by 2 HP. For every 3 HP of healing your characters receive, this card accumulates 1 Sea-Dyed Foam (maximum of 2). When this character deals DMG: Consume all Sea-Dyed Foam. DMG is increased by 1 for each Sea-Dyed Foam consumed. (Characters can equip at most 1 Artifact)", # noqa: E501 55 | } 56 | }, 57 | }, 58 | } 59 | 60 | 61 | register_class(OceanHuedClam_4_3, desc) 62 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v43/balance/stone_and_contracts_4_3.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Literal 2 | 3 | from ....status.team_status.event_cards import ( 4 | StoneAndContracts_3_7 as StoneAndContractsStatus_3_7, 5 | ) 6 | 7 | from .....utils.class_registry import register_class 8 | from ....action import CreateDiceAction, DrawCardAction, RemoveObjectAction 9 | from .....utils.desc_registry import DescDictType 10 | from ....card.event.resonance import StoneAndContracts_3_7 11 | 12 | 13 | class StoneAndContractsStatus_4_3(StoneAndContractsStatus_3_7): 14 | version: Literal["4.3"] = "4.3" 15 | 16 | def event_handler_ROUND_PREPARE( 17 | self, event: Any, match: Any 18 | ) -> List[CreateDiceAction | RemoveObjectAction | DrawCardAction]: 19 | """ 20 | When round prepare, create 3 omni element and remove self. 21 | """ 22 | return super().event_handler_ROUND_PREPARE(event, match) + [ 23 | DrawCardAction( 24 | player_idx=self.position.player_idx, 25 | number=1, 26 | draw_if_filtered_not_enough=True, 27 | ) 28 | ] 29 | 30 | 31 | class StoneAndContracts_4_3(StoneAndContracts_3_7): 32 | version: Literal["4.3"] = "4.3" 33 | 34 | 35 | desc: Dict[str, DescDictType] = { 36 | "TEAM_STATUS/Stone and Contracts": { 37 | "descs": { 38 | "4.3": { 39 | "zh-CN": "下回合行动阶段开始时:生成3点万能元素,抓一张牌。", 40 | "en-US": "When the Action Phase of the next Round begins: Create 3 Omni Element, draw a card.", # noqa: E501 41 | } 42 | }, 43 | }, 44 | "CARD/Stone and Contracts": { 45 | "descs": { 46 | "4.3": { 47 | "zh-CN": "下回合行动阶段开始时:生成3点万能元素,抓一张牌。(牌组包含至少2个「璃月」角色,才能加入牌组)", # noqa: E501 48 | "en-US": "When the Action Phase of the next Round begins: Create 3 Omni Element, draw a card. (You must have at least 2 Liyue characters in your deck to add this card to your deck.)", # noqa: E501 49 | } 50 | }, 51 | }, 52 | } 53 | 54 | 55 | register_class(StoneAndContractsStatus_4_3 | StoneAndContracts_4_3, desc) 56 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v43/balance/wind_and_freedom_4_3.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Literal 2 | 3 | from ....status.team_status.event_cards import ( 4 | WindAndFreedom_4_1 as WindAndFreedomStatus_4_1, 5 | ) 6 | 7 | from .....utils.class_registry import register_class 8 | from .....utils.desc_registry import DescDictType 9 | from ....struct import Cost 10 | from ....card.event.resonance import WindAndFreedom_4_1 11 | 12 | 13 | class WindAndFreedomStatus_4_3(WindAndFreedomStatus_4_1): 14 | version: Literal["4.3"] = "4.3" 15 | 16 | 17 | class WindAndFreedom_4_3(WindAndFreedom_4_1): 18 | version: Literal["4.3"] = "4.3" 19 | cost: Cost = Cost() 20 | 21 | 22 | desc: Dict[str, DescDictType] = { 23 | "TEAM_STATUS/Wind and Freedom": { 24 | "descs": { 25 | "4.3": { 26 | "zh-CN": "$TEAM_STATUS/Wind and Freedom|descs|4.1|zh-CN", 27 | "en-US": "$TEAM_STATUS/Wind and Freedom|descs|4.1|en-US", 28 | } 29 | }, 30 | }, 31 | "CARD/Wind and Freedom": { 32 | "descs": { 33 | "4.3": { 34 | "zh-CN": "$CARD/Wind and Freedom|descs|4.1|zh-CN", 35 | "en-US": "$CARD/Wind and Freedom|descs|4.1|en-US", 36 | } 37 | }, 38 | }, 39 | } 40 | 41 | 42 | register_class(WindAndFreedomStatus_4_3 | WindAndFreedom_4_3, desc) 43 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v43/cards/__init__.py: -------------------------------------------------------------------------------- 1 | from .....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v43/cards/fish_and_chips_4_3.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | from ....modifiable_values import CostValue 5 | from ....consts import IconType, ObjectPositionType 6 | from ....status.character_status.base import RoundCharacterStatus, UsageCharacterStatus 7 | from ....struct import Cost 8 | from ....card.event.foods import AllCharacterFoodCard 9 | from .....utils.desc_registry import DescDictType 10 | 11 | 12 | class FishAndChipsStatus_4_3(RoundCharacterStatus, UsageCharacterStatus): 13 | name: Literal["Fish and Chips"] = "Fish and Chips" 14 | version: Literal["4.3"] = "4.3" 15 | usage: int = 1 16 | max_usage: int = 1 17 | icon_type: Literal[IconType.BUFF] = IconType.BUFF 18 | 19 | def value_modifier_COST( 20 | self, value: CostValue, match: Any, mode: Literal["TEST", "REAL"] 21 | ) -> CostValue: 22 | """ 23 | If this character use skill, decrease 1 cost. 24 | """ 25 | if self.usage <= 0: # pragma: no cover 26 | # no usage, not modify 27 | return value 28 | if not self.position.check_position_valid( 29 | value.position, 30 | match, 31 | player_idx_same=True, 32 | character_idx_same=True, 33 | target_area=ObjectPositionType.SKILL, 34 | ): 35 | # not character use skill, not modify 36 | return value 37 | # modify 38 | ele_cost = value.cost.elemental_dice_color 39 | if value.cost.decrease_cost(ele_cost): # pragma: no branch 40 | # decrease success 41 | if mode == "REAL": 42 | self.usage -= 1 43 | return value 44 | 45 | 46 | class FishAndChips_4_3(AllCharacterFoodCard): 47 | name: Literal["Fish and Chips"] 48 | version: Literal["4.3"] = "4.3" 49 | cost: Cost = Cost(any_dice_number=2) 50 | can_eat_only_if_damaged: bool = False 51 | 52 | 53 | desc: Dict[str, DescDictType] = { 54 | "CARD/Fish and Chips": { 55 | "names": {"en-US": "Fish and Chips", "zh-CN": "炸鱼薯条"}, 56 | "descs": { 57 | "4.3": { 58 | "en-US": "During this Round, all your characters will use 1 less Elemental Die when using their next Skill.\n(A character can consume at most 1 Food per Round)", # noqa: E501 59 | "zh-CN": "本回合中,所有我方角色下次使用技能时少花费1个元素骰。\n(每回合每个角色最多食用1次「料理」)", # noqa: E501 60 | } 61 | }, 62 | "image_path": "cardface/Event_Food_Chips.png", # noqa: E501 63 | "id": 333013, 64 | }, 65 | "CHARACTER_STATUS/Fish and Chips": { 66 | "names": {"en-US": "Fish and Chips", "zh-CN": "炸鱼薯条"}, 67 | "descs": { 68 | "4.3": { 69 | "en-US": "During this Round, this character will use 1 less Elemental Die when using their next Skill.", # noqa: E501 70 | "zh-CN": "本回合中,该角色下次使用技能时少花费1个元素骰。", 71 | } 72 | }, 73 | }, 74 | } 75 | 76 | 77 | register_class(FishAndChips_4_3 | FishAndChipsStatus_4_3, desc) 78 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v43/cards/gilded_dreams_4_3.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | from ....consts import ELEMENT_TO_DIE_COLOR, DieColor 5 | from ....match import Match 6 | from ....action import CreateDiceAction 7 | from ....struct import Cost 8 | from ....card.equipment.artifact.gilded_dreams import ShadowOfTheSandKing_4_2 9 | from .....utils.desc_registry import DescDictType 10 | 11 | 12 | class GildedDreams_4_3(ShadowOfTheSandKing_4_2): 13 | name: Literal["Gilded Dreams"] 14 | version: Literal["4.3"] = "4.3" 15 | cost: Cost = Cost(any_dice_number=3) 16 | max_usage_per_round: int = 2 17 | 18 | def equip(self, match: Match) -> List[CreateDiceAction]: 19 | """ 20 | When equipped, create one elemental dice. If there are 3 different 21 | elemental dice in your team, create one omni dice. 22 | """ 23 | super().equip(match) 24 | characters = match.player_tables[self.position.player_idx].characters 25 | equip_character = characters[self.position.character_idx] 26 | ret: List[CreateDiceAction] = [ 27 | CreateDiceAction( 28 | player_idx=self.position.player_idx, 29 | number=1, 30 | color=ELEMENT_TO_DIE_COLOR[equip_character.element], 31 | ) 32 | ] 33 | elements = set() 34 | for character in characters: 35 | elements.add(character.element) 36 | if len(elements) >= 3: 37 | ret.append( 38 | CreateDiceAction( 39 | player_idx=self.position.player_idx, number=1, color=DieColor.OMNI 40 | ) 41 | ) 42 | return ret 43 | 44 | 45 | desc: Dict[str, DescDictType] = { 46 | "ARTIFACT/Gilded Dreams": { 47 | "names": {"en-US": "Gilded Dreams", "zh-CN": "饰金之梦"}, 48 | "descs": { 49 | "4.3": { 50 | "en-US": "When played: generate 1 Die of the same Element as the attached character. If you have 3 different Elemental Types in your party, generate 1 Omni Element in addition to this.\nWhen the character to which this card is attached is your active character, then when an opposing character takes Elemental Reaction DMG: Draw a card. (Twice per Round)\n(A character can equip a maximum of 1 Artifact)", # noqa: E501 51 | "zh-CN": "入场时:生成1个所附属角色类型的元素骰。如果我方队伍中存在3种不同元素类型的角色,则额外生成1个万能元素。\n所附属角色为出战角色期间,敌方受到元素反应伤害时:抓1张牌。(每回合至多2次)\n(角色最多装备1件「圣遗物」)", # noqa: E501 52 | } 53 | }, 54 | "image_path": "cardface/Modify_Artifact_Chiwangtao.png", # noqa: E501 55 | "id": 312018, 56 | }, 57 | } 58 | 59 | 60 | register_class(GildedDreams_4_3, desc) 61 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v43/cards/seed_dispensary_4_3.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Literal 2 | 3 | from ....action import Actions 4 | from ....event import MoveObjectEventArguments 5 | from ....card.support.base import UsageWithRoundRestrictionSupportBase 6 | from ....card.support.items import ItemBase 7 | from .....utils.class_registry import register_class 8 | from ....match import Match 9 | from ....modifiable_values import CostValue 10 | from ....consts import CostLabels, IconType, ObjectPositionType 11 | from ....struct import Cost 12 | from .....utils.desc_registry import DescDictType 13 | 14 | 15 | class SeedDispensary_4_3(ItemBase, UsageWithRoundRestrictionSupportBase): 16 | name: Literal["Seed Dispensary"] 17 | version: Literal["4.3"] = "4.3" 18 | cost: Cost = Cost() 19 | usage: int = 2 20 | max_usage_one_round: int = 1 21 | icon_type: Literal[IconType.TIMESTATE] = IconType.TIMESTATE 22 | 23 | decrease_target: int = CostLabels.SUPPORTS.value | CostLabels.EQUIPMENT.value 24 | decrease_threshold: int = 1 25 | 26 | def value_modifier_COST( 27 | self, value: CostValue, match: Match, mode: Literal["TEST", "REAL"] 28 | ) -> CostValue: 29 | """ 30 | if card label match and original cost greater than threshold, 31 | reduce cost. 32 | """ 33 | assert value.cost.original_value is not None 34 | if ( 35 | self.position.area == ObjectPositionType.SUPPORT 36 | and value.position.player_idx == self.position.player_idx 37 | and value.cost.original_value.total_dice_cost <= self.decrease_threshold 38 | and value.cost.label & self.decrease_target != 0 39 | and self.has_usage() 40 | ): 41 | # area right, player right, cost label match, cost smaller than 42 | # threshold, and not used this round 43 | if value.cost.decrease_cost(value.cost.elemental_dice_color): 44 | if mode == "REAL": 45 | self.use() 46 | return value 47 | 48 | def event_handler_MOVE_OBJECT( 49 | self, event: MoveObjectEventArguments, match: Match 50 | ) -> List[Actions]: 51 | if self.usage == 0: 52 | # if usage is zero, it run out of usage, remove it 53 | return list(self.check_should_remove()) 54 | # otherwise, do parent event handler 55 | return super().event_handler_MOVE_OBJECT(event, match) 56 | 57 | 58 | desc: Dict[str, DescDictType] = { 59 | "SUPPORT/Seed Dispensary": { 60 | "names": {"en-US": "Seed Dispensary", "zh-CN": "化种匣"}, 61 | "descs": { 62 | "4.3": { 63 | "en-US": "When you play an Equipment or Support Card with an original cost of at least 1 Elemental Die: Spend 1 less Elemental Die. (Once per Round)\nUsage(s): 2", # noqa: E501 64 | "zh-CN": "我方打出原本元素骰费用为1的装备或支援牌时:少花费1个元素骰。(每回合1次)\n可用次数:2", # noqa: E501 65 | } 66 | }, 67 | "image_path": "cardface/Assist_Prop_Seedbox.png", # noqa: E501 68 | "id": 323005, 69 | }, 70 | } 71 | 72 | 73 | register_class(SeedDispensary_4_3, desc) 74 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v43/cards/weeping_willow_4_3.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | from ....action import DrawCardAction, RemoveObjectAction 5 | from ....match import Match 6 | from ....event import RoundEndEventArguments 7 | from ....consts import IconType 8 | from ....struct import Cost 9 | from ....card.support.locations import LocationBase 10 | from .....utils.desc_registry import DescDictType 11 | 12 | 13 | class WeepingWillowOfTheLake_4_3(LocationBase): 14 | name: Literal["Weeping Willow of the Lake"] 15 | version: Literal["4.3"] = "4.3" 16 | cost: Cost = Cost(same_dice_number=1) 17 | usage: int = 2 18 | icon_type: Literal[IconType.TIMESTATE] = IconType.TIMESTATE 19 | 20 | def event_handler_ROUND_END( 21 | self, event: RoundEndEventArguments, match: Match 22 | ) -> List[DrawCardAction | RemoveObjectAction]: 23 | """ 24 | When in round end, if hand size less than 2, draw 2 cards, and check 25 | if should remove. 26 | """ 27 | if self.position.area != "SUPPORT": 28 | # not in support area, do nothing 29 | return [] 30 | if len(match.player_tables[self.position.player_idx].hands) > 2: 31 | # hand size more than 2, do nothing 32 | return [] 33 | self.usage -= 1 34 | return [ 35 | DrawCardAction( 36 | player_idx=self.position.player_idx, 37 | number=2, 38 | draw_if_filtered_not_enough=True, 39 | ), 40 | ] + self.check_should_remove() 41 | 42 | 43 | desc: Dict[str, DescDictType] = { 44 | "SUPPORT/Weeping Willow of the Lake": { 45 | "names": {"en-US": "Weeping Willow of the Lake", "zh-CN": "湖中垂柳"}, 46 | "descs": { 47 | "4.3": { 48 | "en-US": "End Phase: If your Hand has no more than 2 cards, draw 2 cards.\nUsage(s): 2", # noqa: E501 49 | "zh-CN": "结束阶段:如果我方手牌数量不多于2,则抓2张牌。\n可用次数:2", # noqa: E501 50 | } 51 | }, 52 | "image_path": "cardface/Assist_Location_Chuiliu.png", # noqa: E501 53 | "id": 321016, 54 | }, 55 | } 56 | 57 | 58 | register_class(WeepingWillowOfTheLake_4_3, desc) 59 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v43/characters/__init__.py: -------------------------------------------------------------------------------- 1 | from .....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v44/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v44/balance/__init__.py: -------------------------------------------------------------------------------- 1 | from .....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v44/balance/in_every_house_a_stove_4_4.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Literal 2 | 3 | from .....utils.class_registry import register_class 4 | 5 | from .....utils.desc_registry import DescDictType 6 | from ....action import ConsumeArcaneLegendAction, DrawCardAction 7 | from ....card.event.arcane_legend import ArcaneLegendBase 8 | from ....match import Match 9 | from ....struct import Cost, ObjectPosition 10 | 11 | 12 | class InEveryHouseAStove_4_4(ArcaneLegendBase): 13 | name: Literal["In Every House a Stove"] 14 | version: Literal["4.4"] = "4.4" 15 | cost: Cost = Cost(arcane_legend=True) 16 | 17 | def get_targets(self, match: Match) -> List[ObjectPosition]: 18 | return [] 19 | 20 | def get_actions( 21 | self, target: ObjectPosition | None, match: Match 22 | ) -> List[ConsumeArcaneLegendAction | DrawCardAction]: 23 | """ 24 | Draw cards. 25 | """ 26 | assert target is None 27 | return super().get_actions(target, match) + [ 28 | DrawCardAction( 29 | player_idx=self.position.player_idx, 30 | number=min(match.round_number - 1, 4), 31 | draw_if_filtered_not_enough=True, 32 | ) 33 | ] 34 | 35 | 36 | desc: Dict[str, DescDictType] = { 37 | "ARCANE/In Every House a Stove": { 38 | "descs": { 39 | "4.4": { 40 | "zh-CN": "我方抓相当于当前的回合数减1的牌。(最多抓4张)(整局游戏只能打出一张「秘传」卡牌;这张牌一定在你的起始手牌中)", # noqa: E501 41 | "en-US": 'Draw a number of cards equal to the current Round number minus 1. (Up to 4 cards can be drawn in this way) (Only one "Arcane Legend" card can be played for the entire game. This card will be in your starting hand.)', # noqa: E501 42 | } 43 | }, 44 | }, 45 | } 46 | 47 | 48 | register_class(InEveryHouseAStove_4_4, desc) 49 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v44/balance/vourukashas_glow_4_4.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Literal 2 | 3 | from ....action import MakeDamageAction 4 | from ....event import RoundEndEventArguments 5 | from ....struct import Cost 6 | from ....patch.v43.cards.vourukashas_glow_4_3 import VourukashasGlow_4_3 7 | from ....match import Match 8 | from .....utils import register_class, DescDictType 9 | 10 | 11 | class VourukashasGlow_4_4(VourukashasGlow_4_3): 12 | name: Literal["Vourukasha's Glow"] 13 | version: Literal["4.4"] = "4.4" 14 | cost: Cost = Cost(same_dice_number=1) 15 | 16 | def event_handler_ROUND_END( 17 | self, desc: RoundEndEventArguments, match: Match 18 | ) -> List[MakeDamageAction]: 19 | if self.usage == self.max_usage_per_round: 20 | # not draw card, return 21 | return [] 22 | return super().event_handler_ROUND_END(desc, match) 23 | 24 | 25 | desc: Dict[str, DescDictType] = { 26 | "ARTIFACT/Vourukasha's Glow": { 27 | "descs": { 28 | "4.4": { 29 | "en-US": 'After this character takes DMG: Draw 1 card. (Once per Round)\nEnd Phase: If this character has triggered the effect "Draw 1 card" this round: heal the attached character for 1 HP.\n(A character can equip a maximum of 1 Artifact)', # noqa: E501 30 | "zh-CN": "角色受到伤害后:如果所附属角色为「出战角色」,则抓1张牌。(每回合1次)\n结束阶段:若本角色在回合内触发了抓1张牌的效果,治疗所附属角色1点。\n(角色最多装备1件「圣遗物」)", # noqa: E501 31 | } 32 | }, 33 | }, 34 | } 35 | 36 | 37 | register_class(VourukashasGlow_4_4, desc) 38 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v44/cards/__init__.py: -------------------------------------------------------------------------------- 1 | from .....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v44/characters/__init__.py: -------------------------------------------------------------------------------- 1 | from .....utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v45/__init__.py: -------------------------------------------------------------------------------- 1 | from lpsim.utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v45/balance/__init__.py: -------------------------------------------------------------------------------- 1 | from lpsim.utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v45/balance/gilded_dreams_4_5.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Literal, List 2 | 3 | from lpsim.server.consts import ELEMENT_TO_DIE_COLOR 4 | from lpsim.server.action import CreateDiceAction 5 | from lpsim.server.patch.v43.cards.gilded_dreams_4_3 import GildedDreams_4_3 6 | from lpsim.server.struct import Cost 7 | from lpsim.server import Match 8 | from lpsim.utils.class_registry import register_class 9 | from lpsim.utils.desc_registry import DescDictType 10 | 11 | 12 | class GildedDreams_4_5(GildedDreams_4_3): 13 | version: Literal["4.5"] = "4.5" 14 | cost: Cost = Cost(same_dice_number=3) 15 | 16 | def equip(self, match: Match) -> List[CreateDiceAction]: 17 | """ 18 | When equipped, create one elemental dice. If there are 3 different 19 | elemental dice in your team, create 2 dice. 20 | """ 21 | super().equip(match) 22 | characters = match.player_tables[self.position.player_idx].characters 23 | equip_character = characters[self.position.character_idx] 24 | ret: CreateDiceAction = CreateDiceAction( 25 | player_idx=self.position.player_idx, 26 | number=1, 27 | color=ELEMENT_TO_DIE_COLOR[equip_character.element], 28 | ) 29 | elements = set() 30 | for character in characters: 31 | elements.add(character.element) 32 | if len(elements) >= 3: 33 | ret.number = 2 34 | return [ret] 35 | 36 | 37 | desc: Dict[str, DescDictType] = { 38 | "ARTIFACT/Gilded Dreams": { 39 | "descs": { 40 | "4.5": { 41 | "en-US": "When played: generate 1 Die of the same Element as the attached character. If you have 3 different Elemental Types in your party, generate 2 Dice of the same Element as the attached character.\nWhen the character to which this card is attached is your active character, then when an opposing character takes Elemental Reaction DMG: Draw a card. (Twice per Round)\n(A character can equip a maximum of 1 Artifact)", # noqa: E501 42 | "zh-CN": "入场时:生成1个所附属角色类型的元素骰。如果我方队伍中存在3种不同元素类型的角色,则生成2个所附属角色类型的元素骰。\n所附属角色为出战角色期间,敌方受到元素反应伤害时:抓1张牌。(每回合至多2次)\n(角色最多装备1件「圣遗物」)", # noqa: E501 43 | } 44 | }, 45 | }, 46 | } 47 | 48 | 49 | register_class(GildedDreams_4_5, desc) 50 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v45/balance/jade_chamber_4_5.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Literal 2 | from lpsim.server.action import CreateDiceAction, RemoveObjectAction 3 | 4 | from lpsim.server.card.support.locations import JadeChamber_4_0 5 | from lpsim.server.consts import DieColor, ObjectPositionType 6 | from lpsim.server.event import RoundPrepareEventArguments 7 | from lpsim.server.match import Match 8 | from lpsim.utils.class_registry import register_class 9 | from lpsim.utils.desc_registry import DescDictType 10 | 11 | 12 | class JadeChamber_4_5(JadeChamber_4_0): 13 | version: Literal["4.5"] = "4.5" 14 | 15 | def event_handler_ROUND_PREPARE( 16 | self, event: RoundPrepareEventArguments, match: Match 17 | ) -> List[CreateDiceAction | RemoveObjectAction]: 18 | """ 19 | When hand number less than 3, create 1 omni die and remove self. 20 | """ 21 | if ( 22 | self.position.area != ObjectPositionType.SUPPORT 23 | or len(match.player_tables[self.position.player_idx].hands) > 3 24 | ): 25 | return [] 26 | return [ 27 | CreateDiceAction( 28 | player_idx=self.position.player_idx, 29 | number=1, 30 | color=DieColor.OMNI, 31 | ), 32 | RemoveObjectAction(object_position=self.position), 33 | ] 34 | 35 | 36 | desc: Dict[str, DescDictType] = { 37 | "SUPPORT/Jade Chamber": { 38 | "descs": { 39 | "4.5": { 40 | "zh-CN": "投掷阶段:2个元素骰初始总是投出我方出战角色类型的元素。\n行动阶段开始时:如果我方手牌数量不多于3,则弃置此牌,生成1个万能元素。", # noqa: E501 41 | "en-US": "Roll Phase: 2 initial Elemental Dice will be of the same Elemental type as your active character.\nWhen Action Phase begins: If you have no more than 3 cards in your Hand, discard this card and create 1 Omni Element.", # noqa: E501 42 | } 43 | }, 44 | }, 45 | } 46 | 47 | 48 | register_class(JadeChamber_4_5, desc) 49 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v45/balance/knights_of_favonius_library_4_5.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Literal 2 | 3 | from lpsim.server.card.support.locations import KnightsOfFavoniusLibrary_3_3 4 | from lpsim.server.struct import Cost 5 | from lpsim.utils.class_registry import register_class 6 | from lpsim.utils.desc_registry import DescDictType 7 | 8 | 9 | class KnightsOfFavoniusLibrary_4_5(KnightsOfFavoniusLibrary_3_3): 10 | version: Literal["4.5"] = "4.5" 11 | cost: Cost = Cost() 12 | 13 | 14 | desc: Dict[str, DescDictType] = { 15 | "SUPPORT/Knights of Favonius Library": { 16 | "descs": { 17 | "4.5": { 18 | "zh-CN": "$SUPPORT/Knights of Favonius Library|descs|3.3|zh-CN", 19 | "en-US": "$SUPPORT/Knights of Favonius Library|descs|3.3|en-US", 20 | } 21 | }, 22 | }, 23 | } 24 | 25 | 26 | register_class(KnightsOfFavoniusLibrary_4_5, desc) 27 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v45/cards/__init__.py: -------------------------------------------------------------------------------- 1 | from lpsim.utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v45/cards/controlled_directional_blast_4_5.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Literal 2 | from lpsim.server.action import ChangeObjectUsageAction 3 | from lpsim.server.match import Match 4 | from lpsim.server.object_base import EventCardBase 5 | from lpsim.server.struct import Cost, ObjectPosition 6 | from lpsim.utils.class_registry import register_class 7 | from lpsim.utils.desc_registry import DescDictType 8 | 9 | 10 | class ControlledDirectionalBlast_4_5(EventCardBase): 11 | name: Literal["Controlled Directional Blast"] = "Controlled Directional Blast" 12 | version: Literal["4.5"] = "4.5" 13 | cost: Cost = Cost(same_dice_number=1) 14 | 15 | def is_valid(self, match: Match) -> bool: 16 | """ 17 | can use if opponent has 4 or more support and summon and any summon exist 18 | """ 19 | return ( 20 | len(self.query(match, "opponent support and opponent summon")) >= 4 21 | and len(self.query(match, "both summon")) > 0 22 | ) 23 | 24 | def get_targets(self, match: Match) -> List[ObjectPosition]: 25 | return [] 26 | 27 | def get_actions( 28 | self, target: ObjectPosition | None, match: Match 29 | ) -> List[ChangeObjectUsageAction]: 30 | assert target is None 31 | targets = self.query(match, "both summon") 32 | ret: List[ChangeObjectUsageAction] = [] 33 | for t in targets: 34 | ret.append( 35 | ChangeObjectUsageAction(object_position=t.position, change_usage=-1) 36 | ) 37 | return ret 38 | 39 | 40 | desc: Dict[str, DescDictType] = { 41 | "CARD/Controlled Directional Blast": { 42 | "names": { 43 | "en-US": "Controlled Directional Blast", 44 | "zh-CN": "可控性去危害化式定向爆破", 45 | }, 46 | "descs": { 47 | "4.5": { 48 | "en-US": "Can only be played when there is a total of at least 4 cards in your opponent's Support Zone and Summons Zone: All Summons on both sides lose 1 Usage.", # noqa: E501 49 | "zh-CN": "对方支援区和召唤物区的卡牌数量总和至少为4时,才能打出:双方所有召唤物的可用次数-1。", 50 | } 51 | }, 52 | "image_path": "cardface/Event_Event_Kexueyuan.png", # noqa: E501 53 | "id": 332030, 54 | }, 55 | } 56 | 57 | 58 | register_class(ControlledDirectionalBlast_4_5, desc) 59 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v45/cards/golden_troupes_reward_4_5.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Literal 2 | from lpsim.server.action import Actions 3 | 4 | from lpsim.server.card.equipment.artifact.base import ArtifactBase 5 | from lpsim.server.consts import CostLabels 6 | from lpsim.server.event import RoundEndEventArguments 7 | from lpsim.server.match import Match 8 | from lpsim.server.modifiable_values import CostValue 9 | from lpsim.server.struct import Cost 10 | from lpsim.utils.class_registry import register_class 11 | from lpsim.utils.desc_registry import DescDictType 12 | 13 | 14 | class GoldenTroupesReward_4_5(ArtifactBase): 15 | name: Literal["Golden Troupe's Reward"] = "Golden Troupe's Reward" 16 | version: Literal["4.5"] = "4.5" 17 | cost: Cost = Cost() 18 | usage: int = 0 19 | max_usage: int = 2 20 | 21 | def event_handler_ROUND_END( 22 | self, event: RoundEndEventArguments, match: Match 23 | ) -> List[Actions]: 24 | if self.position.satisfy("source area=character active=false", match=match): 25 | self.usage = min(self.usage + 1, self.max_usage) 26 | return [] 27 | 28 | def value_modifier_COST( 29 | self, value: CostValue, match: Match, mode: Literal["TEST", "REAL"] 30 | ) -> CostValue: 31 | """ 32 | If self use any skill, or equip talent, cost decrease based on self usage. 33 | """ 34 | if not self._check_value_self_skill_or_talent( 35 | value, match, CostLabels.ELEMENTAL_SKILL.value 36 | ): 37 | return value 38 | # decrease cost 39 | for _ in range(self.usage): 40 | if value.cost.decrease_cost(value.cost.elemental_dice_color): 41 | if mode == "REAL": 42 | self.usage -= 1 43 | return value 44 | 45 | 46 | desc: Dict[str, DescDictType] = { 47 | "ARTIFACT/Golden Troupe's Reward": { 48 | "names": {"en-US": "Golden Troupe's Reward", "zh-CN": "黄金剧团的奖赏"}, 49 | "descs": { 50 | "4.5": { 51 | "en-US": 'End Phase: If the character to which this is attached is on standby, this card gains 1 "Recompense" point. (Max 2 points)\nWhen you play a Talent card or a Character uses an Elemental Skill: For each "Recompense" point this card has, consume it to spend 1 less Elemental Die.\n(A character can equip a maximum of 1 Artifact)', # noqa: E501 52 | "zh-CN": "结束阶段:如果所附属角色在后台,则此牌累积1点「报酬」。(最多累积2点)\n对角色打出「天赋」或角色使用「元素战技」时:此牌每有1点「报酬」,就将其消耗,以少花费1个元素骰。\n(角色最多装备1件「圣遗物」)", # noqa: E501 53 | } 54 | }, 55 | "image_path": "cardface/Modify_Artifact_HuangjinjutuanXiaojian.png", # noqa: E501 56 | "id": 312025, 57 | }, 58 | } 59 | 60 | 61 | register_class(GoldenTroupesReward_4_5, desc) 62 | -------------------------------------------------------------------------------- /src/lpsim/server/patch/v45/characters/__init__.py: -------------------------------------------------------------------------------- 1 | from lpsim.utils import import_all_modules 2 | 3 | 4 | import_all_modules(__file__, __name__) 5 | -------------------------------------------------------------------------------- /src/lpsim/server/status/__init__.py: -------------------------------------------------------------------------------- 1 | from ...utils import import_all_modules 2 | from .base import StatusBase 3 | from .character_status.base import CharacterStatusBase 4 | from .team_status.base import TeamStatusBase 5 | 6 | 7 | import_all_modules(__file__, __name__) 8 | __all__ = ("StatusBase", "CharacterStatusBase", "TeamStatusBase") 9 | -------------------------------------------------------------------------------- /src/lpsim/server/status/character_status/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | from .base import CharacterStatusBase 3 | 4 | 5 | import_all_modules(__file__, __name__) 6 | __all__ = ("CharacterStatusBase",) 7 | -------------------------------------------------------------------------------- /src/lpsim/server/status/character_status/artifacts.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ...consts import IconType, SkillType 6 | 7 | from ...modifiable_values import DamageIncreaseValue 8 | from .base import RoundCharacterStatus, ShieldCharacterStatus 9 | 10 | 11 | class UnmovableMountain_3_5(ShieldCharacterStatus): 12 | name: Literal["Unmovable Mountain"] = "Unmovable Mountain" 13 | version: Literal["3.5"] = "3.5" 14 | usage: int = 2 15 | max_usage: int = 2 16 | 17 | 18 | class VermillionHereafter_4_0(RoundCharacterStatus): 19 | name: Literal["Vermillion Hereafter"] 20 | version: Literal["4.0"] = "4.0" 21 | usage: int = 1 22 | max_usage: int = 1 23 | icon_type: Literal[IconType.ATK_UP] = IconType.ATK_UP 24 | 25 | def value_modifier_DAMAGE_INCREASE( 26 | self, value: DamageIncreaseValue, match: Any, mode: Literal["TEST", "REAL"] 27 | ) -> DamageIncreaseValue: 28 | if not value.is_corresponding_character_use_damage_skill( 29 | self.position, match, SkillType.NORMAL_ATTACK 30 | ): 31 | # not current character using normal attack 32 | return value 33 | # increase damage 34 | assert mode == "REAL" 35 | value.damage += 1 36 | return value 37 | 38 | 39 | register_class(UnmovableMountain_3_5 | VermillionHereafter_4_0) 40 | -------------------------------------------------------------------------------- /src/lpsim/server/status/character_status/system.py: -------------------------------------------------------------------------------- 1 | """ 2 | Character status generated by system. 3 | """ 4 | 5 | from typing import Any, List, Literal 6 | 7 | from ....utils.class_registry import register_class 8 | from .base import RoundCharacterStatus 9 | from ...modifiable_values import DamageIncreaseValue 10 | from ...consts import DamageElementalType, IconType 11 | from ...event import MakeDamageEventArguments 12 | from ...action import Actions 13 | 14 | 15 | class Frozen_3_3(RoundCharacterStatus): 16 | """ 17 | Frozen. 18 | """ 19 | 20 | name: Literal["Frozen"] = "Frozen" 21 | version: Literal["3.3"] = "3.3" 22 | usage: int = 1 23 | max_usage: int = 1 24 | icon_type: Literal[IconType.FROZEN] = IconType.FROZEN 25 | 26 | def value_modifier_DAMAGE_INCREASE( 27 | self, value: DamageIncreaseValue, match: Any, mode: Literal["TEST", "REAL"] 28 | ) -> DamageIncreaseValue: 29 | """ 30 | Increase damage for pyro and physical damages to self by 2, and 31 | decrease usage. 32 | """ 33 | if not value.is_corresponding_character_receive_damage(self.position, match): 34 | # not this character receive damage, not modify 35 | return value 36 | if ( 37 | value.damage_elemental_type 38 | in [ 39 | DamageElementalType.PYRO, 40 | DamageElementalType.PHYSICAL, 41 | ] 42 | and self.usage > 0 43 | ): 44 | value.damage += 2 45 | assert mode == "REAL" 46 | self.usage -= 1 47 | return value 48 | 49 | def event_handler_MAKE_DAMAGE( 50 | self, event: MakeDamageEventArguments, match: Any 51 | ) -> List[Actions]: 52 | """ 53 | When damage made, check whether the status should be removed. 54 | Not trigger on AFTER_MAKE_DAMAGE because when damage made, run out 55 | of usage, but new one is generated, should remove first then generate 56 | new one, otherwise newly updated status will be removed. 57 | """ 58 | return list(self.check_should_remove()) 59 | 60 | 61 | register_class(Frozen_3_3) 62 | -------------------------------------------------------------------------------- /src/lpsim/server/status/character_status/weapons.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ...action import RemoveObjectAction 6 | 7 | from ...event import MoveObjectEventArguments, UseSkillEventArguments 8 | 9 | from ...consts import CostLabels, IconType 10 | 11 | from ...modifiable_values import CostValue 12 | from .base import RoundCharacterStatus, ShieldCharacterStatus 13 | 14 | 15 | class LithicSpear_3_3(ShieldCharacterStatus): 16 | name: Literal["Lithic Spear"] = "Lithic Spear" 17 | version: Literal["3.3"] = "3.3" 18 | usage: int 19 | max_usage: int 20 | 21 | def __init__(self, *args, **kwargs) -> None: 22 | super().__init__(*args, **kwargs) 23 | 24 | def renew(self, object: "LithicSpear_3_3") -> None: 25 | self.max_usage = object.max_usage 26 | self.usage = max(object.usage, self.usage) 27 | self.usage = min(self.max_usage, self.usage) 28 | 29 | 30 | class KingsSquire_4_0(RoundCharacterStatus): 31 | name: Literal["King's Squire"] = "King's Squire" 32 | version: Literal["4.0"] = "4.0" 33 | usage: int = 1 34 | max_usage: int = 1 35 | icon_type: Literal[IconType.BUFF] = IconType.BUFF 36 | 37 | def value_modifier_COST( 38 | self, value: CostValue, match: Any, mode: Literal["TEST", "REAL"] 39 | ) -> CostValue: 40 | """ 41 | If self use elemental skill, or equip talent, cost -2. 42 | """ 43 | if not self._check_value_self_skill_or_talent( 44 | value, match, CostLabels.ELEMENTAL_SKILL.value 45 | ): 46 | return value 47 | # decrease cost 48 | assert self.usage > 0 49 | decrease_result = [ 50 | value.cost.decrease_cost(value.cost.elemental_dice_color), 51 | value.cost.decrease_cost(value.cost.elemental_dice_color), 52 | ] 53 | if True in decrease_result: # pragma: no branch 54 | # decrease cost success 55 | if mode == "REAL": 56 | self.usage -= 1 57 | return value 58 | 59 | def event_handler_USE_SKILL( 60 | self, event: UseSkillEventArguments, match: Any 61 | ) -> List[RemoveObjectAction]: 62 | return self.check_should_remove() 63 | 64 | def event_handler_MOVE_OBJECT( 65 | self, event: MoveObjectEventArguments, match: Any 66 | ) -> List[RemoveObjectAction]: 67 | return self.check_should_remove() 68 | 69 | 70 | class Moonpiercer_4_1(KingsSquire_4_0): 71 | name: Literal["Moonpiercer"] = "Moonpiercer" 72 | version: Literal["4.1"] = "4.1" 73 | 74 | 75 | register_class(LithicSpear_3_3 | KingsSquire_4_0 | Moonpiercer_4_1) 76 | -------------------------------------------------------------------------------- /src/lpsim/server/status/team_status/__init__.py: -------------------------------------------------------------------------------- 1 | from ....utils import import_all_modules 2 | from .base import TeamStatusBase 3 | 4 | 5 | import_all_modules(__file__, __name__) 6 | __all__ = ("TeamStatusBase",) 7 | -------------------------------------------------------------------------------- /src/lpsim/server/status/team_status/electro_characters.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ...struct import Cost 6 | 7 | from ...consts import DamageElementalType, DamageType, IconType, SkillType 8 | 9 | from ...modifiable_values import DamageDecreaseValue, DamageValue 10 | 11 | from ...action import MakeDamageAction 12 | 13 | from ...event import PlayerActionStartEventArguments 14 | from .base import ExtraAttackTeamStatus, RoundTeamStatus, UsageTeamStatus 15 | 16 | 17 | class TenkoThunderbolts_3_7(UsageTeamStatus): 18 | name: Literal["Tenko Thunderbolts"] = "Tenko Thunderbolts" 19 | version: Literal["3.7"] = "3.7" 20 | usage: int = 1 21 | max_usage: int = 1 22 | icon_type: Literal[IconType.OTHERS] = IconType.OTHERS 23 | 24 | def event_handler_PLAYER_ACTION_START( 25 | self, event: PlayerActionStartEventArguments, match: Any 26 | ) -> List[MakeDamageAction]: 27 | """ 28 | Attack opponent active character 29 | """ 30 | if self.position.player_idx != event.player_idx: 31 | # not our turn 32 | return [] 33 | target_table = match.player_tables[1 - self.position.player_idx] 34 | target_character_idx = target_table.active_character_idx 35 | target_character = target_table.characters[target_character_idx] 36 | self.usage -= 1 37 | return [ 38 | MakeDamageAction( 39 | damage_value_list=[ 40 | DamageValue( 41 | position=self.position, 42 | damage_type=DamageType.DAMAGE, 43 | target_position=target_character.position, 44 | damage=3, 45 | damage_elemental_type=DamageElementalType.ELECTRO, 46 | cost=Cost(), 47 | ) 48 | ], 49 | ) 50 | ] 51 | 52 | 53 | class ThunderbeastsTarge_3_4(RoundTeamStatus, ExtraAttackTeamStatus): 54 | name: Literal["Thunderbeast's Targe"] = "Thunderbeast's Targe" 55 | version: Literal["3.4"] = "3.4" 56 | usage: int = 2 57 | max_usage: int = 2 58 | icon_type: Literal[IconType.OTHERS] = IconType.OTHERS 59 | 60 | trigger_skill_type: SkillType | None = SkillType.NORMAL_ATTACK 61 | damage: int = 1 62 | damage_elemental_type: DamageElementalType = DamageElementalType.ELECTRO 63 | decrease_usage: bool = False 64 | 65 | def value_modifier_DAMAGE_DECREASE( 66 | self, 67 | value: DamageDecreaseValue, 68 | match: Any, 69 | mode: Literal["TEST", "REAL"], 70 | ) -> DamageDecreaseValue: 71 | """ 72 | If this character receives damage, and not piercing, and greater than 73 | 3, decrease damage by 1 74 | """ 75 | if ( 76 | value.is_corresponding_character_receive_damage( 77 | self.position, 78 | match, 79 | ) 80 | and value.damage >= 3 81 | ): 82 | value.damage -= 1 83 | return value 84 | 85 | 86 | register_class(TenkoThunderbolts_3_7 | ThunderbeastsTarge_3_4) 87 | -------------------------------------------------------------------------------- /src/lpsim/server/status/team_status/weapons.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Literal 2 | 3 | from ....utils.class_registry import register_class 4 | 5 | from ...consts import IconType 6 | 7 | from ...modifiable_values import DamageIncreaseValue 8 | from .base import RoundTeamStatus, ShieldTeamStatus 9 | 10 | 11 | class RebelliousShield_3_7(ShieldTeamStatus): 12 | name: Literal["Rebellious Shield"] = "Rebellious Shield" 13 | version: Literal["3.7"] = "3.7" 14 | usage: int = 1 15 | max_usage: int = 2 16 | 17 | 18 | class MillennialMovementFarewellSong_3_7(RoundTeamStatus): 19 | name: Literal[ 20 | "Millennial Movement: Farewell Song" 21 | ] = "Millennial Movement: Farewell Song" 22 | version: Literal["3.7"] = "3.7" 23 | usage: int = 2 24 | max_usage: int = 2 25 | icon_type: Literal[IconType.ATK_UP] = IconType.ATK_UP 26 | 27 | def value_modifier_DAMAGE_INCREASE( 28 | self, value: DamageIncreaseValue, match: Any, mode: Literal["TEST", "REAL"] 29 | ) -> DamageIncreaseValue: 30 | if value.is_corresponding_character_use_damage_skill( 31 | self.position, match, None 32 | ): 33 | assert mode == "REAL" 34 | value.damage += 1 35 | return value 36 | 37 | 38 | register_class(RebelliousShield_3_7 | MillennialMovementFarewellSong_3_7) 39 | -------------------------------------------------------------------------------- /src/lpsim/server/summon/__init__.py: -------------------------------------------------------------------------------- 1 | from ...utils import import_all_modules 2 | from .base import SummonBase 3 | 4 | 5 | import_all_modules(__file__, __name__) 6 | __all__ = ("SummonBase",) 7 | -------------------------------------------------------------------------------- /src/lpsim/server/summon/events.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | from ...utils.class_registry import register_class 4 | 5 | from ..consts import DamageElementalType 6 | from .base import AttackerSummonBase 7 | 8 | 9 | class CryoHilichurlShooter_3_3(AttackerSummonBase): 10 | name: Literal["Cryo Hilichurl Shooter"] = "Cryo Hilichurl Shooter" 11 | version: Literal["3.3"] = "3.3" 12 | usage: int = 2 13 | max_usage: int = 2 14 | damage_elemental_type: DamageElementalType = DamageElementalType.CRYO 15 | damage: int = 1 16 | 17 | 18 | class ElectroHilichurlShooter_3_3(AttackerSummonBase): 19 | name: Literal["Electro Hilichurl Shooter"] = "Electro Hilichurl Shooter" 20 | version: Literal["3.3"] = "3.3" 21 | usage: int = 2 22 | max_usage: int = 2 23 | damage_elemental_type: DamageElementalType = DamageElementalType.ELECTRO 24 | damage: int = 1 25 | 26 | 27 | class HilichurlBerserker_3_3(AttackerSummonBase): 28 | name: Literal["Hilichurl Berserker"] = "Hilichurl Berserker" 29 | version: Literal["3.3"] = "3.3" 30 | usage: int = 2 31 | max_usage: int = 2 32 | damage_elemental_type: DamageElementalType = DamageElementalType.PYRO 33 | damage: int = 1 34 | 35 | 36 | class HydroSamachurl_3_3(AttackerSummonBase): 37 | name: Literal["Hydro Samachurl"] = "Hydro Samachurl" 38 | version: Literal["3.3"] = "3.3" 39 | usage: int = 2 40 | max_usage: int = 2 41 | damage_elemental_type: DamageElementalType = DamageElementalType.HYDRO 42 | damage: int = 1 43 | 44 | 45 | register_class( 46 | CryoHilichurlShooter_3_3 47 | | ElectroHilichurlShooter_3_3 48 | | HilichurlBerserker_3_3 49 | | HydroSamachurl_3_3 50 | ) 51 | -------------------------------------------------------------------------------- /src/lpsim/server/summon/system.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | from ...utils.class_registry import register_class 4 | from .base import AttackerSummonBase 5 | from ..consts import DamageElementalType 6 | 7 | 8 | class BurningFlame_3_3(AttackerSummonBase): 9 | name: Literal["Burning Flame"] = "Burning Flame" 10 | version: Literal["3.3"] = "3.3" 11 | usage: int = 1 12 | max_usage: int = 2 13 | damage_elemental_type: DamageElementalType = DamageElementalType.PYRO 14 | damage: int = 1 15 | renew_type: Literal["ADD"] = "ADD" 16 | 17 | 18 | register_class(BurningFlame_3_3) 19 | -------------------------------------------------------------------------------- /src/lpsim/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import pydantic 2 | import os 3 | import importlib 4 | from typing import List, Literal, get_origin, get_type_hints 5 | from .class_registry import ( # noqa: F401 6 | register_class, 7 | get_instance, 8 | get_class_list_by_base_class, 9 | ) 10 | from .desc_registry import ( # noqa: F401 11 | DescDictType, 12 | ExpectedLanguageType, 13 | update_desc, 14 | desc_exist, 15 | get_desc_patch, 16 | ) 17 | from .deck_code import ( # noqa: F401 18 | deck_code_to_deck_str, 19 | deck_str_to_deck_code, 20 | ) 21 | 22 | 23 | class BaseModel(pydantic.BaseModel): 24 | class Config: 25 | extra = pydantic.Extra.forbid # default forbid extra fields 26 | 27 | 28 | def list_unique_range_right(data: List[int], minn: int, maxn: int) -> bool: 29 | """ 30 | Check if the list is unique and in range [minn, maxn). 31 | """ 32 | if len(data) != len(set(data)): 33 | return False 34 | for i in data: 35 | if i < minn or i >= maxn: 36 | return False 37 | return True 38 | 39 | 40 | def import_all_modules(py_file: str, name: str, exceptions: set[str] = set()) -> None: 41 | """ 42 | This function is used to activate all modules in the directory for __init__ 43 | and they can do default actions, i.e. register themselves to the class 44 | registry. To expose inner modules and classes, cannot use this function, 45 | import manually instead. 46 | """ 47 | for file in os.listdir(os.path.dirname(py_file)): 48 | if file[0] == "_" or file[0] == ".": 49 | continue 50 | if file.endswith(".py"): 51 | file = file[:-3] 52 | if file not in exceptions: # pragma: no branch 53 | importlib.import_module("." + file, package=name) 54 | 55 | 56 | def accept_same_or_higher_version(cls, v: str, values): 57 | msg = f"class {cls}: version annotation must be Literal with one str. " 58 | if not isinstance(v, str): 59 | raise ValueError(msg + f"v is {type(v)} not str") 60 | version_hints = get_type_hints(cls)["version"] 61 | if get_origin(version_hints) != Literal: 62 | raise ValueError(msg + f"version_hints is {version_hints}") 63 | version_hints = version_hints.__args__ 64 | if len(version_hints) > 1: 65 | raise ValueError(msg + f"version_hints is {version_hints}") 66 | version_hint = version_hints[0] 67 | if values["strict_version_validation"] and v != version_hint: 68 | raise ValueError(f"version {v} is not equal to {version_hint}") 69 | if v < version_hint: 70 | raise ValueError(f"version {v} is lower than {version_hint}") 71 | return version_hint 72 | 73 | 74 | if __name__ == "__main__": 75 | pass 76 | -------------------------------------------------------------------------------- /templates/card.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Literal 2 | from lpsim.server.action import DrawCardAction 3 | from lpsim.server.object_base import EventCardBase 4 | from lpsim.server.struct import Cost, ObjectPosition 5 | from lpsim.utils.class_registry import register_class 6 | from lpsim.utils.desc_registry import DescDictType 7 | 8 | 9 | class BigStrategize_3_3(EventCardBase): 10 | """ 11 | Big Strategize 12 | """ 13 | name: Literal['Big Strategize'] = 'Big Strategize' 14 | version: Literal['3.3'] = '3.3' 15 | cost = Cost( 16 | any_dice_number = 2 17 | ) 18 | 19 | def get_targets(self, match: Any) -> List[ObjectPosition]: 20 | # no targets 21 | return [] 22 | 23 | def get_actions( 24 | self, target: ObjectPosition | None, match: Any 25 | ) -> List[DrawCardAction]: 26 | """ 27 | Act the card. Draw three cards. 28 | """ 29 | assert target is None # no targets 30 | return [DrawCardAction( 31 | player_idx = self.position.player_idx, 32 | number = 3, 33 | draw_if_filtered_not_enough = True 34 | )] 35 | 36 | 37 | card_descs: Dict[str, DescDictType] = { 38 | 'CARD/Big Strategize': { 39 | 'names': { 40 | 'zh-CN': '大心海', 41 | 'en-US': 'Big Strategize', 42 | }, 43 | "descs": { 44 | "3.3": { 45 | "zh-CN": "抓3张牌。", 46 | "en-US": "Draw 3 cards." 47 | } 48 | }, 49 | "id": 332099, 50 | "image_path": "cardface/Event_Event_Yunchouweiwo.png" 51 | } 52 | } 53 | 54 | 55 | register_class(BigStrategize_3_3, card_descs) 56 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LPSim/backend/0866b9b2a0a6a696eeb8291652df7fb36eeec3c9/tests/__init__.py -------------------------------------------------------------------------------- /tests/server/bugfix/test_boar_talent.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | get_test_id_from_command, 5 | make_respond, 6 | set_16_omni, 7 | read_from_log_json, 8 | ) 9 | 10 | 11 | def test_boar_talent(): 12 | match, agent_0, agent_1 = read_from_log_json( 13 | os.path.join(os.path.dirname(__file__), "jsons", "test_boar_talent.json") # noqa: E501 14 | ) 15 | match.version = "0.0.4" 16 | match.config.history_level = 0 17 | # modify hp 18 | # for i in range(2): 19 | # characters = match.player_tables[i].player_deck_information.characters # noqa: E501 20 | # for c in characters: 21 | # c.hp = c.max_hp = 30 22 | # add omnipotent guide 23 | set_16_omni(match) 24 | match.start() 25 | match.step() 26 | new_commands = [[], []] 27 | while True: 28 | if match.need_respond(0): 29 | agent = agent_0 30 | nc = new_commands[0] 31 | elif match.need_respond(1): 32 | agent = agent_1 33 | nc = new_commands[1] 34 | else: 35 | raise AssertionError("No need respond.") 36 | # do tests 37 | while True: 38 | nc.append(agent.commands[0]) 39 | cmd = agent.commands[0].strip().split(" ") 40 | test_id = get_test_id_from_command(agent) 41 | if test_id == 0: 42 | # id 0 means current command is not a test command. 43 | break 44 | elif test_id == 7: 45 | pidx = int(cmd[2][1]) 46 | d = {} 47 | for c in match.player_tables[pidx].dice.colors: 48 | d[c.value] = d.get(c.value, 0) + 1 49 | for c in cmd[3:]: 50 | d[c] -= 1 51 | if d[c] == 0: 52 | del d[c] 53 | assert len(d) == 0 54 | else: 55 | raise AssertionError(f"Unknown test id {test_id}") 56 | # respond 57 | make_respond(agent, match) 58 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 59 | break 60 | 61 | # simulate ends, check final state 62 | assert match.state != MatchState.ERROR 63 | 64 | 65 | if __name__ == "__main__": 66 | test_boar_talent() 67 | -------------------------------------------------------------------------------- /tests/server/bugfix/test_counter_reset_when_revive.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_hp, 5 | check_usage, 6 | get_pidx_cidx, 7 | get_test_id_from_command, 8 | make_respond, 9 | set_16_omni, 10 | read_from_log_json, 11 | ) 12 | 13 | 14 | def test_counter_reset_when_revive(): 15 | match, agent_0, agent_1 = read_from_log_json( 16 | os.path.join( 17 | os.path.dirname(__file__), "jsons", "test_counter_reset_when_revive.json" 18 | ) # noqa: E501 19 | ) 20 | match.version = "0.0.4" 21 | match.config.history_level = 0 22 | # modify hp 23 | # for i in range(2): 24 | # characters = match.player_tables[i].player_deck_information.characters # noqa: E501 25 | # for c in characters: 26 | # c.hp = c.max_hp = 30 27 | # add omnipotent guide 28 | set_16_omni(match) 29 | match.start() 30 | match.step() 31 | new_commands = [[], []] 32 | while True: 33 | if match.need_respond(0): 34 | agent = agent_0 35 | nc = new_commands[0] 36 | elif match.need_respond(1): 37 | agent = agent_1 38 | nc = new_commands[1] 39 | else: 40 | raise AssertionError("No need respond.") 41 | # do tests 42 | while True: 43 | nc.append(agent.commands[0]) 44 | cmd = agent.commands[0].strip().split(" ") 45 | test_id = get_test_id_from_command(agent) 46 | if test_id == 0: 47 | # id 0 means current command is not a test command. 48 | break 49 | elif test_id == 1: 50 | hps = cmd[2:] 51 | hps = [int(x) for x in hps] 52 | hps = [hps[:5], hps[5:]] 53 | check_hp(match, hps) 54 | elif test_id == 5: 55 | pidx, cidx = get_pidx_cidx(cmd) 56 | check_usage(match.player_tables[pidx].characters[cidx].status, cmd[3:]) 57 | else: 58 | raise AssertionError(f"Unknown test id {test_id}") 59 | # respond 60 | make_respond(agent, match) 61 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 62 | break 63 | 64 | # simulate ends, check final state 65 | assert match.state != MatchState.ERROR 66 | 67 | 68 | if __name__ == "__main__": 69 | test_counter_reset_when_revive() 70 | -------------------------------------------------------------------------------- /tests/server/bugfix/test_dunyarzad_no_draw.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | get_test_id_from_command, 5 | make_respond, 6 | set_16_omni, 7 | read_from_log_json, 8 | ) 9 | 10 | 11 | def test_dunyarzad_no_draw(): 12 | match, agent_0, agent_1 = read_from_log_json( 13 | os.path.join(os.path.dirname(__file__), "jsons", "test_dunyarzad_no_draw.json") # noqa: E501 14 | ) 15 | match.config.history_level = 0 16 | # modify hp 17 | # for i in range(2): 18 | # characters = match.player_tables[i].player_deck_information.characters # noqa: E501 19 | # for c in characters: 20 | # c.hp = c.max_hp = 30 21 | # add omnipotent guide 22 | set_16_omni(match) 23 | match.start() 24 | match.step() 25 | new_commands = [[], []] 26 | while True: 27 | if match.need_respond(0): 28 | agent = agent_0 29 | nc = new_commands[0] 30 | elif match.need_respond(1): 31 | agent = agent_1 32 | nc = new_commands[1] 33 | else: 34 | raise AssertionError("No need respond.") 35 | # do tests 36 | while True: 37 | nc.append(agent.commands[0]) 38 | cmd = agent.commands[0].strip().split(" ") 39 | test_id = get_test_id_from_command(agent) 40 | if test_id == 0: 41 | # id 0 means current command is not a test command. 42 | break 43 | elif test_id == 6: 44 | pidx = int(cmd[2][1]) 45 | assert len(match.player_tables[pidx].hands) == int(cmd[3]) 46 | else: 47 | raise AssertionError(f"Unknown test id {test_id}") 48 | # respond 49 | make_respond(agent, match) 50 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 51 | break 52 | 53 | # simulate ends, check final state 54 | assert match.state != MatchState.ERROR 55 | 56 | 57 | if __name__ == "__main__": 58 | test_dunyarzad_no_draw() 59 | -------------------------------------------------------------------------------- /tests/server/bugfix/test_dvalin_talent.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_hp, 5 | check_usage, 6 | get_pidx_cidx, 7 | get_test_id_from_command, 8 | make_respond, 9 | set_16_omni, 10 | read_from_log_json, 11 | ) 12 | 13 | 14 | def test_dvalin_talent(): 15 | match, agent_0, agent_1 = read_from_log_json( 16 | os.path.join(os.path.dirname(__file__), "jsons", "test_dvalin_talent.json") # noqa: E501 17 | ) 18 | match.config.history_level = 0 19 | # modify hp 20 | # for i in range(2): 21 | # characters = match.player_tables[i].player_deck_information.characters # noqa: E501 22 | # for c in characters: 23 | # c.hp = c.max_hp = 30 24 | # add omnipotent guide 25 | set_16_omni(match) 26 | match.start() 27 | match.step() 28 | new_commands = [[], []] 29 | while True: 30 | if match.need_respond(0): 31 | agent = agent_0 32 | nc = new_commands[0] 33 | elif match.need_respond(1): 34 | agent = agent_1 35 | nc = new_commands[1] 36 | else: 37 | raise AssertionError("No need respond.") 38 | # do tests 39 | while True: 40 | nc.append(agent.commands[0]) 41 | cmd = agent.commands[0].strip().split(" ") 42 | test_id = get_test_id_from_command(agent) 43 | if test_id == 0: 44 | # id 0 means current command is not a test command. 45 | break 46 | elif test_id == 1: 47 | hps = cmd[2:] 48 | hps = [int(x) for x in hps] 49 | hps = [hps[:3], hps[3:]] 50 | check_hp(match, hps) 51 | elif test_id == 5: 52 | pidx, cidx = get_pidx_cidx(cmd) 53 | check_usage(match.player_tables[pidx].characters[cidx].status, cmd[3:]) 54 | else: 55 | raise AssertionError(f"Unknown test id {test_id}") 56 | # respond 57 | make_respond(agent, match) 58 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 59 | break 60 | 61 | # simulate ends, check final state 62 | assert match.state != MatchState.ERROR 63 | 64 | 65 | if __name__ == "__main__": 66 | test_dvalin_talent() 67 | -------------------------------------------------------------------------------- /tests/server/bugfix/test_dvalin_talent_2.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_hp, 5 | check_usage, 6 | get_pidx_cidx, 7 | get_test_id_from_command, 8 | make_respond, 9 | set_16_omni, 10 | read_from_log_json, 11 | ) 12 | 13 | 14 | def test_dvalin_talent_2(): 15 | json_fname = "test_dvalin_talent_2.json" 16 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 17 | match, agent_0, agent_1 = read_from_log_json(json_path) 18 | match.config.history_level = 0 19 | # modify hp 20 | # for i in range(2): 21 | # characters = match.player_tables[i].player_deck_information.characters # noqa: E501 22 | # for c in characters: 23 | # c.hp = c.max_hp = 30 24 | # add omnipotent guide 25 | set_16_omni(match) 26 | match.start() 27 | match.step() 28 | new_commands = [[], []] 29 | while True: 30 | if match.need_respond(0): 31 | agent = agent_0 32 | nc = new_commands[0] 33 | elif match.need_respond(1): 34 | agent = agent_1 35 | nc = new_commands[1] 36 | else: 37 | raise AssertionError("No need respond.") 38 | # do tests 39 | while True: 40 | nc.append(agent.commands[0]) 41 | cmd = agent.commands[0].strip().split(" ") 42 | test_id = get_test_id_from_command(agent) 43 | if test_id == 0: 44 | # id 0 means current command is not a test command. 45 | break 46 | elif test_id == 1: 47 | hps = cmd[2:] 48 | hps = [int(x) for x in hps] 49 | hps = [hps[:3], hps[3:]] 50 | check_hp(match, hps) 51 | elif test_id == 5: 52 | pidx, cidx = get_pidx_cidx(cmd) 53 | check_usage(match.player_tables[pidx].characters[cidx].status, cmd[3:]) 54 | else: 55 | raise AssertionError(f"Unknown test id {test_id}") 56 | # respond 57 | make_respond(agent, match) 58 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 59 | break 60 | 61 | # simulate ends, check final state 62 | assert match.state != MatchState.ERROR 63 | 64 | 65 | if __name__ == "__main__": 66 | test_dvalin_talent_2() 67 | -------------------------------------------------------------------------------- /tests/server/bugfix/test_json_bugfix.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tests.utils_for_test import do_log_tests 3 | 4 | 5 | def test_issue_106(): 6 | json_fname = "test_issue_106.json" 7 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 8 | do_log_tests(json_path, match_version="0.0.4") 9 | 10 | 11 | def test_issue_82(): 12 | json_fname = "test_issue_82.json" 13 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 14 | do_log_tests(json_path) 15 | 16 | 17 | if __name__ == "__main__": 18 | test_issue_106() 19 | test_issue_82() 20 | -------------------------------------------------------------------------------- /tests/server/bugfix/test_kazuha_attack_by_baizhu_q.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_hp, 5 | get_test_id_from_command, 6 | make_respond, 7 | set_16_omni, 8 | read_from_log_json, 9 | ) 10 | 11 | 12 | def test_kazuha_attack_by_baizhu_q(): 13 | json_fname = "test_kazuha_attack_by_baizhu_q.json" 14 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 15 | match, agent_0, agent_1 = read_from_log_json(json_path) 16 | match.config.history_level = 0 17 | # modify hp 18 | # for i in range(2): 19 | # characters = match.player_tables[i].player_deck_information.characters # noqa: E501 20 | # for c in characters: 21 | # c.hp = c.max_hp = 30 22 | # add omnipotent guide 23 | set_16_omni(match) 24 | match.start() 25 | match.step() 26 | new_commands = [[], []] 27 | while True: 28 | if match.need_respond(0): 29 | agent = agent_0 30 | nc = new_commands[0] 31 | elif match.need_respond(1): 32 | agent = agent_1 33 | nc = new_commands[1] 34 | else: 35 | raise AssertionError("No need respond.") 36 | # do tests 37 | while True: 38 | nc.append(agent.commands[0]) 39 | cmd = agent.commands[0].strip().split(" ") 40 | test_id = get_test_id_from_command(agent) 41 | if test_id == 0: 42 | # id 0 means current command is not a test command. 43 | break 44 | elif test_id == 1: 45 | hps = cmd[2:] 46 | hps = [int(x) for x in hps] 47 | hps = [hps[:3], hps[3:]] 48 | check_hp(match, hps) 49 | else: 50 | raise AssertionError(f"Unknown test id {test_id}") 51 | # respond 52 | make_respond(agent, match) 53 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 54 | break 55 | 56 | # simulate ends, check final state 57 | assert match.state != MatchState.ERROR 58 | 59 | 60 | if __name__ == "__main__": 61 | test_kazuha_attack_by_baizhu_q() 62 | -------------------------------------------------------------------------------- /tests/server/bugfix/test_nilou_dendro_core.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_hp, 5 | check_usage, 6 | get_test_id_from_command, 7 | make_respond, 8 | set_16_omni, 9 | read_from_log_json, 10 | ) 11 | 12 | 13 | def test_nilou_dendro_core(): 14 | json_fname = "test_nilou_dendro_core.json" 15 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 16 | match, agent_0, agent_1 = read_from_log_json(json_path) 17 | match.version = "0.0.4" 18 | match.config.history_level = 0 19 | # modify hp 20 | # for i in range(2): 21 | # characters = match.player_tables[i].player_deck_information.characters # noqa: E501 22 | # for c in characters: 23 | # c.hp = c.max_hp = 30 24 | # add omnipotent guide 25 | set_16_omni(match) 26 | match.start() 27 | match.step() 28 | new_commands = [[], []] 29 | while True: 30 | if match.need_respond(0): 31 | agent = agent_0 32 | nc = new_commands[0] 33 | elif match.need_respond(1): 34 | agent = agent_1 35 | nc = new_commands[1] 36 | else: 37 | raise AssertionError("No need respond.") 38 | # do tests 39 | while True: 40 | nc.append(agent.commands[0]) 41 | cmd = agent.commands[0].strip().split(" ") 42 | test_id = get_test_id_from_command(agent) 43 | if test_id == 0: 44 | # id 0 means current command is not a test command. 45 | break 46 | elif test_id == 1: 47 | hps = cmd[2:] 48 | hps = [int(x) for x in hps] 49 | hps = [hps[:3], hps[3:]] 50 | check_hp(match, hps) 51 | elif test_id == 4: 52 | pidx = int(cmd[2][1]) 53 | check_usage(match.player_tables[pidx].team_status, cmd[3:]) 54 | else: 55 | raise AssertionError(f"Unknown test id {test_id}") 56 | # respond 57 | make_respond(agent, match) 58 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 59 | break 60 | 61 | # simulate ends, check final state 62 | assert match.state != MatchState.ERROR 63 | 64 | 65 | if __name__ == "__main__": 66 | test_nilou_dendro_core() 67 | -------------------------------------------------------------------------------- /tests/server/bugfix/test_trigger_order_in_character.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_hp, 5 | get_test_id_from_command, 6 | make_respond, 7 | set_16_omni, 8 | read_from_log_json, 9 | ) 10 | 11 | 12 | def test_trigger_order_in_character(): 13 | match, agent_0, agent_1 = read_from_log_json( 14 | os.path.join( 15 | os.path.dirname(__file__), "jsons", "test_trigger_order_in_character.json" 16 | ) # noqa: E501 17 | ) 18 | match.config.history_level = 0 19 | # modify hp 20 | # for i in range(2): 21 | # characters = match.player_tables[i].player_deck_information.characters # noqa: E501 22 | # for c in characters: 23 | # c.hp = c.max_hp = 30 24 | # add omnipotent guide 25 | set_16_omni(match) 26 | match.start() 27 | match.step() 28 | new_commands = [[], []] 29 | while True: 30 | if match.need_respond(0): 31 | agent = agent_0 32 | nc = new_commands[0] 33 | elif match.need_respond(1): 34 | agent = agent_1 35 | nc = new_commands[1] 36 | else: 37 | raise AssertionError("No need respond.") 38 | # do tests 39 | while True: 40 | nc.append(agent.commands[0]) 41 | cmd = agent.commands[0].strip().split(" ") 42 | test_id = get_test_id_from_command(agent) 43 | if test_id == 0: 44 | # id 0 means current command is not a test command. 45 | break 46 | elif test_id == 1: 47 | hps = cmd[2:] 48 | hps = [int(x) for x in hps] 49 | hps = [hps[:3], hps[3:]] 50 | check_hp(match, hps) 51 | else: 52 | raise AssertionError(f"Unknown test id {test_id}") 53 | # respond 54 | make_respond(agent, match) 55 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 56 | break 57 | 58 | # simulate ends, check final state 59 | assert match.state != MatchState.ERROR 60 | 61 | 62 | if __name__ == "__main__": 63 | test_trigger_order_in_character() 64 | -------------------------------------------------------------------------------- /tests/server/characters/test_mobs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from lpsim.server.deck import Deck 3 | 4 | 5 | def test_create_mob(): 6 | TEST_DECK = { 7 | "name": "Deck", 8 | "characters": [ 9 | { 10 | "name": "PhysicalMob", 11 | "element": "ELECTRO", 12 | }, 13 | { 14 | "name": "ElectroMobMage", 15 | "element": "ELECTRO", 16 | }, 17 | { 18 | "name": "ElectroMob", 19 | "element": "ELECTRO", 20 | }, 21 | { 22 | "name": "DendroMob", 23 | }, 24 | { 25 | "name": "DendroMobMage", 26 | }, 27 | ], 28 | "cards": [ 29 | { 30 | "name": "Strategize", 31 | } 32 | ] 33 | * 30, 34 | } 35 | Deck(**TEST_DECK) 36 | TEST_DECK["characters"][1]["element"] = "DENDRO" 37 | with pytest.raises(ValueError): 38 | Deck(**TEST_DECK) 39 | TEST_DECK["characters"][1]["element"] = "ELECTRO" 40 | TEST_DECK["characters"][2]["element"] = "DENDRO" 41 | with pytest.raises(ValueError): 42 | Deck(**TEST_DECK) 43 | TEST_DECK["characters"][2]["element"] = "ELECTRO" 44 | -------------------------------------------------------------------------------- /tests/server/patch/v43/test_alhaitham.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_hp, 5 | check_usage, 6 | get_pidx_cidx, 7 | get_test_id_from_command, 8 | make_respond, 9 | set_16_omni, 10 | read_from_log_json, 11 | ) 12 | 13 | 14 | def test_alhaitham(): 15 | match, agent_0, agent_1 = read_from_log_json( 16 | os.path.join(os.path.dirname(__file__), "jsons", "test_alhaitham.json") 17 | ) 18 | # add omnipotent guide 19 | set_16_omni(match) 20 | match.start() 21 | match.step() 22 | new_commands = [[], []] 23 | 24 | while True: 25 | if match.need_respond(0): 26 | agent = agent_0 27 | nc = new_commands[0] 28 | elif match.need_respond(1): 29 | agent = agent_1 30 | nc = new_commands[1] 31 | else: 32 | raise AssertionError("No need respond.") 33 | # do tests 34 | while True: 35 | nc.append(agent.commands[0]) 36 | cmd = agent.commands[0].strip().split(" ") 37 | test_id = get_test_id_from_command(agent) 38 | if test_id == 0: 39 | # id 0 means current command is not a test command. 40 | break 41 | elif test_id == 1: 42 | hps = cmd[2:] 43 | hps = [int(x) for x in hps] 44 | hps = [hps[:3], hps[3:]] 45 | check_hp(match, hps) 46 | elif test_id == 2: 47 | pidx = int(cmd[2][1]) 48 | check_usage(match.player_tables[pidx].summons, cmd[3:]) 49 | elif test_id == 3: 50 | pidx = int(cmd[2][1]) 51 | check_usage(match.player_tables[pidx].supports, cmd[3:]) 52 | elif test_id == 4: 53 | pidx = int(cmd[2][1]) 54 | check_usage(match.player_tables[pidx].team_status, cmd[3:]) 55 | elif test_id == 5: 56 | pidx, cidx = get_pidx_cidx(cmd) 57 | check_usage(match.player_tables[pidx].characters[cidx].status, cmd[3:]) 58 | elif test_id == 6: 59 | pidx = int(cmd[2][1]) 60 | assert len(match.player_tables[pidx].hands) == int(cmd[3]) 61 | else: 62 | raise AssertionError(f"Unknown test id {test_id}") 63 | # respond 64 | make_respond(agent, match) 65 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 66 | break 67 | 68 | # simulate ends, check final state 69 | assert match.state != MatchState.ERROR 70 | 71 | 72 | if __name__ == "__main__": 73 | test_alhaitham() 74 | -------------------------------------------------------------------------------- /tests/server/patch/v43/test_dvalin.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_hp, 5 | check_usage, 6 | get_pidx_cidx, 7 | get_test_id_from_command, 8 | make_respond, 9 | set_16_omni, 10 | read_from_log_json, 11 | ) 12 | 13 | 14 | def test_dvalin(): 15 | match, agent_0, agent_1 = read_from_log_json( 16 | os.path.join(os.path.dirname(__file__), "jsons", "test_dvalin.json") 17 | ) 18 | # add omnipotent guide 19 | set_16_omni(match) 20 | match.start() 21 | match.step() 22 | new_commands = [[], []] 23 | 24 | while True: 25 | if match.need_respond(0): 26 | agent = agent_0 27 | nc = new_commands[0] 28 | elif match.need_respond(1): 29 | agent = agent_1 30 | nc = new_commands[1] 31 | else: 32 | raise AssertionError("No need respond.") 33 | # do tests 34 | while True: 35 | nc.append(agent.commands[0]) 36 | cmd = agent.commands[0].strip().split(" ") 37 | test_id = get_test_id_from_command(agent) 38 | if test_id == 0: 39 | # id 0 means current command is not a test command. 40 | break 41 | elif test_id == 1: 42 | hps = cmd[2:] 43 | hps = [int(x) for x in hps] 44 | hps = [hps[:3], hps[3:]] 45 | check_hp(match, hps) 46 | elif test_id == 2: 47 | pidx = int(cmd[2][1]) 48 | check_usage(match.player_tables[pidx].summons, cmd[3:]) 49 | elif test_id == 3: 50 | pidx = int(cmd[2][1]) 51 | check_usage(match.player_tables[pidx].supports, cmd[3:]) 52 | elif test_id == 4: 53 | pidx = int(cmd[2][1]) 54 | check_usage(match.player_tables[pidx].team_status, cmd[3:]) 55 | elif test_id == 5: 56 | pidx, cidx = get_pidx_cidx(cmd) 57 | check_usage(match.player_tables[pidx].characters[cidx].status, cmd[3:]) 58 | elif test_id == 6: 59 | pidx = int(cmd[2][1]) 60 | assert len(match.player_tables[pidx].hands) == int(cmd[3]) 61 | else: 62 | raise AssertionError(f"Unknown test id {test_id}") 63 | # respond 64 | make_respond(agent, match) 65 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 66 | break 67 | 68 | # simulate ends, check final state 69 | assert match.state != MatchState.ERROR 70 | 71 | 72 | if __name__ == "__main__": 73 | test_dvalin() 74 | -------------------------------------------------------------------------------- /tests/server/patch/v43/test_eremite.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tests.utils_for_test import do_log_tests 3 | 4 | 5 | def test_eremite(): 6 | json_fname = "test_eremite.json" 7 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 8 | do_log_tests(json_path) 9 | 10 | 11 | if __name__ == "__main__": 12 | test_eremite() 13 | -------------------------------------------------------------------------------- /tests/server/patch/v43/test_gorou.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_hp, 5 | check_usage, 6 | get_test_id_from_command, 7 | make_respond, 8 | set_16_omni, 9 | read_from_log_json, 10 | ) 11 | 12 | 13 | def test_gorou(): 14 | match, agent_0, agent_1 = read_from_log_json( 15 | os.path.join(os.path.dirname(__file__), "jsons", "test_gorou.json") 16 | ) 17 | # add omnipotent guide 18 | set_16_omni(match) 19 | match.start() 20 | match.step() 21 | new_commands = [[], []] 22 | 23 | while True: 24 | if match.need_respond(0): 25 | agent = agent_0 26 | nc = new_commands[0] 27 | elif match.need_respond(1): 28 | agent = agent_1 29 | nc = new_commands[1] 30 | else: 31 | raise AssertionError("No need respond.") 32 | # do tests 33 | while True: 34 | nc.append(agent.commands[0]) 35 | cmd = agent.commands[0].strip().split(" ") 36 | test_id = get_test_id_from_command(agent) 37 | if test_id == 0: 38 | # id 0 means current command is not a test command. 39 | break 40 | elif test_id == 1: 41 | hps = cmd[2:] 42 | hps = [int(x) for x in hps] 43 | hps = [hps[:3], hps[3:]] 44 | check_hp(match, hps) 45 | elif test_id == 2: 46 | pidx = int(cmd[2][1]) 47 | check_usage(match.player_tables[pidx].summons, cmd[3:]) 48 | elif test_id == 4: 49 | pidx = int(cmd[2][1]) 50 | check_usage(match.player_tables[pidx].team_status, cmd[3:]) 51 | elif test_id == 6: 52 | pidx = int(cmd[2][1]) 53 | assert len(match.player_tables[pidx].hands) == int(cmd[3]) 54 | else: 55 | raise AssertionError(f"Unknown test id {test_id}") 56 | # respond 57 | make_respond(agent, match) 58 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 59 | break 60 | 61 | # simulate ends, check final state 62 | assert match.state != MatchState.ERROR 63 | 64 | 65 | if __name__ == "__main__": 66 | test_gorou() 67 | -------------------------------------------------------------------------------- /tests/server/patch/v43/test_layla_yelan.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_hp, 5 | check_usage, 6 | get_pidx_cidx, 7 | get_test_id_from_command, 8 | make_respond, 9 | set_16_omni, 10 | read_from_log_json, 11 | ) 12 | 13 | 14 | def test_layla_yelan(): 15 | match, agent_0, agent_1 = read_from_log_json( 16 | os.path.join(os.path.dirname(__file__), "jsons", "test_layla_yelan.json") 17 | ) 18 | # add omnipotent guide 19 | set_16_omni(match) 20 | match.start() 21 | match.step() 22 | new_commands = [[], []] 23 | 24 | while True: 25 | if match.need_respond(0): 26 | agent = agent_0 27 | nc = new_commands[0] 28 | elif match.need_respond(1): 29 | agent = agent_1 30 | nc = new_commands[1] 31 | else: 32 | raise AssertionError("No need respond.") 33 | # do tests 34 | while True: 35 | nc.append(agent.commands[0]) 36 | cmd = agent.commands[0].strip().split(" ") 37 | test_id = get_test_id_from_command(agent) 38 | if test_id == 0: 39 | # id 0 means current command is not a test command. 40 | break 41 | elif test_id == 1: 42 | hps = cmd[2:] 43 | hps = [int(x) for x in hps] 44 | hps = [hps[:3], hps[3:]] 45 | check_hp(match, hps) 46 | elif test_id == 2: 47 | pidx = int(cmd[2][1]) 48 | check_usage(match.player_tables[pidx].summons, cmd[3:]) 49 | elif test_id == 3: 50 | pidx = int(cmd[2][1]) 51 | check_usage(match.player_tables[pidx].supports, cmd[3:]) 52 | elif test_id == 4: 53 | pidx = int(cmd[2][1]) 54 | check_usage(match.player_tables[pidx].team_status, cmd[3:]) 55 | elif test_id == 5: 56 | pidx, cidx = get_pidx_cidx(cmd) 57 | check_usage(match.player_tables[pidx].characters[cidx].status, cmd[3:]) 58 | elif test_id == 6: 59 | pidx = int(cmd[2][1]) 60 | assert len(match.player_tables[pidx].hands) == int(cmd[3]) 61 | elif test_id == 8: 62 | pidx, cidx = get_pidx_cidx(cmd) 63 | character = match.player_tables[pidx].characters[cidx] 64 | ele_app = character.element_application 65 | assert len(ele_app) == 0 66 | assert len(cmd[4:]) == 0 67 | else: 68 | raise AssertionError(f"Unknown test id {test_id}") 69 | # respond 70 | make_respond(agent, match) 71 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 72 | break 73 | 74 | # simulate ends, check final state 75 | assert match.state != MatchState.ERROR 76 | 77 | 78 | if __name__ == "__main__": 79 | test_layla_yelan() 80 | -------------------------------------------------------------------------------- /tests/server/patch/v43/test_lynette.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tests.utils_for_test import do_log_tests 3 | 4 | 5 | def test_lynette(): 6 | json_fname = "test_lynette.json" 7 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 8 | do_log_tests(json_path, omnipotent=False, match_version="0.0.4") 9 | 10 | 11 | if __name__ == "__main__": 12 | test_lynette() 13 | -------------------------------------------------------------------------------- /tests/server/patch/v43/test_lyney.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_hp, 5 | check_usage, 6 | get_pidx_cidx, 7 | get_test_id_from_command, 8 | make_respond, 9 | set_16_omni, 10 | read_from_log_json, 11 | ) 12 | 13 | 14 | def test_lyney(): 15 | match, agent_0, agent_1 = read_from_log_json( 16 | os.path.join(os.path.dirname(__file__), "jsons", "test_lyney.json") 17 | ) 18 | # modify hp 19 | for i in range(2): 20 | characters = match.player_tables[i].player_deck_information.characters 21 | for c in characters: 22 | c.hp = c.max_hp = 30 23 | # add omnipotent guide 24 | set_16_omni(match) 25 | match.start() 26 | match.step() 27 | 28 | while True: 29 | if match.need_respond(0): 30 | agent = agent_0 31 | elif match.need_respond(1): 32 | agent = agent_1 33 | else: 34 | raise AssertionError("No need respond.") 35 | # do tests 36 | while True: 37 | cmd = agent.commands[0].strip().split(" ") 38 | test_id = get_test_id_from_command(agent) 39 | if test_id == 0: 40 | # id 0 means current command is not a test command. 41 | break 42 | elif test_id == 1: 43 | # a sample of HP check based on the command string. 44 | hps = cmd[2:] 45 | hps = [int(x) for x in hps] 46 | hps = [hps[:3], hps[3:]] 47 | check_hp(match, hps) 48 | elif test_id == 2: 49 | pidx, cidx = get_pidx_cidx(cmd) 50 | status = match.player_tables[pidx].characters[cidx].status 51 | check_usage(status, cmd[4:]) 52 | elif test_id == 3: 53 | pidx = int(cmd[2][1]) 54 | check_usage(match.player_tables[pidx].summons, cmd[4:]) 55 | else: 56 | raise AssertionError(f"Unknown test id {test_id}") 57 | # respond 58 | make_respond(agent, match) 59 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 60 | break 61 | 62 | # simulate ends, check final state 63 | assert match.state != MatchState.ERROR 64 | 65 | 66 | if __name__ == "__main__": 67 | test_lyney() 68 | -------------------------------------------------------------------------------- /tests/server/patch/v43/test_new_4_transform.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tests.utils_for_test import do_log_tests 3 | 4 | 5 | def test_new_4_transform(): 6 | json_fname = "test_new_4_transform.json" 7 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 8 | do_log_tests(json_path) 9 | 10 | 11 | if __name__ == "__main__": 12 | test_new_4_transform() 13 | -------------------------------------------------------------------------------- /tests/server/patch/v43/test_seed_dispensary.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | get_test_id_from_command, 5 | make_respond, 6 | set_16_omni, 7 | read_from_log_json, 8 | ) 9 | 10 | 11 | def test_seed_dispensary(): 12 | match, agent_0, agent_1 = read_from_log_json( 13 | os.path.join(os.path.dirname(__file__), "jsons", "test_seed_dispensary.json") 14 | ) 15 | match.version = "0.0.4" 16 | # modify hp 17 | for i in range(2): 18 | characters = match.player_tables[i].player_deck_information.characters 19 | for c in characters: 20 | c.hp = c.max_hp = 30 21 | # add omnipotent guide 22 | set_16_omni(match) 23 | match.start() 24 | match.step() 25 | 26 | while True: 27 | if match.need_respond(0): 28 | agent = agent_0 29 | elif match.need_respond(1): 30 | agent = agent_1 31 | else: 32 | raise AssertionError("No need respond.") 33 | # do tests 34 | while True: 35 | test_id = get_test_id_from_command(agent) 36 | if test_id == 0: 37 | # id 0 means current command is not a test command. 38 | break 39 | else: 40 | raise AssertionError(f"Unknown test id {test_id}") 41 | # respond 42 | make_respond(agent, match) 43 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 44 | break 45 | 46 | # simulate ends, check final state 47 | assert match.state != MatchState.ERROR 48 | 49 | 50 | if __name__ == "__main__": 51 | test_seed_dispensary() 52 | -------------------------------------------------------------------------------- /tests/server/patch/v43/test_signora.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_hp, 5 | check_usage, 6 | get_pidx_cidx, 7 | get_test_id_from_command, 8 | make_respond, 9 | set_16_omni, 10 | read_from_log_json, 11 | ) 12 | 13 | 14 | def test_signora(): 15 | match, agent_0, agent_1 = read_from_log_json( 16 | os.path.join(os.path.dirname(__file__), "jsons", "test_signora.json") 17 | ) 18 | # add omnipotent guide 19 | set_16_omni(match) 20 | match.start() 21 | match.step() 22 | new_commands = [[], []] 23 | 24 | while True: 25 | if match.need_respond(0): 26 | agent = agent_0 27 | nc = new_commands[0] 28 | elif match.need_respond(1): 29 | agent = agent_1 30 | nc = new_commands[1] 31 | else: 32 | raise AssertionError("No need respond.") 33 | # do tests 34 | while True: 35 | nc.append(agent.commands[0]) 36 | cmd = agent.commands[0].strip().split(" ") 37 | test_id = get_test_id_from_command(agent) 38 | if test_id == 0: 39 | # id 0 means current command is not a test command. 40 | break 41 | elif test_id == 1: 42 | hps = cmd[2:] 43 | hps = [int(x) for x in hps] 44 | hps = [hps[:4], hps[4:]] 45 | check_hp(match, hps) 46 | elif test_id == 2: 47 | pidx = int(cmd[2][1]) 48 | check_usage(match.player_tables[pidx].summons, cmd[3:]) 49 | elif test_id == 3: 50 | pidx = int(cmd[2][1]) 51 | check_usage(match.player_tables[pidx].supports, cmd[3:]) 52 | elif test_id == 4: 53 | pidx = int(cmd[2][1]) 54 | check_usage(match.player_tables[pidx].team_status, cmd[3:]) 55 | elif test_id == 5: 56 | pidx, cidx = get_pidx_cidx(cmd) 57 | check_usage(match.player_tables[pidx].characters[cidx].status, cmd[3:]) 58 | elif test_id == 6: 59 | pidx = int(cmd[2][1]) 60 | assert len(match.player_tables[pidx].hands) == int(cmd[3]) 61 | elif test_id == 7: 62 | pidx = int(cmd[2][1]) 63 | d = {} 64 | for c in match.player_tables[pidx].dice.colors: 65 | d[c.value] = d.get(c.value, 0) + 1 66 | for c in cmd[3:]: 67 | d[c] -= 1 68 | if d[c] == 0: 69 | del d[c] 70 | assert len(d) == 0 71 | else: 72 | raise AssertionError(f"Unknown test id {test_id}") 73 | # respond 74 | make_respond(agent, match) 75 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 76 | break 77 | 78 | # simulate ends, check final state 79 | assert match.state != MatchState.ERROR 80 | 81 | 82 | if __name__ == "__main__": 83 | test_signora() 84 | -------------------------------------------------------------------------------- /tests/server/patch/v43/test_thunder_manifestation.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_hp, 5 | check_usage, 6 | get_pidx_cidx, 7 | get_test_id_from_command, 8 | make_respond, 9 | set_16_omni, 10 | read_from_log_json, 11 | ) 12 | 13 | 14 | def test_thunder_manifestation(): 15 | match, agent_0, agent_1 = read_from_log_json( 16 | os.path.join( 17 | os.path.dirname(__file__), "jsons", "test_thunder_manifestation.json" 18 | ) 19 | ) 20 | # add omnipotent guide 21 | set_16_omni(match) 22 | match.start() 23 | match.step() 24 | new_commands = [[], []] 25 | 26 | while True: 27 | if match.need_respond(0): 28 | agent = agent_0 29 | nc = new_commands[0] 30 | elif match.need_respond(1): 31 | agent = agent_1 32 | nc = new_commands[1] 33 | else: 34 | raise AssertionError("No need respond.") 35 | # do tests 36 | while True: 37 | nc.append(agent.commands[0]) 38 | cmd = agent.commands[0].strip().split(" ") 39 | test_id = get_test_id_from_command(agent) 40 | if test_id == 0: 41 | # id 0 means current command is not a test command. 42 | break 43 | elif test_id == 1: 44 | hps = cmd[2:] 45 | hps = [int(x) for x in hps] 46 | hps = [hps[:3], hps[3:]] 47 | check_hp(match, hps) 48 | elif test_id == 2: 49 | pidx = int(cmd[2][1]) 50 | check_usage(match.player_tables[pidx].summons, cmd[3:]) 51 | elif test_id == 3: 52 | pidx = int(cmd[2][1]) 53 | check_usage(match.player_tables[pidx].supports, cmd[3:]) 54 | elif test_id == 4: 55 | pidx = int(cmd[2][1]) 56 | check_usage(match.player_tables[pidx].team_status, cmd[3:]) 57 | elif test_id == 5: 58 | pidx, cidx = get_pidx_cidx(cmd) 59 | check_usage(match.player_tables[pidx].characters[cidx].status, cmd[3:]) 60 | elif test_id == 6: 61 | pidx = int(cmd[2][1]) 62 | assert len(match.player_tables[pidx].hands) == int(cmd[3]) 63 | else: 64 | raise AssertionError(f"Unknown test id {test_id}") 65 | # respond 66 | make_respond(agent, match) 67 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 68 | break 69 | 70 | # simulate ends, check final state 71 | assert match.state != MatchState.ERROR 72 | 73 | 74 | if __name__ == "__main__": 75 | test_thunder_manifestation() 76 | -------------------------------------------------------------------------------- /tests/server/patch/v43/test_timaeus_wagner_v43.py: -------------------------------------------------------------------------------- 1 | import os 2 | from lpsim import MatchState 3 | from tests.utils_for_test import ( 4 | check_usage, 5 | get_test_id_from_command, 6 | make_respond, 7 | set_16_omni, 8 | read_from_log_json, 9 | ) 10 | 11 | 12 | def test_timaeus_wagner_v43_draw_card(): 13 | match, agent_0, agent_1 = read_from_log_json( 14 | os.path.join(os.path.dirname(__file__), "jsons", "test_timaeus_wagner.json") 15 | ) 16 | # add omnipotent guide 17 | set_16_omni(match) 18 | match.start() 19 | match.step() 20 | new_commands = [[], []] 21 | 22 | while True: 23 | if match.need_respond(0): 24 | agent = agent_0 25 | nc = new_commands[0] 26 | elif match.need_respond(1): 27 | agent = agent_1 28 | nc = new_commands[1] 29 | else: 30 | raise AssertionError("No need respond.") 31 | # do tests 32 | while True: 33 | nc.append(agent.commands[0]) 34 | cmd = agent.commands[0].strip().split(" ") 35 | test_id = get_test_id_from_command(agent) 36 | if test_id == 0: 37 | # id 0 means current command is not a test command. 38 | break 39 | elif test_id == 3: 40 | pidx = int(cmd[2][1]) 41 | check_usage(match.player_tables[pidx].supports, cmd[3:]) 42 | elif test_id == 6: 43 | pidx = int(cmd[2][1]) 44 | assert len(match.player_tables[pidx].hands) == int(cmd[3]) 45 | else: 46 | raise AssertionError(f"Unknown test id {test_id}") 47 | # respond 48 | make_respond(agent, match) 49 | if len(agent_1.commands) == 0 and len(agent_0.commands) == 0: 50 | break 51 | 52 | # simulate ends, check final state 53 | assert match.state != MatchState.ERROR 54 | 55 | 56 | if __name__ == "__main__": 57 | test_timaeus_wagner_v43_draw_card() 58 | -------------------------------------------------------------------------------- /tests/server/patch/v44/test_json_4_4.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tests.utils_for_test import do_log_tests 3 | 4 | 5 | def test_balance(): 6 | json_fname = "balance_thuncer_vourukashas_stove.json" 7 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 8 | do_log_tests(json_path, hp_modify=10) 9 | 10 | 11 | def test_cryo_hypostasis(): 12 | json_fname = "cryo_hypostasis.json" 13 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 14 | do_log_tests(json_path, match_version="0.0.4") 15 | 16 | 17 | def millennial_999_test(match, cmd): 18 | """ 19 | p0c0 millennial is talent status 0 20 | """ 21 | status = match.player_tables[0].characters[0].status[0] 22 | assert status.desc == "talent" 23 | 24 | 25 | def test_millennial_pearl_seahorse(): 26 | json_fname = "millennial_pearl_seahorse.json" 27 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 28 | do_log_tests( 29 | json_path, other_tests={999: millennial_999_test}, match_version="0.0.4" 30 | ) 31 | 32 | 33 | def test_millennial_pearl_seahorse_2(): 34 | json_fname = "millennial_pearl_seahorse2.json" 35 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 36 | do_log_tests(json_path) 37 | 38 | 39 | def test_thoma(): 40 | json_fname = "thoma.json" 41 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 42 | do_log_tests(json_path) 43 | 44 | 45 | def test_sayu(): 46 | json_fname = "sayu.json" 47 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 48 | do_log_tests(json_path) 49 | 50 | 51 | def test_sapwood_machine_veteran(): 52 | json_fname = "sapwood_machine_veteran.json" 53 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 54 | do_log_tests(json_path, match_version="0.0.4") 55 | 56 | 57 | def test_machine_2(): 58 | json_fname = "machine_2.json" 59 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 60 | do_log_tests(json_path, match_version="0.0.4") 61 | 62 | 63 | def test_veteran_2(): 64 | json_fname = "veteran_2.json" 65 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 66 | do_log_tests(json_path, match_version="0.0.4") 67 | 68 | 69 | def test_silver(): 70 | json_fname = "silver.json" 71 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 72 | do_log_tests(json_path, match_version="0.0.4") 73 | 74 | 75 | def test_jeht_sunyata(): 76 | json_fname = "jeht_sunyata.json" 77 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 78 | do_log_tests(json_path, match_version="0.0.4") 79 | 80 | 81 | if __name__ == "__main__": 82 | # test_balance() 83 | test_cryo_hypostasis() 84 | # test_millennial_pearl_seahorse() 85 | # test_millennial_pearl_seahorse_2() 86 | test_thoma() 87 | test_sayu() 88 | # test_sapwood_machine_veteran() 89 | # test_machine_2() 90 | # test_veteran_2() 91 | # test_silver() 92 | -------------------------------------------------------------------------------- /tests/server/patch/v45/test_json_4_5.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tests.utils_for_test import do_log_tests 3 | 4 | 5 | def test_arcane_blast_v45(): 6 | json_fname = "arcane_blast_v45.json" 7 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 8 | do_log_tests(json_path, match_version="0.0.4") 9 | 10 | 11 | def test_lumenstone_enternalflow_v45(): 12 | json_fname = "lumenstone_enternalflow_v45.json" 13 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 14 | do_log_tests(json_path, match_version="0.0.4") 15 | 16 | 17 | def test_balance_v45(): 18 | json_fname = "balance_v45.json" 19 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 20 | do_log_tests(json_path) 21 | 22 | 23 | def test_meropide_golden_v45(): 24 | json_fname = "meropide_golden_v45.json" 25 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 26 | do_log_tests(json_path, match_version="0.0.4") 27 | 28 | 29 | def test_coverage_improve_v45(): 30 | json_fname = "coverage_improve_v45.json" 31 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 32 | do_log_tests(json_path, match_version="0.0.4") 33 | 34 | 35 | def test_neuvillette_v45(): 36 | json_fname = "neuvillette_v45.json" 37 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 38 | do_log_tests(json_path, match_version="0.0.4") 39 | 40 | 41 | def test_kirara_v45(): 42 | json_fname = "kirara_v45.json" 43 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 44 | do_log_tests(json_path, match_version="0.0.4") 45 | 46 | 47 | def test_charlotte_v45(): 48 | json_fname = "charlotte_v45.json" 49 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 50 | do_log_tests(json_path) 51 | 52 | 53 | def test_electro_cicin_mage_v45(): 54 | json_fname = "electro_cicin_mage_v45.json" 55 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 56 | do_log_tests(json_path, match_version="0.0.4") 57 | 58 | 59 | def test_electro_cicin_mage_2_v45(): 60 | json_fname = "electro_cicin_mage_2_v45.json" 61 | json_path = os.path.join(os.path.dirname(__file__), "jsons", json_fname) 62 | do_log_tests(json_path) 63 | 64 | 65 | if __name__ == "__main__": 66 | test_arcane_blast_v45() 67 | test_balance_v45() 68 | test_lumenstone_enternalflow_v45() 69 | test_meropide_golden_v45() 70 | test_coverage_improve_v45() 71 | test_neuvillette_v45() 72 | test_kirara_v45() 73 | test_charlotte_v45() 74 | test_electro_cicin_mage_v45() 75 | -------------------------------------------------------------------------------- /tests/server/test_draw_card.py: -------------------------------------------------------------------------------- 1 | from lpsim.server.interaction import SwitchCardResponse 2 | from tests.utils_for_test import make_respond 3 | from lpsim.agents.interaction_agent import InteractionAgent 4 | from lpsim.agents.nothing_agent import NothingAgent 5 | from lpsim.server.match import Match 6 | from lpsim.server.deck import Deck 7 | 8 | 9 | def test_draw_card(): 10 | """ 11 | when all card same, should raise no error and hand number right 12 | """ 13 | deck = Deck.from_str( 14 | """ 15 | default_version:4.0 16 | character:Nahida*3 17 | Strategize*30 18 | """ 19 | ) 20 | match = Match() 21 | match.set_deck([deck, deck]) 22 | match.config.max_same_card_number = 30 23 | match.config.random_first_player = False 24 | match.start() 25 | match.step() 26 | agent_0 = NothingAgent(player_idx=0) 27 | agent_1 = InteractionAgent( 28 | player_idx=1, 29 | commands=[ 30 | "sw_card 1 2 3 4", 31 | ], 32 | only_use_command=True, 33 | ) 34 | while True: 35 | if match.need_respond(0): 36 | make_respond(agent_0, match) 37 | elif match.need_respond(1): 38 | make_respond(agent_1, match) 39 | else: 40 | raise AssertionError("No need respond.") 41 | if len(agent_1.commands) == 0: 42 | break 43 | assert len(agent_1.commands) == 0 44 | assert len(match.player_tables[1].hands) == 5 45 | 46 | """ 47 | when two types of cards, should become another type in all hand 48 | """ 49 | deck = Deck.from_str( 50 | """ 51 | default_version:4.0 52 | character:Nahida*3 53 | Strategize*15 54 | Rana*15 55 | """ 56 | ) 57 | for _ in range(100): 58 | match = Match() 59 | match.set_deck([deck, deck]) 60 | match.config.max_same_card_number = 30 61 | match.config.random_first_player = False 62 | match.start() 63 | match.step() 64 | agent_0 = NothingAgent(player_idx=0) 65 | while True: 66 | if match.need_respond(0): 67 | make_respond(agent_0, match) 68 | elif match.need_respond(1): 69 | assert len(match.requests) == 1 70 | assert match.requests[0].name == "SwitchCardRequest" 71 | req = match.requests[0] 72 | idxs = [] 73 | for id, name in enumerate(req.card_names): 74 | if name == "Strategize": 75 | idxs.append(id) 76 | resp: SwitchCardResponse = SwitchCardResponse( 77 | request=req, 78 | card_idxs=idxs, 79 | ) 80 | match.respond(resp) 81 | match.step() 82 | break 83 | else: 84 | raise AssertionError("No need respond.") 85 | assert len(match.player_tables[1].hands) == 5 86 | for card in match.player_tables[1].hands: 87 | assert card.name == "Rana" 88 | 89 | 90 | if __name__ == "__main__": 91 | test_draw_card() 92 | --------------------------------------------------------------------------------