Initial project structure: reusable isometric bot engine with D2R implementation

This commit is contained in:
Hoid 2026-02-14 08:50:36 +00:00
commit e0282a7111
44 changed files with 3433 additions and 0 deletions

0
games/d2r/__init__.py Normal file
View file

83
games/d2r/config.py Normal file
View file

@ -0,0 +1,83 @@
"""Diablo II: Resurrected configuration.
Game-specific settings for screen regions, colors, and timings.
"""
from dataclasses import dataclass, field
from typing import Dict, Tuple
@dataclass
class D2RScreenRegions:
"""Screen regions for UI elements at 1920x1080."""
health_orb: Tuple[int, int, int, int] = (28, 545, 170, 170)
mana_orb: Tuple[int, int, int, int] = (1722, 545, 170, 170)
xp_bar: Tuple[int, int, int, int] = (0, 1058, 1920, 22)
belt: Tuple[int, int, int, int] = (838, 1010, 244, 48)
minimap: Tuple[int, int, int, int] = (1600, 0, 320, 320)
inventory: Tuple[int, int, int, int] = (960, 330, 530, 440)
stash: Tuple[int, int, int, int] = (430, 330, 530, 440)
chat: Tuple[int, int, int, int] = (0, 800, 500, 200)
skill_left: Tuple[int, int, int, int] = (194, 1036, 52, 52)
skill_right: Tuple[int, int, int, int] = (1674, 1036, 52, 52)
@dataclass
class D2RColors:
"""HSV color ranges for game element detection."""
health_filled: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = (
(0, 100, 100), (10, 255, 255) # Red
)
mana_filled: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = (
(100, 100, 100), (130, 255, 255) # Blue
)
item_unique: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = (
(15, 100, 180), (30, 255, 255) # Gold/unique
)
item_set: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = (
(35, 100, 150), (55, 255, 255) # Green/set
)
item_rare: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = (
(15, 50, 200), (25, 150, 255) # Yellow/rare
)
portal_blue: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = (
(90, 150, 150), (120, 255, 255) # Town portal blue
)
@dataclass
class D2RTimings:
"""Game-specific timing constants in seconds."""
loading_screen_max: float = 15.0
town_portal_cast: float = 3.5
teleport_delay: float = 0.15
potion_cooldown: float = 1.0
click_delay: float = 0.1
pickup_delay: float = 0.3
vendor_interaction: float = 0.5
@dataclass
class D2RConfig:
"""Master D2R configuration."""
resolution: Tuple[int, int] = (1920, 1080)
regions: D2RScreenRegions = field(default_factory=D2RScreenRegions)
colors: D2RColors = field(default_factory=D2RColors)
timings: D2RTimings = field(default_factory=D2RTimings)
# Loot filter
pickup_uniques: bool = True
pickup_sets: bool = True
pickup_rares: bool = True
pickup_runes: bool = True
min_rune_tier: int = 10 # Lem+
pickup_gems: bool = False
# Safety
health_potion_threshold: float = 0.5
mana_potion_threshold: float = 0.3
chicken_threshold: float = 0.2 # Exit game if health below this

97
games/d2r/game.py Normal file
View file

@ -0,0 +1,97 @@
"""Main D2R bot class — orchestrates the bot loop.
Entry point for the Diablo II: Resurrected bot. Manages the main
game loop, state transitions, and routine execution.
"""
import logging
import time
from typing import Optional
from engine.screen.capture import ScreenCapture
from engine.input.humanize import Humanizer
from engine.state.events import EventBus
from engine.safety.timing import SessionTimer
from games.d2r.config import D2RConfig
from games.d2r.screens.ingame import InGameDetector
from games.d2r.screens.menu import MenuDetector
logger = logging.getLogger(__name__)
class D2RBot:
"""Main Diablo II: Resurrected bot."""
def __init__(self, config: Optional[D2RConfig] = None):
self.config = config or D2RConfig()
self.screen = ScreenCapture()
self.humanizer = Humanizer()
self.events = EventBus()
self.session_timer = SessionTimer()
self.menu_detector = MenuDetector(self.config)
self.ingame_detector = InGameDetector(self.config)
self._running = False
self._current_routine = None
def start(self, routine_name: str = "mephisto") -> None:
"""Start the bot with a specific farming routine."""
logger.info(f"Starting D2R bot with routine: {routine_name}")
self._running = True
try:
self._main_loop(routine_name)
except KeyboardInterrupt:
logger.info("Bot stopped by user")
except Exception as e:
logger.error(f"Bot error: {e}", exc_info=True)
finally:
self._running = False
def stop(self) -> None:
"""Signal the bot to stop."""
self._running = False
def _main_loop(self, routine_name: str) -> None:
"""Core bot loop."""
while self._running:
# Check session timing
if self.session_timer.should_stop_session():
break_duration = self.session_timer.get_break_duration()
logger.info(f"Session break: {break_duration/60:.0f} min")
time.sleep(break_duration)
self.session_timer.start_new_session()
# Check for breaks
break_time = self.humanizer.should_take_break()
if break_time:
time.sleep(break_time)
# Capture screen
frame = self.screen.capture_screen()
# Detect state and act
# TODO: Implement state detection and routine execution
# Small delay between loop iterations
time.sleep(0.05)
def main():
"""CLI entry point."""
import argparse
parser = argparse.ArgumentParser(description="D2R Bot")
parser.add_argument("--routine", default="mephisto", choices=["mephisto", "pindle", "countess"])
parser.add_argument("--resolution", default="1920x1080")
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s")
config = D2RConfig()
bot = D2RBot(config)
bot.start(args.routine)
if __name__ == "__main__":
main()

