Skip to content

Commit

Permalink
Updated documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Typhi committed Sep 20, 2024
1 parent 549c7bf commit 148ff43
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 28 deletions.
28 changes: 28 additions & 0 deletions capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## Capabilities
See (TODO link to tierlist video) for a general overview of capabilities and their current state.

### Ironclad
- Missing cards: several, but also specifically: Infernal Blade, True Grit

### Silent
- Missing cards: Distraction, All Out Attack, Well Laid Plans, Masterful Stab, Doppelganger, Setup, Nightmare, Alchemize,
- Strategies around poison use would require additional comparator adjustments: e.g. who to best target with Corpse Explosion? When do we want to apply Catalyst?

### Defect
- Missing cards: Force Field, Hologram, Rebound, Seek, Static Discharge, White Noise

### Watcher
- Missing cards: Conjure Blade, Foreign Influence, Meditate, Omniscience, Vault

### General
- **Calculation limit:** The bot will stop thinking after it has evaluated 11,000 battle paths for one play. This will often result in it just playing the card that's all the way on the right. This is an intentional trade-off vs waiting a long time. Most likely to happen when you've got a LOT of cards in hand, the potential to play a lot of them, and 3+ enemies.
- **Heart**: The bot doesn't know how to find the keys
- **Bug**: Prayer Wheel card reward is only checked if first card reward is picked up
- **Missing colorless cards**: Chrysalis, Discovery, Forethought, Jack of All Trades, Madness, Metamorphosis, Panic Button, Purity, Secret Technique, Secret Weapon, The Bomb, Thinking Ahead, Transmutation, Violence

### Misc improvement ideas
- Know that Writhing Mass will change intent after each hit. Could do: stop dealing damage when we can block the hit. Also avoid the curse.
- Know when Time Eater is going to heal, so don't waste resources on him.
- Add purchasing of potions (e.g. Ritual Potion) to the shop purchase handler.

We maintain a big backlog of issues, let us know if you want inspiration. :D
37 changes: 37 additions & 0 deletions how_to_make_your_own_bot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## How to: Make your own bot
In Bottled AI, a bot Strategy is a collection of configurations that make up its behavior. Some configurations are straightforward, like "what character do we play?" or "what cards do we pick up?".
Others are a little more complicated, like "how do we weigh damaging opponents vs powering ourselves up?".

Changing a Strategy's configurations lets you customize your own bot.

### Create a new bot Strategy
1) Go to `\rs\ai`
2) Copy the entire `_example` folder
3) Rename example.py and EXAMPLE_STRATEGY to match your new Strategy's name
4) Search for `_example` imports and rename them
5) (Change the Strategy that is run in `main.py`)

### Basics
- Come up with an idea of how you want the bot to behave
- Adjust the lists in your Strategy's `config.py` file and your strategy's handlers.

### State of Bottled AI
Important! For the current state of Bottled AI's capabilities, see [capabilities.py](capabilities.py).


## Advanced
### Handlers
Handlers contain sets of behavior to be applied when their `can_handle` conditions are applied.
There are a set of Common handler shared between Strategies, but you can create new handlers on a Strategy level.
Remember to adjust which handlers are used in your `[your_strategy's_name.py]`.

### Battle
Battles are the most complicated part of the bot. In battle, the bot will simulate/calculate the outcome from playing all of its cards in all possible configurations. We call this part the 'calculator'. Secondly, the bot will then play cards based on which plays led to the most desirable outcome.

There are therefore 2 ways to adjust in-battle behavior:
1) **Extending the simulation**: i.e. teaching the bot what happens when a card is played / relic triggered / etc. Place to start: `\rs\calculator\battle_state`.
2) **Changing the desirability of outcomes** to impact its decision-making. Place to start: `\rs\common\comparators\common_general_comparator.py`

### Other
- If you're making deeper behavior adjustments, we recommend adding some tests covering your new functionality See `/tests`
- TODO probably more
1 change: 1 addition & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rs.ai.claw_is_law.claw_is_law import CLAW_IS_LAW
from rs.ai.peaceful_pummeling.peaceful_pummeling import PEACEFUL_PUMMELING
from rs.ai.pwnder_my_orbs.pwnder_my_orbs import PWNDER_MY_ORBS
from rs.ai.requested_strike.requested_strike import REQUESTED_STRIKE
from rs.ai.shivs_and_giggles.shivs_and_giggles import SHIVS_AND_GIGGLES
from rs.helper.seed import make_random_seed
from rs.api.client import Client
Expand Down
54 changes: 33 additions & 21 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
## Setup Guidelines
## Setup