View file

View file

@ -0,0 +1,84 @@
"""Countess farming routine for D2R.
Rune farming: Create game Black Marsh WP Tower Cellar
Kill Countess Loot runes Exit Repeat
"""
import logging
from enum import Enum, auto
logger = logging.getLogger(__name__)
class CountessPhase(Enum):
CREATE_GAME = auto()
WAYPOINT_TO_MARSH = auto()
FIND_TOWER = auto()
NAVIGATE_CELLAR = auto()
KILL_COUNTESS = auto()
LOOT = auto()
EXIT_GAME = auto()
class CountessRoutine:
"""Automated Countess farming for rune drops.
Best route for mid-tier rune farming (up to Ist).
Requires navigating 5 tower cellar levels.
"""
def __init__(self, bot):
self.bot = bot
self.phase = CountessPhase.CREATE_GAME
self.run_count = 0
def execute_run(self) -> bool:
"""Execute a single Countess run."""
logger.info(f"Starting Countess run #{self.run_count + 1}")
phases = [
(CountessPhase.CREATE_GAME, self._create_game),
(CountessPhase.WAYPOINT_TO_MARSH, self._go_to_marsh),
(CountessPhase.FIND_TOWER, self._find_tower),
(CountessPhase.NAVIGATE_CELLAR, self._navigate_cellar),
(CountessPhase.KILL_COUNTESS, self._kill_countess),
(CountessPhase.LOOT, self._loot_runes),
(CountessPhase.EXIT_GAME, self._exit_game),
]
for phase, handler in phases:
self.phase = phase
if not handler():
return False
self.bot.humanizer.wait()
self.run_count += 1
return True
def _create_game(self) -> bool:
return True
def _go_to_marsh(self) -> bool:
"""Take waypoint to Black Marsh."""
return True
def _find_tower(self) -> bool:
"""Navigate from Black Marsh to Forgotten Tower entrance."""
# TODO: This is the hardest part — tower location is random
return True
def _navigate_cellar(self) -> bool:
"""Navigate through 5 cellar levels to level 5."""
# TODO: Find stairs on each level, descend
return True
def _kill_countess(self) -> bool:
"""Kill the Countess."""
return True
def _loot_runes(self) -> bool:
"""Pick up rune drops (Countess has special rune drop table)."""
return True
def _exit_game(self) -> bool:
return True

View file