### Getting Python Setup:
### Python Setup
1) Have Python 3.11.8 or so installed
- Windows: you might need to add Python to the system path variable things
- Windows: you may need to add Python to the Path Environmental Variable
- MacOS: you need Python 3.11+ within xcode. This requires xcode 14.0+ (this applied for python 3.9+, might need to be higher now), which in turn requires MacOS Monterey (lower versions won't work!)
2) Have PIP (python package manager) installed: https://pip.pypa.io/en/stable/installation/

### Getting the Project Setup:
1) Checkout this repository, and clone it into the steam app folder in a new folder: `ai\requested_strike`.
- Windows ex: ` E:\Steam\steamapps\common\SlayTheSpire\ai\requested_strike`
- MacOS ex: Browse local files of StS via Steam -> Right click and Show Package Contents -> Resources -> ai -> requested_strike
### Project Setup
1) Clone this repository into the game's install folder, in a new folder: `ai\requested_strike`.
- Windows example: ` E:\Steam\steamapps\common\SlayTheSpire\ai\requested_strike`
- MacOS example: Browse local files of StS via Steam -> Right click and Show Package Contents -> Resources -> ai -> requested_strike
2) Run `python -m pip install -r requirements.txt` in the root folder of the project (installs python dependencies)

### Getting The Game Setup
### Game Setup
1) Through the steam workshop, make sure you have:
- BaseMod https://steamcommunity.com/sharedfiles/filedetails/?id=1605833019
- StSLib https://steamcommunity.com/sharedfiles/filedetails/?id=1609158507
Expand All @@ -26,32 +26,44 @@

### Running the Bot
- Run the bot via the game's main menu
- -> Mods
- -> Communication Mod
- -> Config (next to "Return")
- -> Start external process
- Mods ->
- Communication Mod ->
- Config (next to "Return") ->
- Start external process
- You can configure some run settings in [main.py](main.py).

Now it should all be able to run!

The process has a timeout of 10s so if you simply see that delay but nothing's happening, then something isn't working.
To debug, check the output in the ModTheSpire console, or the `communication_mod_errors.log` in the StS folder.

### Screenshots
- You'll need to adjust the display settings in game to be bordered window (not fullscreen, not borderless window)
- In the config parameters in main.py, turn "take_screenshots" to True
- Only the last run will have its screenshots saved, and they're in logs/screenshots (starting a new run will delete anything in there)
- Doesn't run on Mac, so also various previous steps about python dependencies may not be required
## Making your own bot
- See [how_to_make_your_own_bot.md](how_to_make_your_own_bot.md).


## Tools

### Testing
- All tests can be found in the /tests directory
### Bot Controls
- Adjust which bot strategy is used, the amount of runs, and the seed in [main.py](main.py).
- Pause the bot in [run_controller.txt](run_controller.txt).
- Adjust the speed of certain actions in [presentation_config.py](presentation_config.py).

### Tests
- All tests can be found in the `/tests` directory
- VERY useful for checking bot behavior without needing to run the game
- You can run coverage checks with:
- `python -m coverage run -m unittest discover .\tests`
- `python -m coverage report`
- `python -m coverage html`
- We don't know if we trust them yet, but yeah

### Screenshots
- You'll need to adjust the display settings in game to be bordered window (not fullscreen, not borderless window)
- In the config parameters in main.py, turn "take_screenshots" to True
- Only the last run will have its screenshots saved, and they're in logs/screenshots (starting a new run will delete anything in there)
- Doesn't run on Mac, so also various previous steps about python dependencies may not be required

### Analyzing
You can run `analyze.py` to do analysis on recent runs. Simply supply a list of seeds to the variable at the top.
You can run [analyze.py](analyze.py) to do analysis on recent runs. Simply supply a list of seeds to the variable at the top.

It can be used for just seeing how a session went, but it will naturally get the last two instances of a seed and compare them.
So, it's made for doing a set of runs, changing the logic and rerunning them, then comparing performance results.
6 changes: 0 additions & 6 deletions rs/ai/_example/_README.md

This file was deleted.

9 changes: 8 additions & 1 deletion rs/ai/_example/config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
CARD_REMOVAL_PRIORITY_LIST = ['strike', 'strike+', 'defend', 'defend+']
# card id. 'strike+' is an upgraded strike
CARD_REMOVAL_PRIORITY_LIST = [
'strike',
'strike+',
'defend',
'defend+']

# [card display name]: [max amount to have in deck]
DESIRED_CARDS_FOR_DECK: dict[str, int] = {
'perfected strike': 1337,
'twin strike': 2,
'wild strike': 2,
}

# card display name: max amount to have in deck
DESIRED_CARDS_FROM_POTIONS: dict[str, int] = {
'demon form': 1,
'apotheosis': 1,
Expand Down
11 changes: 11 additions & 0 deletions rs/ai/_example/example.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from typing import List

from rs.ai._example.config import CARD_REMOVAL_PRIORITY_LIST, DESIRED_CARDS_FOR_DECK, HIGH_PRIORITY_UPGRADES, \
DESIRED_CARDS_FROM_POTIONS, DESIRED_POTIONS
from rs.ai._example.handlers.event_handler import EventHandler
from rs.ai._example.handlers.potions_handler import PotionsBossHandler, PotionsEventFightHandler, PotionsEliteHandler
from rs.ai._example.handlers.shop_purchase_handler import ShopPurchaseHandler
from rs.ai._example.handlers.upgrade_handler import UpgradeHandler
from rs.common.handlers.common_astrolabe_handler import CommonAstrolabeHandler
Expand All @@ -19,6 +22,14 @@
from rs.common.handlers.common_transform_handler import CommonTransformHandler
from rs.machine.ai_strategy import AiStrategy
from rs.machine.character import Character
from rs.machine.handlers.handler import Handler

example_battle_potion_handlers: List[Handler] = [
# Potions Handlers First
PotionsBossHandler(),
PotionsEventFightHandler(),
PotionsEliteHandler(),
]

EXAMPLE_STRATEGY: AiStrategy = AiStrategy(
name='EXAMPLE_STRATEGY',
Expand Down
90 changes: 90 additions & 0 deletions rs/ai/_example/handlers/potions_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from typing import List

from rs.game.screen_type import ScreenType
from rs.machine.command import Command
from rs.machine.handlers.handler import Handler
from rs.machine.handlers.handler_action import HandlerAction
from rs.machine.state import GameState

# see also common_combat_reward_handler.py for discarding potions
# these potions might still sneak into our slots with entropric brew
dont_play_potions = [
'Smoke Bomb',
'Elixir Potion',
'Liquid Memories',
'Snecko Oil',
'Stance Potion',
'Ambrosia',
'Distilled Chaos',
]


class PotionsBaseHandler(Handler):

def can_handle(self, state: GameState) -> bool:
# must be implemented by children
pass

def handle(self, state: GameState) -> HandlerAction:
pot = self.get_potions_to_play(state)[0]
wait_command = "wait 30"
if pot['requires_target']:
target = 0
for m_index, monster in enumerate(state.get_monsters()): # Find the back-est monster that isn't dead
if monster['name'] == 'Reptomancer': # Special case since he might not be in the back
target = m_index
break
if not monster['is_gone']:
target = m_index
return HandlerAction(commands=[wait_command, "potion use " + str(pot['idx']) + " " + str(target), wait_command])
return HandlerAction(commands=[wait_command, "potion use " + str(pot['idx']), wait_command])

def get_potions_to_play(self, state: GameState) -> List[dict]:
to_play = []
for idx, pot in enumerate(state.get_potions()):
if pot['can_use'] and pot['name'] not in dont_play_potions:
pot['idx'] = idx
to_play.append(pot)
return to_play


class PotionsEliteHandler(PotionsBaseHandler):
def __int__(self):
super().__init__()

def can_handle(self, state: GameState) -> bool:
hp_per = state.get_player_health_percentage() * 100
return state.has_command(Command.POTION) \
and state.combat_state() \
and state.screen_type() == ScreenType.NONE.value \
and state.game_state()['room_type'] == "MonsterRoomElite" \
and (hp_per <= 50 and state.combat_state()['turn'] == 1) \
and self.get_potions_to_play(state)


class PotionsEventFightHandler(PotionsBaseHandler): # Treat most Event Fights like Elites
def __int__(self):
super().__init__()

def can_handle(self, state: GameState) -> bool:
hp_per = state.get_player_health_percentage() * 100
return state.has_command(Command.POTION) \
and state.combat_state() \
and state.screen_type() == ScreenType.NONE.value \
and state.game_state()['room_type'] == "EventRoom" \
and not state.has_monster("Fungi Beast") \
and (hp_per <= 50 and state.combat_state()['turn'] == 1) \
and self.get_potions_to_play(state)


class PotionsBossHandler(PotionsBaseHandler):
def __int__(self):
super().__init__()

def can_handle(self, state: GameState) -> bool:
return state.has_command(Command.POTION) \
and state.combat_state() \
and state.screen_type() == ScreenType.NONE.value \
and state.game_state()['room_type'] == "MonsterRoomBoss" \
and state.combat_state()['turn'] == 1 \
and self.get_potions_to_play(state)

0 comments on commit 148ff43

Please sign in to comment.