@ -0,0 +1,105 @@
"""Mephisto farming routine for D2R.
Classic Mephisto run: Create game Teleport to Durance 3
Kill Mephisto Loot Exit Repeat
"""
import logging
import time
from enum import Enum, auto
logger = logging.getLogger(__name__)
class MephistoPhase(Enum):
CREATE_GAME = auto()
TELEPORT_TO_DURANCE = auto()
FIND_MEPHISTO = auto()
KILL_MEPHISTO = auto()
LOOT = auto()
TOWN_PORTAL = auto()
STASH_ITEMS = auto()
EXIT_GAME = auto()
class MephistoRoutine:
"""Automated Mephisto farming runs.
Designed for Sorceress with Teleport. Can be adapted for
other classes with Enigma runeword.
"""
def __init__(self, bot):
self.bot = bot
self.phase = MephistoPhase.CREATE_GAME
self.run_count = 0
self.items_found = 0
def execute_run(self) -> bool:
"""Execute a single Mephisto run. Returns True if successful."""
logger.info(f"Starting Mephisto run #{self.run_count + 1}")
phases = [
(MephistoPhase.CREATE_GAME, self._create_game),
(MephistoPhase.TELEPORT_TO_DURANCE, self._teleport_to_durance),
(MephistoPhase.FIND_MEPHISTO, self._find_mephisto),
(MephistoPhase.KILL_MEPHISTO, self._kill_mephisto),
(MephistoPhase.LOOT, self._loot_items),
(MephistoPhase.TOWN_PORTAL, self._town_portal),
(MephistoPhase.STASH_ITEMS, self._stash_items),
(MephistoPhase.EXIT_GAME, self._exit_game),
]
for phase, handler in phases:
self.phase = phase
logger.debug(f"Phase: {phase.name}")
if not handler():
logger.warning(f"Phase {phase.name} failed")
return False
self.bot.humanizer.wait()
self.run_count += 1
logger.info(f"Run #{self.run_count} complete. Total items: {self.items_found}")
return True
def _create_game(self) -> bool:
"""Create a new game."""
# TODO: Navigate menu → create game with random name
return True
def _teleport_to_durance(self) -> bool:
"""Teleport from Act 3 town to Durance of Hate Level 3."""
# TODO: Navigate waypoint → Durance 2 → teleport to Durance 3
return True
def _find_mephisto(self) -> bool:
"""Locate Mephisto on Durance 3."""
# TODO: Teleport around to find Mephisto (moat trick position)
return True
def _kill_mephisto(self) -> bool:
"""Kill Mephisto using appropriate skill rotation."""
# TODO: Position at moat trick spot, cast spells
return True
def _loot_items(self) -> bool:
"""Pick up valuable items."""
# TODO: Detect and pick up items based on loot filter
return True
def _town_portal(self) -> bool:
"""Cast town portal and go to town."""
# TODO: Cast TP, click portal
return True
def _stash_items(self) -> bool:
"""Stash items if inventory is getting full."""
# TODO: Open stash, transfer items
return True
def _exit_game(self) -> bool:
"""Exit the current game."""
# TODO: Save & Exit
return True

View file

@ -0,0 +1,80 @@
"""Pindleskin farming routine for D2R.
Fastest MF run: Create game Take red portal in Harrogath
Kill Pindleskin Loot Exit Repeat
"""
import logging
from enum import Enum, auto
logger = logging.getLogger(__name__)
class PindlePhase(Enum):
CREATE_GAME = auto()
TAKE_PORTAL = auto()
FIND_PINDLE = auto()
KILL_PINDLE = auto()
LOOT = auto()
EXIT_GAME = auto()
class PindleRoutine:
"""Automated Pindleskin farming runs.
The simplest and fastest MF route. Requires Act 5 red portal
(Anya quest completed). Works with any class.
"""
def __init__(self, bot):
self.bot = bot
self.phase = PindlePhase.CREATE_GAME
self.run_count = 0
def execute_run(self) -> bool:
"""Execute a single Pindleskin run."""
logger.info(f"Starting Pindle run #{self.run_count + 1}")
phases = [
(PindlePhase.CREATE_GAME, self._create_game),
(PindlePhase.TAKE_PORTAL, self._take_red_portal),
(PindlePhase.FIND_PINDLE, self._find_pindle),
(PindlePhase.KILL_PINDLE, self._kill_pindle),
(PindlePhase.LOOT, self._loot_items),
(PindlePhase.EXIT_GAME, self._exit_game),
]
for phase, handler in phases:
self.phase = phase
if not handler():
return False
self.bot.humanizer.wait()
self.run_count += 1
return True
def _create_game(self) -> bool:
return True
def _take_red_portal(self) -> bool:
"""Navigate to and enter the red portal near Anya."""
# TODO: Find red portal in Harrogath, click it
return True
def _find_pindle(self) -> bool:
"""Locate Pindleskin in Nihlathak's Temple entrance."""
# TODO: Move toward Pindle's fixed spawn location
return True
def _kill_pindle(self) -> bool:
"""Kill Pindleskin and his minions."""
# TODO: Attack routine
return True
def _loot_items(self) -> bool:
"""Pick up valuable drops."""
return True
def _exit_game(self) -> bool:
"""Exit game."""
return True

View file

View file

@ -0,0 +1,89 @@
"""In-game state detection for D2R.
Detects health/mana, location, enemies, items on ground, etc.
"""
from typing import Optional, List, Tuple
import logging
import numpy as np
from engine.vision.color import ColorAnalyzer
from engine.vision.detector import ElementDetector, Detection
from games.d2r.config import D2RConfig
logger = logging.getLogger(__name__)
class InGameDetector:
"""Detects in-game state from screen captures."""
def __init__(self, config: D2RConfig):
self.config = config
self.color = ColorAnalyzer()
self.detector = ElementDetector()
def is_in_game(self, screen: np.ndarray) -> bool:
"""Check if we're in an active game (health/mana orbs visible)."""
health = self.get_health_percentage(screen)
return health > 0
def get_health_percentage(self, screen: np.ndarray) -> float:
"""Read current health from the health orb."""
return self.color.read_bar_percentage(
screen,
self.config.regions.health_orb,
self.config.colors.health_filled,
)
def get_mana_percentage(self, screen: np.ndarray) -> float:
"""Read current mana from the mana orb."""
return self.color.read_bar_percentage(
screen,
self.config.regions.mana_orb,
self.config.colors.mana_filled,
)
def is_dead(self, screen: np.ndarray) -> bool:
"""Check if character is dead."""
# Health at 0 + death screen elements
return self.get_health_percentage(screen) == 0
def should_use_health_potion(self, screen: np.ndarray) -> bool:
"""Check if health is below potion threshold."""
return self.get_health_percentage(screen) < self.config.health_potion_threshold
def should_chicken(self, screen: np.ndarray) -> bool:
"""Check if health is critically low (exit game)."""
return self.get_health_percentage(screen) < self.config.chicken_threshold
def find_items_on_ground(self, screen: np.ndarray) -> List[Detection]:
"""Detect item labels on the ground."""
items = []
if self.config.pickup_uniques:
items.extend(self.detector.find_by_color(
screen, *self.config.colors.item_unique,
min_area=50, label="unique",
))
if self.config.pickup_sets:
items.extend(self.detector.find_by_color(
screen, *self.config.colors.item_set,
min_area=50, label="set",
))
return items
def find_portal(self, screen: np.ndarray) -> Optional[Detection]:
"""Detect a town portal on screen."""
portals = self.detector.find_by_color(
screen, *self.config.colors.portal_blue,
min_area=200, label="portal",
)
return portals[0] if portals else None
def is_inventory_open(self, screen: np.ndarray) -> bool:
"""Check if the inventory panel is open."""
# TODO: Template match inventory panel
return False

View file

@ -0,0 +1,40 @@
"""Inventory management for D2R.
Handles inventory scanning, item identification, stash management.
"""
from typing import List, Optional, Tuple
import logging
import numpy as np
from engine.vision.detector import ElementDetector, Detection
from games.d2r.config import D2RConfig
logger = logging.getLogger(__name__)
class InventoryManager:
"""Manages inventory state and item operations."""
def __init__(self, config: D2RConfig):
self.config = config
self.detector = ElementDetector()
def is_full(self, screen: np.ndarray) -> bool:
"""Check if inventory is full."""
# TODO: Scan inventory grid for empty slots
return False
def find_empty_slot(self, screen: np.ndarray) -> Optional[Tuple[int, int]]:
"""Find an empty inventory slot."""
# TODO: Grid scanning
return None
def count_items(self, screen: np.ndarray) -> int:
"""Count items in inventory."""
return 0
def should_go_to_town(self, screen: np.ndarray) -> bool:
"""Check if inventory is full enough to warrant a town trip."""
return self.is_full(screen)

45
games/d2r/screens/menu.py Normal file
View file

@ -0,0 +1,45 @@
"""Main menu and character select screen detection for D2R."""
from typing import Optional
import logging
import numpy as np
from engine.vision.detector import ElementDetector, Detection
from games.d2r.config import D2RConfig
logger = logging.getLogger(__name__)
class MenuDetector:
"""Detects D2R menu screens (main menu, character select, lobby)."""
def __init__(self, config: D2RConfig):
self.config = config
self.detector = ElementDetector()
def is_main_menu(self, screen: np.ndarray) -> bool:
"""Check if we're on the main menu."""
# TODO: Template match for main menu elements
return False
def is_character_select(self, screen: np.ndarray) -> bool:
"""Check if we're on character select screen."""
return False
def is_lobby(self, screen: np.ndarray) -> bool:
"""Check if we're in the game lobby."""
return False
def is_loading(self, screen: np.ndarray) -> bool:
"""Check if a loading screen is active."""
return False
def select_character(self, screen: np.ndarray, char_index: int = 0) -> Optional[Detection]:
"""Find and return the character slot to click."""
# TODO: Detect character slots
return None
def find_create_game_button(self, screen: np.ndarray) -> Optional[Detection]:
"""Find the create game button."""
return None

View file