diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 57522e8..fff7cc9 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -4,7 +4,7 @@ on: pull_request
jobs:
test:
- runs-on: ubuntu-latest
+ runs-on: windows-latest
steps:
- name: Checkout code
@@ -13,21 +13,13 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
- python-version: '3.12' # Use the appropriate version of Python
+ python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- - name: Install Xvfb
- run: sudo apt-get install -y xvfb
-
- name: Run tests
run: |
- sudo Xvfb :99 -ac -screen 0 1920x1080x24 &
- export DISPLAY=:99
- touch /tmp/.Xauthority
- export XAUTHORITY=/tmp/.Xauthority
python -m unittest discover -s test -p "_test_*.py"
-
diff --git a/.gitignore b/.gitignore
index 969fbf1..17c2958 100644
--- a/.gitignore
+++ b/.gitignore
@@ -130,3 +130,7 @@ dmypy.json
# JetBrains
.idea
+
+# Project
+/test/*/settings/
+/settings/*.json
diff --git a/README.md b/README.md
index fdb67c5..7ff3e72 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,66 @@
-# AutoHackerPDA
\ No newline at end of file
+# AutoHackerPDA
+
+AutoHackerPDA is a tool designed to automate the process of hacking PDAs in the game Thief Simulator. It simplifies the hacking mini-game, allowing you to focus on the main aspects of the game.
+
+## Features
+
+- Automatically hacks PDAs in Thief Simulator
+- Easy to use interface
+- Configurable settings for different hacking scenarios
+
+## Requirements
+
+- Thief Simulator from Steam
+
+## Installation
+
+1. Download the latest version of AutoHackerPDA from the [Releases](https://github.com/yourusername/AutoHackerPDA/releases) page.
+2. Extract the downloaded zip file to a location of your choice.
+3. Run `AutoHackerPDA.exe` to start the application.
+
+## Usage
+
+1. Launch Thief Simulator and navigate to the PDA hacking mini-game.
+2. Run `AutoHackerPDA.exe`.
+3. Configure the settings as per your requirement.
+4. Click the "Start Hacking" button.
+5. The tool will automatically complete the hacking mini-game for you.
+
+## Configuration
+
+AutoHackerPDA comes with several configuration options to customize the hacking process. You can adjust these settings in the application interface:
+
+- **Speed**: Adjust the speed of the hacking process.
+- **Retries**: Set the number of retries in case of a failed attempt.
+- **Logging**: Enable or disable logging for debugging purposes.
+
+## Contributing
+
+We welcome contributions to improve AutoHackerPDA. If you would like to contribute, please follow these steps:
+
+1. Fork the repository.
+2. Create a new branch for your feature or bugfix.
+3. Make your changes and commit them with a clear message.
+4. Submit a pull request to the main repository.
+
+Please read our [Contributing Guidelines](CONTRIBUTING.md) for more details.
+
+## License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
+
+## Support
+
+If you encounter any issues or have questions about AutoHackerPDA, please open an issue in the [issue tracker](https://github.com/yourusername/AutoHackerPDA/issues) or contact us at support@yourproject.com.
+
+## Disclaimer
+
+AutoHackerPDA is intended for use in Thief Simulator only. Use of this tool in any other context may violate the terms of service of the software or platform.
+
+## About me
+
+If you want to know more about me or my other projects, visit my [GitHub profile](https://github.com/LukasKrah)
+
+---
+
+Thank you for using AutoHackerPDA! We hope you enjoy the enhanced gameplay experience.
diff --git a/algorithm/__init__.py b/algorithm/__init__.py
index cd872f2..fb35bae 100644
--- a/algorithm/__init__.py
+++ b/algorithm/__init__.py
@@ -21,4 +21,4 @@
Created: 19/04/2023
"""
-from ._algorithm_service import AlgorithmService
+from ._algorithm import Algorithm
diff --git a/screen_service/_output_service.py b/algorithm/_algorithm.py
similarity index 54%
rename from screen_service/_output_service.py
rename to algorithm/_algorithm.py
index aa3f485..4354057 100644
--- a/screen_service/_output_service.py
+++ b/algorithm/_algorithm.py
@@ -17,45 +17,45 @@
Contact me: l.krahbichler@proton.me
-File: screen_service/_output_service.py
-Created: 18/04/2023
+File: algorithm/_algorithm.py
+Created: 04/08/2024
"""
##################################################
# Imports #
##################################################
-from time import sleep
-import pyautogui
-import autoit
+from cv2.typing import MatLike
+
+from ._simulation import Simulation
##################################################
# Code #
##################################################
-class OutputService:
- """
- Do something one the screen
- """
-
- @classmethod
- def pad_clicks(cls, click: dict[tuple[int, int], int]) -> None:
- width: int
- height: int
- width, height = pyautogui.size()
-
- x_left: int = (width // 3) + 40
- x_right: int = int(width / 1.536) - 30
- y_top: int = int(height / 3.85714)
- y_bot: int = int(height / 1.30120)
-
- field_width = (x_right - x_left) // 8
- field_height = (y_bot - y_top) // 8
-
- for row, col in click:
- x: int = x_left + (field_width * col) + (field_width // 2)
- y: int = y_top + (field_height * row) + (field_height // 2)
- for i in range(click[(row, col)]):
- autoit.mouse_click("left", x, y, speed=0)
- sleep(0.02)
+class _Algorithm:
+ __sim: Simulation
+
+ def __init__(self) -> None:
+ self.__sim = Simulation()
+
+ def path_finder(
+ self,
+ _pads: list[list[MatLike]],
+ pad_directions: list[list[str]],
+ io_ports: tuple[int, int]
+ ) -> dict[tuple[int, int], int]:
+ self.__sim.update_pads(pad_directions)
+ self.__sim.update_io_ports(io_ports)
+
+ his = self.__sim.simulate()
+
+ diffs = self.__sim.found_diffs(his)
+ # row | column
+
+ # Window.grid_pads(_pads, self.__sim.get_directions(), io_ports)
+ return diffs
+
+
+Algorithm = _Algorithm()
diff --git a/algorithm/_circuit.py b/algorithm/_circuit.py
new file mode 100644
index 0000000..6fb8db4
--- /dev/null
+++ b/algorithm/_circuit.py
@@ -0,0 +1,141 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: algorithm/_circuit.py
+Created: 04/08/2024
+"""
+
+##################################################
+# Imports #
+##################################################
+
+from __future__ import annotations
+
+from typing import Literal
+
+from ._types import CIRCUIT, CIRCUIT_TYPE, DIRECTIONS, DIRECTIONS_TYPE
+
+
+##################################################
+# Code #
+##################################################
+
+# Todo: Performance measurements and improvements
+# Todo: Tests
+class Circuit:
+ _type: CIRCUIT_TYPE | None
+ _direction: DIRECTIONS_TYPE | None
+ _is_powered: bool
+
+ _top_port_power: bool
+ _bot_port_power: bool
+ _right_port_power: bool
+ _left_port_power: bool
+
+ def __init__(self):
+ self._type = None
+ self._direction = None
+ self._is_powered = False
+
+ self._top_port_power = False
+ self._bot_port_power = False
+ self._right_port_power = False
+ self._left_port_power = False
+
+ def __update(self) -> None:
+ # Facing: Top -> top, left, bot, right
+ emitting: tuple[bool, bool, bool, bool] = (False,) * 4
+
+ if self._type == "oneway":
+ emitting = (True, False, True, False)
+ elif self._type == "corner":
+ emitting = (True, False, False, True)
+ elif self._type == "junction":
+ emitting = (True, True, False, True)
+
+ if self._direction:
+ self._top_port_power = emitting[(0 - DIRECTIONS.index(self._direction)) % 4]
+ self._left_port_power = emitting[(1 - DIRECTIONS.index(self._direction)) % 4]
+ self._bot_port_power = emitting[(2 - DIRECTIONS.index(self._direction)) % 4]
+ self._right_port_power = emitting[(3 - DIRECTIONS.index(self._direction)) % 4]
+
+ def __repr__(self) -> str:
+ return f"Model: {self._type} - Facing: {self._direction} - Ports(T, L, B, R): " \
+ f"{self._top_port_power}, {self._left_port_power}, {self._bot_port_power}, {self._right_port_power}"
+
+ def set_attributes(self, pad_string: str) -> None:
+ cut_string: list[Literal["oneway", "corner", "junction", "", "top", "bot", "left", "right"]]
+ cut_string = pad_string.split("_") # noqa
+
+ if len(cut_string) >= 2:
+ self._type = cut_string[0] if cut_string[0] in CIRCUIT else ""
+ self._direction = cut_string[1] if cut_string[1] in DIRECTIONS else ""
+ else:
+ self._type = None
+ self._direction = None
+ self.__update()
+
+ def rotate_by(
+ self,
+ by: int | None = 1
+ ) -> None:
+ if self._direction:
+ self._direction = DIRECTIONS[(DIRECTIONS.index(self._direction) + by) % 4]
+ self.__update()
+
+ @property
+ def state(self) -> bool:
+ return self._is_powered
+
+ @state.setter
+ def state(self, value: bool) -> None:
+ self._is_powered = value
+
+ def empower(self, direction: DIRECTIONS_TYPE | None) -> None:
+ self._is_powered = self.__dict__[f"_Pad__port_{direction}"]
+ self.__update()
+
+ @property
+ def port_top(self) -> bool:
+ return self._top_port_power
+
+ @property
+ def port_left(self) -> bool:
+ return self._left_port_power
+
+ @property
+ def port_bot(self) -> bool:
+ return self._bot_port_power
+
+ @property
+ def port_right(self) -> bool:
+ return self._right_port_power
+
+ @property
+ def facing(self) -> DIRECTIONS_TYPE | None:
+ return self._direction
+
+ @facing.setter
+ def facing(self, value: DIRECTIONS_TYPE | None) -> None:
+ self._direction = value
+ self.__update()
+
+ @property
+ def model(self) -> CIRCUIT_TYPE | None:
+ return self._type
diff --git a/algorithm/_algorithm_service.py b/algorithm/_simulation.py
similarity index 56%
rename from algorithm/_algorithm_service.py
rename to algorithm/_simulation.py
index a390812..a6a4e13 100644
--- a/algorithm/_algorithm_service.py
+++ b/algorithm/_simulation.py
@@ -17,7 +17,7 @@
Contact me: l.krahbichler@proton.me
-File: algorithm/_algorithm_service.py
+File: algorithm/_simulation.py
Created: 19/04/2023
"""
@@ -25,194 +25,90 @@
# Imports #
##################################################
+from __future__ import annotations
+
import numpy as np
+
from typing import Literal
from copy import deepcopy
+from ._types import DIRECTIONS_TYPE
+from ._circuit import Circuit
+
##################################################
# Code #
##################################################
-PADS: Literal["oneway_up", "oneway_right",
- "corner_top_right", "corner_right_bot", "corner_bot_left", "corner_left_top",
- "junction_top", "junction_bot", "junction_left", "junction_right"]
-
-
-class _Pad:
- __model: Literal["oneway", "corner", "junction", ""]
- __facing: Literal["top", "right", "bot", "left", ""]
- __state: bool
-
- __DIRECTIONS: list[Literal["top", "left", "bot", "right"]] = ["top", "left", "bot", "right"]
-
- __port_top: bool
- __port_bot: bool
- __port_right: bool
- __port_left: bool
-
- def __init__(self):
- self.__model = ""
- self.__facing = ""
- self.__state = False
-
- self.__port_top = False
- self.__port_bot = False
- self.__port_right = False
- self.__port_left = False
-
- def __update(self) -> None:
- # Facing: Top -> top, left, bot, right
- emitting: tuple[bool, bool, bool, bool] = (False,) * 4
-
- if self.__model == "oneway":
- emitting = (True, False, True, False)
- elif self.__model == "corner":
- emitting = (True, False, False, True)
- elif self.__model == "junction":
- emitting = (True, True, False, True)
-
- if self.__facing != "":
- self.__port_top = emitting[(0-self.__DIRECTIONS.index(self.__facing)) % 4]
- self.__port_left = emitting[(1-self.__DIRECTIONS.index(self.__facing)) % 4]
- self.__port_bot = emitting[(2-self.__DIRECTIONS.index(self.__facing)) % 4]
- self.__port_right = emitting[(3-self.__DIRECTIONS.index(self.__facing)) % 4]
-
- def __repr__(self) -> str:
- return f"Model: {self.__model} - Facing: {self.__facing} - Ports(T, L, B, R): " \
- f"{self.__port_top}, {self.__port_left}, {self.__port_bot}, {self.__port_right}"
-
- def set_attributes(self, pad_string: str) -> None:
- cut_string: list[Literal["oneway", "corner", "junction", "", "top", "bot", "left", "right"]]
- cut_string = pad_string.split("_") # noqa
-
- if len(cut_string) >= 2:
- self.__model = cut_string[0]
- self.__facing = cut_string[1]
- else:
- self.__model = ""
- self.__facing = ""
- self.__update()
-
- def rotate_by(
- self,
- by: int | None = 1
- ) -> None:
- if self.__facing != "":
- self.__facing = self.__DIRECTIONS[(self.__DIRECTIONS.index(self.__facing) + by) % 4]
- self.__update()
-
- @property
- def state(self) -> bool:
- return self.__state
-
- @state.setter
- def state(self, value: bool) -> None:
- self.__state = value
-
- def empower(self, direction: Literal["top", "left", "bot", "right"]) -> None:
- self.__state = self.__dict__[f"_Pad__port_{direction}"]
- self.__update()
-
- @property
- def port_top(self) -> bool:
- return self.__port_top
-
- @property
- def port_left(self) -> bool:
- return self.__port_left
-
- @property
- def port_bot(self) -> bool:
- return self.__port_bot
-
- @property
- def port_right(self) -> bool:
- return self.__port_right
-
- @property
- def facing(self) -> Literal["top", "left", "bot", "right"]:
- return self.__facing
-
- @facing.setter
- def facing(self, value: Literal["top", "left", "bot", "right"]) -> None:
- self.__facing = value
- self.__update()
-
- @property
- def model(self) -> Literal["oneway", "corner", "junction", ""]:
- return self.__model
-
-
-class _Simulation:
+# Todo: Performance measurements and improvements
+# Todo: Tests
+class Simulation:
# rows[columns[]]
- __pads: list[list[_Pad]]
+ __pads: list[list[Circuit]]
__io_ports: tuple[int, int]
- __img_directions: list[list[Literal["top", "left", "bot", "right"]]]
- __initial_directions: list[list[Literal["top", "left", "bot", "right"]]]
-
+ __img_directions: list[list[DIRECTIONS_TYPE]]
+ __initial_directions: list[list[DIRECTIONS_TYPE]]
+
__found: bool
__found_history: list[tuple[int, int]]
- __found_directions: list[list[Literal["top", "left", "bot", "right"]]]
-
+ __found_directions: list[list[DIRECTIONS_TYPE]]
+
__histories: list[list[tuple[int, int]]]
-
- __DIRECTIONS: list[Literal["top", "left", "bot", "right"]] = ["top", "left", "bot", "right"]
-
+
def __init__(self):
self.__pads = []
self.__io_ports = (0, 0)
self.__init_directions = []
self.__img_directions = []
-
+
self.__histories = []
-
+
self.__found = False
self.__found_history = []
self.__found_directions = []
-
+
for x in range(8):
- row_list: list[_Pad] = []
-
+ row_list: list[Circuit] = []
+
for y in range(8):
- row_list.append(_Pad())
-
+ row_list.append(Circuit())
+
self.__pads.append(row_list)
-
+
def update_pads(self, pad_strings: list[list[str]]) -> None:
for x in range(8):
for y in range(8):
self.__pads[x][y].set_attributes(pad_strings[x][y])
-
+
def update_io_ports(self, io_ports: tuple[int, int]) -> None:
self.__io_ports = io_ports
-
+
def set_initial_directions(self) -> None:
self.__init_directions = self.get_directions(False)
-
+
def simulate(self) -> list[tuple[int, int]]:
print("START SIM")
self.__found = False
self.__found_history = []
self.__found_directions = []
self.__histories = []
-
+
row: int = self.__io_ports[0]
col: int = 0
-
- pad: _Pad = self.__pads[row][col]
-
+
+ pad: Circuit = self.__pads[row][col]
+
self.set_initial_directions()
-
+
pad.empower("left")
while not pad.state:
pad.rotate_by()
pad.empower("left")
-
+
self.algorithm_basic(row, col, self.get_directions(False), "left", [])
print("END SIM")
return self.__found_history
-
+
def algorithm_basic(
self,
row: int,
@@ -224,33 +120,33 @@ def algorithm_basic(
# Deecopy lists to not influnce outer lists
pad_turns = deepcopy(pad_turns)
history = deepcopy(history2)
-
+
# Set pad instances to wanted directions
for r, pad_row in enumerate(self.__pads):
for c, pad in enumerate(pad_row):
pad.facing = pad_turns[r][c]
-
+
# Add self to history
history.append((row, col))
-
- pad: _Pad = self.__pads[row][col]
-
+
+ pad: Circuit = self.__pads[row][col]
+
if history in self.__histories:
return
self.__histories.append(history)
-
+
for direction in range(4):
if self.__found:
return
-
+
# Rest state, rotate and evaluate state
pad.state = False
if direction != 0:
pad.rotate_by(1)
pad.empower(input_direction)
-
+
pad_turns[row][col] = pad.facing
-
+
# Check if pda is solved
if row == self.__io_ports[1] and col == len(self.__pads[0]) - 1:
if pad.port_right and pad.state:
@@ -258,10 +154,10 @@ def algorithm_basic(
self.__found_history = history
self.__found_directions = pad_turns
return
-
+
# Determine influenced neighbors
influenced_neighbors: list[tuple[int, int, Literal["top", "left", "bot", "right"]]] = []
-
+
if pad.port_top and pad.state and row != 0 and input_direction != "top":
influenced_neighbors.append((row - 1, col, "bot"))
if pad.port_left and pad.state and col != 0 and input_direction != "left":
@@ -270,22 +166,22 @@ def algorithm_basic(
influenced_neighbors.append((row + 1, col, "top"))
if pad.port_right and pad.state and col != len(self.__pads[0]) - 1 and input_direction != "right":
influenced_neighbors.append((row, col + 1, "left"))
-
+
# Resume with every influenced neighbor
for n_row, n_col, n_input in influenced_neighbors:
# Cancel if neighbor is already in path
if (n_row, n_col) in history:
return
self.algorithm_basic(n_row, n_col, pad_turns, n_input, history)
-
+
def found_diffs(self, history: list[tuple[int, int]]) -> dict[tuple[int, int], int]:
result = {}
-
+
# Set pad instances to initial directions
for r, pad_row in enumerate(self.__pads):
for c, pad in enumerate(pad_row):
pad.facing = self.__init_directions[r][c]
-
+
for row, pad_row in enumerate(self.__found_directions):
for col, pad in enumerate(pad_row):
if (row, col) in history:
@@ -294,42 +190,15 @@ def found_diffs(self, history: list[tuple[int, int]]) -> dict[tuple[int, int], i
if diff != 0:
result[(row, col)] = diff
self.__pads[row][col].rotate_by(diff)
-
+
return result
-
- def get_directions(self, with_model: bool | None = True) -> list[list[Literal["top", "left", "bot", "right"] | str]]:
- directions = []
+
+ def get_directions(self, with_model: bool | None = True) -> list[list[DIRECTIONS_TYPE]]:
+ directions: list[list[DIRECTIONS_TYPE]] = []
for row in range(8):
- row_directions = []
+ row_directions: list[DIRECTIONS_TYPE] = []
for col in range(8):
- row_directions.append(f"{self.__pads[row][col].model+'_' if with_model else ''}"
+ row_directions.append(f"{self.__pads[row][col].model + '_' if with_model else ''}"
f"{self.__pads[row][col].facing}")
directions.append(row_directions)
return directions
-
-
-class _AlgorithmService:
- __sim: _Simulation
-
- def __init__(self) -> None:
- self.__sim = _Simulation()
-
- def path_finder(
- self,
- _pads: list[list[np.ndarray]],
- pad_directions: list[list[str]],
- io_ports: tuple[int, int]
- ) -> dict[tuple[int, int], int]:
- self.__sim.update_pads(pad_directions)
- self.__sim.update_io_ports(io_ports)
-
- his = self.__sim.simulate()
-
- diffs = self.__sim.found_diffs(his)
- # row | column
-
- # Window.grid_pads(_pads, self.__sim.get_directions(), io_ports)
- return diffs
-
-
-AlgorithmService = _AlgorithmService()
diff --git a/algorithm/_types.py b/algorithm/_types.py
new file mode 100644
index 0000000..e09559c
--- /dev/null
+++ b/algorithm/_types.py
@@ -0,0 +1,38 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: algorithm/_types.py
+Created: 04/08/2024
+"""
+
+##################################################
+# Imports #
+##################################################
+
+from typing import Literal
+
+
+##################################################
+# Code #
+##################################################
+
+CIRCUIT_TYPE = Literal["oneway", "corner", "junction"]
+CIRCUIT: list[CIRCUIT_TYPE] = ["oneway", "corner", "junction"]
+DIRECTIONS_TYPE = Literal["top", "left", "bot", "right"]
+DIRECTIONS: list[DIRECTIONS_TYPE] = ["top", "left", "bot", "right"]
diff --git a/cv_analyze/_compare.py b/cv_analyze/_compare.py
deleted file mode 100644
index c83b66c..0000000
--- a/cv_analyze/_compare.py
+++ /dev/null
@@ -1,107 +0,0 @@
-"""
-AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
-Copyright (C) 2024 Lukas Krahbichler
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-
-Contact me: l.krahbichler@proton.me
-
-File: cv_analyze/_compare.py
-Created: 18/04/2023
-"""
-
-##################################################
-# Imports #
-##################################################
-
-from typing import Literal
-import numpy as np
-import cv2
-
-
-##################################################
-# Code #
-##################################################
-
-PADS: Literal["oneway_top", "oneway_right",
- "corner_top_right", "corner_right_bot", "corner_bot_left", "corner_left_top",
- "junction_top", "junction_bot", "junction_left", "junction_right"]
-
-
-class _CompareService:
- THRESHOLD: float = 0.80
-
- def compare_io(self, pads: list[list[np.ndarray]]) -> tuple[int, int]:
- imgs = ["arrows_in.png", "arrows_out.png"]
-
- compared = self.compare_pads(pads, imgs=imgs, threshold=0.5)
- res = []
-
- for in_out in compared:
- for i, comp in enumerate(in_out):
- if comp != "":
- res.append(i)
- break
- else:
- res.append(0)
-
- return res[0], res[1]
-
- def compare_pads(
- self,
- pads: list[list[np.ndarray]],
- imgs: list[str] | None = None,
- threshold: float | None = None
- ) -> list[list[str]]:
- results: list[list[str]] = []
-
- if not threshold:
- threshold = self.THRESHOLD
-
- if not imgs:
- imgs = ["oneway_top.png", "oneway_right.png",
- "corner_top_right.png", "corner_right_bot.png",
- "corner_bot_left.png", "corner_left_top.png",
- "junction_top.png", "junction_bot.png",
- "junction_left.png", "junction_right.png"]
-
- imgs_to_compare: list[tuple[np.ndarray, PADS]] = []
-
- for img in imgs:
- imgs_to_compare.append((
- cv2.cvtColor(cv2.imread("imgs/"+img), cv2.COLOR_BGR2GRAY),
- "".join(img.split(".")[:-1])
- ))
-
- for pad_col in pads:
- col_list: list[str] = []
-
- for pad_row in pad_col:
- for img_to_compare in imgs_to_compare:
- res = cv2.matchTemplate(pad_row, img_to_compare[0], cv2.TM_CCOEFF_NORMED)
-
- loc = np.where(res >= threshold)
- loc_list = [*zip(*loc[::-1])]
- if loc_list:
- col_list.append(img_to_compare[1])
- break
- else:
- col_list.append("")
-
- results.append(col_list)
-
- return results
-
-
-CompareService = _CompareService()
diff --git a/cv_analyze/__init__.py b/image_processing/__init__.py
similarity index 90%
rename from cv_analyze/__init__.py
rename to image_processing/__init__.py
index 730ce2d..e393422 100644
--- a/cv_analyze/__init__.py
+++ b/image_processing/__init__.py
@@ -17,8 +17,8 @@
Contact me: l.krahbichler@proton.me
-File: cv_analyze/__init__.py
+File: image_processing/__init__.py
Created: 18/04/2023
"""
-from ._compare import CompareService
+from ._image_processing import ImageProcessing
diff --git a/image_processing/_image_processing.py b/image_processing/_image_processing.py
new file mode 100644
index 0000000..b3d26a3
--- /dev/null
+++ b/image_processing/_image_processing.py
@@ -0,0 +1,112 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: image_processing/_image_processing.py
+Created: 18/04/2023
+"""
+
+##################################################
+# Imports #
+##################################################
+
+from __future__ import annotations
+
+import cv2
+
+from imgs.arrows import ARROWS_TYPE, ARROW_FILES, ARROW_FILES_TYPE, ARROW_IO_FILES, ARROW_IO_FILES_TYPE, ARROW_IO_TYPE
+from numpy import any as numpy_any
+from cv2.typing import MatLike
+from typing import List, Tuple
+
+
+##################################################
+# Code #
+##################################################
+
+# Todo: Measure performance and improve @apply_to_methods(timer)
+# Todo: Tests
+class ImageProcessing:
+ """
+ The ImageProcessing class provides methods for comparing PDA screen segments with reference images
+ to identify circuit types and input/output arrows.
+
+ Attributes:
+ __DEFAULT_THRESHOLD (float): Default matching threshold for template comparison.
+ """
+ __DEFAULT_THRESHOLD: float = 0.80
+
+ @staticmethod
+ def io_arrows(pads: List[List[MatLike]]) -> tuple[int, ...]:
+ """
+ Compare input and output arrows on the PDA screen.
+
+ :param pads: 2D list of cropped images representing the circuits.
+ :type pads: List[List[cv2.typing.MatLike]]
+ :return: Tuple indicating the index of the matched input and output arrow.
+ :rtype: Tuple[int, int]
+ """
+ comparisons = ImageProcessing.circuits(pads, ARROW_IO_FILES, threshold=0.5)
+
+ return tuple(
+ next((i for i, comp in enumerate(comp_list) if comp), 0)
+ for comp_list in comparisons
+ )
+
+ @staticmethod
+ def circuits(
+ pads: List[List[MatLike]],
+ images: List[ARROW_IO_FILES_TYPE | ARROW_FILES_TYPE] | None = None,
+ threshold: float | None = None
+ ) -> List[List[str]]:
+ """
+ Compare the cropped circuits with reference images to identify the type of circuit.
+
+ :param pads: 2D list of cropped images representing the circuits.
+ :type pads: List[List[cv2.typing.MatLike]]
+ :param images: List of image filenames to compare against.
+ :type images: List[str] | None
+ :param threshold: Matching threshold for template comparison.
+ :type threshold: float | None
+ :return: 2D list of identified circuit types.
+ :rtype: List[List[str]]
+ """
+ if threshold is None:
+ threshold = ImageProcessing.__DEFAULT_THRESHOLD
+
+ if images is None:
+ images = ARROW_FILES
+
+ reference_images: list[tuple[MatLike, ARROWS_TYPE | ARROW_IO_TYPE]] = [
+ (cv2.cvtColor(cv2.imread(f"imgs/{img}"), cv2.COLOR_BGR2GRAY), img.split(".", 1)[0])
+ for img in images
+ ]
+
+ results = [
+ [
+ next(
+ (ref_name for ref_img, ref_name in reference_images
+ if numpy_any(cv2.matchTemplate(pad, ref_img, cv2.TM_CCOEFF_NORMED) >= threshold)),
+ ""
+ )
+ for pad in pad_col
+ ]
+ for pad_col in pads
+ ]
+
+ return results
diff --git a/imgs/arrows.py b/imgs/arrows.py
new file mode 100644
index 0000000..5d7a103
--- /dev/null
+++ b/imgs/arrows.py
@@ -0,0 +1,70 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: imgs/arrows.py
+Created: 04/08/2024
+"""
+
+##################################################
+# Imports #
+##################################################
+
+from typing import Literal
+
+
+##################################################
+# Code #
+##################################################
+
+ARROWS_TYPE = Literal[
+ "oneway_top", "oneway_right",
+ "corner_top_right", "corner_right_bot",
+ "corner_bot_left", "corner_left_top",
+ "junction_top", "junction_bot",
+ "junction_left", "junction_right"
+]
+ARROWS: list[ARROWS_TYPE] = [
+ "oneway_top", "oneway_right",
+ "corner_top_right", "corner_right_bot",
+ "corner_bot_left", "corner_left_top",
+ "junction_top", "junction_bot",
+ "junction_left", "junction_right"
+]
+
+ARROW_IO_TYPE = Literal["arrow_in", "arrow_out"]
+ARROW_IO: list[ARROW_IO_TYPE] = ["arrow_in", "arrow_out"]
+
+
+ARROW_FILES_TYPE = Literal[
+ "oneway_top.png", "oneway_right.png",
+ "corner_top_right.png", "corner_right_bot.png",
+ "corner_bot_left.png", "corner_left_top.png",
+ "junction_top.png", "junction_bot.png",
+ "junction_left.png", "junction_right.png"
+]
+ARROW_FILES: list[ARROW_FILES_TYPE] = [
+ "oneway_top.png", "oneway_right.png",
+ "corner_top_right.png", "corner_right_bot.png",
+ "corner_bot_left.png", "corner_left_top.png",
+ "junction_top.png", "junction_bot.png",
+ "junction_left.png", "junction_right.png"
+]
+
+ARROW_IO_FILES_TYPE = Literal["arrow_in.png", "arrow_out.png"]
+ARROW_IO_FILES: list[ARROW_IO_FILES_TYPE] = ["arrow_in.png", "arrow_out.png"]
diff --git a/main.py b/main.py
index eeaa0a8..3618fed 100644
--- a/main.py
+++ b/main.py
@@ -17,7 +17,7 @@
Contact me: l.krahbichler@proton.me
-File: /main.py
+File: main.py
Created: 18/04/2023
"""
@@ -29,10 +29,10 @@
from os import kill, getpid
from numpy import ndarray
-from screen_service import InputService, OutputService
-from algorithm import AlgorithmService
-from cv_analyze import CompareService
+from image_processing import ImageProcessing
from terminal_interaction import Licensing
+from screen import ScreenInput, Mouse
+from algorithm import Algorithm
##################################################
@@ -44,21 +44,21 @@ def __run_pda_hack() -> None:
Runs the PDA hack.
:return: None
"""
- pads: list[list[ndarray]] = InputService.pda_single()
- io_pads: list[list[ndarray]] = InputService.pda_io()
-
- styles: list[list[str]] = CompareService.compare_pads(pads)
- io: tuple[int, int] = CompareService.compare_io(io_pads)
- clicks: dict[tuple[int, int], int] = AlgorithmService.path_finder(pads, styles, io)
-
- OutputService.pad_clicks(clicks)
+ pads: list[list[ndarray]] = ScreenInput.circuits_crop()
+ io_pads: list[list[ndarray]] = ScreenInput.io_arrows_crop()
+
+ styles: list[list[str]] = ImageProcessing.circuits(pads)
+ io: tuple[int, ...] = ImageProcessing.io_arrows(io_pads)
+ clicks: dict[tuple[int, int], int] = Algorithm.path_finder(pads, styles, io)
+
+ Mouse.click_on_circuits(clicks)
# Start main program
if __name__ == '__main__':
Licensing.show_information()
-
+
add_hotkey("ctrl+x", __run_pda_hack)
add_hotkey("ctrl+c", lambda: kill(getpid(), 9))
-
+
Licensing.wait_for_commands()
diff --git a/requirements.txt b/requirements.txt
index b7267af..ccdba2d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,5 +4,5 @@ keyboard
pygame
future
Pillow
-autoit
+PyAutoIt
numpy
diff --git a/screen_service/__init__.py b/screen/__init__.py
similarity index 84%
rename from screen_service/__init__.py
rename to screen/__init__.py
index e049914..772f154 100644
--- a/screen_service/__init__.py
+++ b/screen/__init__.py
@@ -17,10 +17,9 @@
Contact me: l.krahbichler@proton.me
-File: /screen_service/__init__.py
+File: screen/__init__.py
Created: 18/02/023
"""
-from ._output_service import OutputService
-from ._input_service import InputService
-from ._visualization import Window
+from ._screen_input import ScreenInput
+from ._mouse import Mouse, MouseEvent
diff --git a/screen/_mouse.py b/screen/_mouse.py
new file mode 100644
index 0000000..4c35eea
--- /dev/null
+++ b/screen/_mouse.py
@@ -0,0 +1,90 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: screen/_mouse.py
+Created: 18/04/2023
+"""
+
+##################################################
+# Imports #
+##################################################
+
+from typing import TypedDict
+from time import sleep
+from pyautogui import size
+# noinspection PyPackageRequirements
+from autoit import mouse_click
+
+from settings import Settings
+
+
+##################################################
+# Code #
+##################################################
+
+class MouseEvent(TypedDict):
+ """
+ Represents a mouse event
+
+ Attributes:
+ row (int): The row of the circuit. (0 is the topmost row)
+ col (int): The column of the circuit. (0 is the leftmost column)
+ clicks (int): The number of clicks.
+ """
+ row: int
+ col: int
+ clicks: int
+
+
+class Mouse:
+ """
+ The Mouse class provides a method to click on the circuits.
+ """
+
+ @classmethod
+ def click_on_circuits(cls, circuits: list[MouseEvent]) -> None:
+ """
+ Clicks on the circuits based on their position and number of clicks.
+
+ :param circuits: A list of circuit events.
+ :type circuits: list[MouseEvent]
+ :return: None
+ :rtype: None
+ """
+ width: int
+ height: int
+ width, height = size()
+
+ x_left: int = (width // 3) + 40
+ x_right: int = int(width / 1.536) - 30
+ y_top: int = int(height / 3.85714)
+ y_bot: int = int(height / 1.30120)
+
+ field_width: int = (x_right - x_left) // 8
+ field_height: int = (y_bot - y_top) // 8
+
+ circuit: MouseEvent
+ for circuit in circuits:
+ x: int = x_left + (field_width * circuit["col"]) + (field_width // 2)
+ y: int = y_top + (field_height * circuit["row"]) + (field_height // 2)
+ for _ in range(circuit["clicks"]):
+ mouse_click(x=x, y=y, speed=0)
+ if Settings["mouse"]["click_delay"] == 0.0:
+ continue
+ sleep(Settings["mouse"]["click_delay"])
diff --git a/screen/_screen_input.py b/screen/_screen_input.py
new file mode 100644
index 0000000..3305205
--- /dev/null
+++ b/screen/_screen_input.py
@@ -0,0 +1,163 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: screen/_screen_input.py
+Created: 18/04/2023
+"""
+
+##################################################
+# Imports #
+##################################################
+
+from __future__ import annotations
+
+import cv2
+
+from numpy import array, ndarray
+from pyautogui import screenshot
+from cv2.typing import MatLike
+from time import time
+
+
+##################################################
+# Code #
+##################################################
+
+class ScreenInput:
+ """
+ The ScreenInput class handles taking and processing screenshots for the PDA hacking interface.
+
+ Attributes:
+ _screenshot (cv2.typing.MatLike | None): Cached screenshot in grayscale.
+ _screenshot_timestamp (float | None): Timestamp of the last taken screenshot.
+ """
+ _screenshot: MatLike | None = None
+ _screenshot_timestamp: float | None = None
+
+ @classmethod
+ def _take_screenshot(cls) -> bool:
+ """
+ Take a screenshot and convert it to grayscale.
+ Caches the screenshot for 0.5 seconds.
+
+ :return: Whether a new screenshot was taken or not
+ :rtype: bool
+ """
+ if cls._screenshot is None or time() - cls._screenshot_timestamp > 0.5:
+ cls._screenshot = cv2.cvtColor(array(screenshot()), cv2.COLOR_BGR2GRAY)
+ cls._screenshot_timestamp = time()
+ return True
+ return False
+
+ @classmethod
+ def _pda_screen_crop(cls) -> MatLike:
+ """
+ Crop the screenshot to the PDA screen area.
+
+ :return: Cropped image of the PDA screen
+ :rtype: cv2.typing.MatLike
+ """
+ # Take a new screenshot if necessary
+ cls._take_screenshot()
+
+ height: int
+ width: int
+ height, width = cls._screenshot.shape[:2]
+
+ # Define the crop area
+ x_left: int = width // 3
+ x_right: int = int(width / 1.536)
+ y_top: int = int(height / 3.85714)
+ y_bot: int = int(height / 1.30120)
+
+ return cls._screenshot[y_top:y_bot, x_left:x_right]
+
+ @classmethod
+ def circuits_crop(
+ cls,
+ cut_left: int | None = 40,
+ cut_right: int | None = 30,
+ pad_x: int | None = 8,
+ pad_y: int | None = 8
+ ) -> list[list[MatLike]]:
+ """
+ Crop the PDA screen into smaller segments representing circuits.
+
+ :param cut_left: Pixels to cut from the left side
+ :type cut_left: int | None
+ :param cut_right: Pixels to cut from the right side
+ :type cut_right: int | None
+ :param pad_x: Number of horizontal segments
+ :type pad_x: int | None
+ :param pad_y: Number of vertical segments
+ :type pad_y: int | None
+ :return: List of cropped segments
+ :rtype: list[list[cv2.typing.MatLike]]
+ """
+ # Crop the PDA screen
+ pda: MatLike = cls._pda_screen_crop()[:, cut_left:-cut_right]
+
+ pad_x: int = pad_x
+ pad_y: int = pad_y
+
+ pad_height: float = pda.shape[0] / pad_y
+ pad_width: float = pda.shape[1] / pad_x
+
+ pdas: list[list[ndarray]] = []
+ for y in range(pad_y):
+ row_list: list[ndarray] = []
+ for x in range(pad_x):
+ row_list.append(
+ pda[int(pad_height * y):int(pad_height * (y + 1)), int(pad_width * x):int(pad_width * (x + 1))])
+
+ pdas.append(row_list)
+
+ return pdas
+
+ @classmethod
+ def io_arrows_crop(cls) -> list[list[MatLike]]:
+ """
+ Crop the input/output arrows from the PDA screen.
+
+ :return: List of input and output arrows
+ :rtype: list[list[cv2.typing.MatLike]]
+ """
+ pdas: list[list[MatLike]] = cls.circuits_crop(0, 1, 20)
+
+ return [[row[0] for row in pdas], [row[-1] for row in pdas]]
+
+ @classmethod
+ def reset(cls) -> None:
+ """
+ Reset the cached screenshot.
+
+ :return: None
+ :rtype: None
+ """
+ cls._screenshot = None
+ cls._screenshot_timestamp = 0.0
+
+
+if __name__ == '__main__':
+ t1 = time()
+
+ ScreenInput.circuits_crop()
+ ScreenInput.io_arrows_crop()
+
+ print("\nTOTAL TIME:", time() - t1)
diff --git a/screen_service/_input_service.py b/screen_service/_input_service.py
deleted file mode 100644
index bdd38d9..0000000
--- a/screen_service/_input_service.py
+++ /dev/null
@@ -1,94 +0,0 @@
-"""
-AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
-Copyright (C) 2024 Lukas Krahbichler
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-
-Contact me: l.krahbichler@proton.me
-
-File: screen_service/_input_service.py
-Created: 18/04/2023
-"""
-
-##################################################
-# Imports #
-##################################################
-
-import numpy as np
-import pyautogui
-import cv2
-
-
-##################################################
-# Code #
-##################################################
-
-class InputService:
- """
- Input something from the screen
- """
-
- @staticmethod
- def screenshot() -> np.ndarray:
- return cv2.cvtColor(np.array(pyautogui.screenshot()), cv2.COLOR_BGR2GRAY)
- # img = cv2.imread("imgs/screen.png")
- # return np.array(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY))
-
- @classmethod
- def pda_whole(cls) -> np.ndarray:
- # 640, 280 - 1250, 830
- screen: np.ndarray = cls.screenshot()
-
- height: int
- width: int
- height, width = screen.shape[:2]
-
- x_left: int = width // 3
- x_right: int = int(width / 1.536)
- y_top: int = int(height / 3.85714)
- y_bot: int = int(height / 1.30120)
-
- return screen[y_top:y_bot, x_left:x_right]
-
- @classmethod
- def pda_io(cls) -> list[list[np.ndarray]]:
- pdas: list[list[np.ndarray]] = cls.pda_single(0, 1, 20, 8)
-
- return [[row[0] for row in pdas], [row[-1] for row in pdas]]
-
- @classmethod
- def pda_single(
- cls,
- cut_left: int | None = 40,
- cut_right: int | None = 30,
- pad_x: int | None = 8,
- pad_y: int | None = 8
- ) -> list[list[np.ndarray]]:
- pda: np.ndarray = cls.pda_whole()[:, cut_left:-cut_right]
-
- PAD_X: int = pad_x
- PAD_Y: int = pad_y
-
- pad_height: float = pda.shape[0] / PAD_Y
- pad_width: float = pda.shape[1] / PAD_X
-
- pdas: list[list[np.ndarray]] = []
- for y in range(PAD_Y):
- row_list: list[np.ndarray] = []
- for x in range(PAD_X):
- row_list.append(pda[int(pad_height*y):int(pad_height*(y+1)), int(pad_width*x):int(pad_width*(x+1))])
-
- pdas.append(row_list)
-
- return pdas
diff --git a/screen_service/_visualization.py b/screen_service/_visualization.py
deleted file mode 100644
index 9045139..0000000
--- a/screen_service/_visualization.py
+++ /dev/null
@@ -1,106 +0,0 @@
-"""
-AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
-Copyright (C) 2024 Lukas Krahbichler
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-
-Contact me: l.krahbichler@proton.me
-
-File: screen_service/_output_service.py
-Created: 18/04/2023
-"""
-
-##################################################
-# Imports #
-##################################################
-
-from tkinter import Tk, Canvas
-from PIL import Image, ImageTk
-from typing import Any
-import numpy as np
-
-
-##################################################
-# Code #
-##################################################
-
-class _Window(Tk):
- __imgs: list[ImageTk.PhotoImage]
-
- def __init__(self, *args: Any, **kwargs: Any) -> None:
- super().__init__(*args, **kwargs)
-
- self.__imgs = []
-
- def grid_pads(self, images: list[list[np.ndarray]], pad_styles: list[list[str]] | None = None,
- io_ports: tuple[int, int] | None = None) -> None:
- for slave in self.grid_slaves():
- slave.grid_forget()
- slave.destroy()
- self.__imgs = []
-
- for row, image_row in enumerate(images):
- for col, image_col in enumerate(image_row):
- self.__imgs.append(ImageTk.PhotoImage(image=Image.fromarray(image_col)))
- can = Canvas(self)
- width = self.__imgs[-1].width()
- height = self.__imgs[-1].height()
-
- can.configure(width=width, height=height)
-
- can.create_image(0, 0, anchor="nw", image=self.__imgs[-1])
-
- if pad_styles:
- if pad_styles[row][col] in "oneway_toponeway_bot":
- can.create_line(width//2, 0, width//2, height, fill="red", width=5)
- elif pad_styles[row][col] in "oneway_rightoneway_left":
- can.create_line(0, height//2, width, height//2, fill="red", width=5)
- elif pad_styles[row][col] in "corner_top_right":
- can.create_line(width//2, 0, width//2, height//2, fill="red", width=5)
- can.create_line(width//2, height//2, width, height//2, fill="red", width=5)
- elif pad_styles[row][col] in "corner_right_bot":
- can.create_line(width//2, height//2, width, height//2, fill="red", width=5)
- can.create_line(width//2, height//2, width//2, height, fill="red", width=5)
- elif pad_styles[row][col] in "corner_bot_left":
- can.create_line(width//2, height//2, width//2, height, fill="red", width=5)
- can.create_line(0, height//2, width//2, height//2, fill="red", width=5)
- elif pad_styles[row][col] in "corner_left_top":
- can.create_line(0, height//2, width//2, height//2, fill="red", width=5)
- can.create_line(width//2, 0, width//2, height//2, fill="red", width=5)
- elif pad_styles[row][col] in "junction_top":
- can.create_line(0, height//2, width, height//2, fill="red", width=5)
- can.create_line(width//2, 0, width//2, height//2, fill="red", width=5)
- elif pad_styles[row][col] in "junction_bot":
- can.create_line(0, height//2, width, height//2, fill="red", width=5)
- can.create_line(width//2, height//2, width//2, height, fill="red", width=5)
- elif pad_styles[row][col] in "junction_right":
- can.create_line(width//2, 0, width//2, height, fill="red", width=5)
- can.create_line(width//2, height//2, width, height//2, fill="red", width=5)
- elif pad_styles[row][col] in "junction_left":
- can.create_line(width//2, 0, width//2, height, fill="red", width=5)
- can.create_line(0, height//2, width//2, height//2, fill="red", width=5)
-
- if io_ports:
- if col == 0 and row == io_ports[0]:
- can.create_line(0, 0, 0, height, fill="green", width=15)
-
- if col == len(image_row)-1 and row == io_ports[1]:
- can.create_line(width, 0, width, height, fill="blue", width=15)
- can.grid(row=row, column=col, sticky="NSEW")
-
- self.grid_rowconfigure("all", weight=1)
- self.grid_columnconfigure("all", weight=1)
-
-
-Window = _Window()
diff --git a/settings/__init__.py b/settings/__init__.py
new file mode 100644
index 0000000..84a5e96
--- /dev/null
+++ b/settings/__init__.py
@@ -0,0 +1,24 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: settings/__init__.py
+Created: 03/08/2024
+"""
+
+from ._settings import Settings
diff --git a/settings/_migrator.py b/settings/_migrator.py
new file mode 100644
index 0000000..42abd33
--- /dev/null
+++ b/settings/_migrator.py
@@ -0,0 +1,129 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: settings/_migrator.py
+Created: 03/08/2024
+"""
+
+##################################################
+# Imports #
+##################################################
+
+from __future__ import annotations
+
+from typing import Any, Dict, Callable, TextIO
+from os import path
+
+
+##################################################
+# Code #
+##################################################
+
+class Migrator:
+ """
+ The Migrator class handles the migration of settings from one version to another.
+
+ Attributes:
+ _FILE (str): Path to the file storing the current version.
+ _version (str | None): Current version of the settings.
+ _migrated_version (str | None): Version to which settings have been migrated.
+ """
+ _FILE: str = "settings/version"
+ _version: str | None = None
+ _migrated_version: str | None = None
+
+ def __init__(self):
+ """
+ Initialize the Migrator object and read the current version from a file.
+ """
+ if path.isfile(self._FILE):
+ file: TextIO
+ with open(self._FILE) as file:
+ self._version = file.read().strip()
+
+ @staticmethod
+ def _migrate_1_0_0(settings: Dict[Any, Any]) -> Dict[Any, Any]:
+ """
+ Migrate settings to version 1.0.0.
+
+ :param settings: A dictionary containing the settings to be migrated.
+ :type settings: Dict[Any, Any]
+ :return: The migrated settings as a dictionary.
+ :rtype: Dict[Any, Any]
+ """
+ print("Migrating to 1.0.0")
+
+ if "mouse" not in settings:
+ settings["mouse"] = {
+ "click_delay": 0.02
+ }
+
+ return settings
+
+ def migrate_settings(self, settings: Dict[Any, Any]) -> Dict[Any, Any]:
+ """
+ Migrate the given settings to the latest version.
+
+ :param settings: The current settings.
+ :type settings: Dict[Any, Any]
+ :return: The migrated settings.
+ :rtype: Dict[Any, Any]
+ """
+ migrations: list[tuple[str, Callable[[Dict[Any, Any]], Dict[Any, Any]]]] = [
+ ("1.0.0", self._migrate_1_0_0),
+ ]
+
+ current_version: str = self._version or "0.0.0"
+
+ version: str
+ migrate_func: Callable[[Dict[Any, Any]], Dict[Any, Any]]
+ for version, migrate_func in migrations:
+ if self._compare_versions(version, current_version):
+ settings = migrate_func(settings)
+ current_version = version
+
+ self._migrated_version = current_version
+
+ return settings
+
+ def __del__(self) -> None:
+ """
+ Write the migrated version to the version file upon deletion of the object.
+ """
+ if self._migrated_version:
+ file: TextIO
+ with open(self._FILE, "w") as file:
+ file.write(self._migrated_version)
+ del self
+
+ @staticmethod
+ def _compare_versions(new_version: str, current_version: str) -> bool:
+ """
+ Compare two version strings to determine if the new version is greater than the current version.
+
+ :param new_version: The new version string.
+ :type new_version: str
+ :param current_version: The current version string.
+ :type current_version: str
+ :return: True if the new version is greater than the current version, False otherwise.
+ :rtype: bool
+ """
+ new_version_tuple: tuple[int, ...] = tuple(map(int, new_version.split('.')))
+ current_version_tuple: tuple[int, ...] = tuple(map(int, current_version.split('.')))
+ return new_version_tuple > current_version_tuple
diff --git a/settings/_settings.py b/settings/_settings.py
new file mode 100644
index 0000000..0cb1634
--- /dev/null
+++ b/settings/_settings.py
@@ -0,0 +1,190 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: settings/_settings.py
+Created: 03/08/2024
+"""
+
+##################################################
+# Imports #
+##################################################
+
+from __future__ import annotations
+
+from typing import Any, Dict, TextIO
+from os.path import isfile
+from pathlib import Path
+from json import dump, load
+
+from ._migrator import Migrator
+
+
+##################################################
+# Code #
+##################################################
+
+class _Settings:
+ """
+ A class for managing settings.
+
+ Attributes:
+ __path (str): The path where the settings file is located.
+ __file (str): The name of the settings file.
+ __default_file (str): The name of the default settings file.
+ __default_values (dict[Any, Any]): The default settings values.
+ __values (dict[Any, Any]): The current settings values.
+ __migrator (Migrator | None): The migrator instance for handling settings migrations.
+ """
+ __path: str
+ __file: str
+ __default_file: str
+ __migrator: Migrator | None
+ __default_values: dict[Any, Any]
+ __values: dict[Any, Any]
+
+ def __init__(
+ self,
+ path: str = "settings",
+ file: str = "settings.json",
+ default_file: str = "default.json"
+ ) -> None:
+ """
+ Initializes the settings object.
+
+ :param path: The path to the directory where the settings file is located.
+ :type path: str
+ :param file: The name of the settings file.
+ :type file: str
+ :param default_file: The name of the default settings file.
+ :type default_file: str
+ :return: None
+ :rtype: None
+ """
+ self.__path = f"{path.rstrip('/')}/"
+ self.__file = file
+ self.__default_file = default_file
+
+ self.__migrator = Migrator()
+
+ self.__values = {}
+ self.__default_values = {}
+
+ self.__load_defaults()
+ self.__load_values()
+
+ self.__migrator = None
+
+ @property
+ def values(self) -> Dict[Any, Any]:
+ """
+ Returns the current settings values.
+
+ :return: A dictionary containing the values.
+ :rtype: Dict[Any, Any]
+ """
+ return self.__values
+
+ def __setitem__(self, key: Any, value: Any) -> None:
+ """
+ Set the value of the specified key.
+
+ :param key: The key of the value to be set.
+ :type key: Any
+ :param value: The new value to set.
+ :type value: Any
+ :return: None
+ :rtype: None
+ """
+ self.__values[key] = value
+ self.write()
+
+ def __getitem__(self, item: Any) -> Any:
+ """
+ Retrieve the value associated with the specified item.
+
+ :param item: The item to be retrieved.
+ :type item: Any
+ :return: The value associated with the item.
+ :rtype: Any
+ """
+ return self.__values.get(item)
+
+ def __load_defaults(self) -> None:
+ """
+ Reads the default values from the 'default.json' file.
+
+ :return: None
+ :rtype: None
+ """
+ if isfile(f"{self.__path}{self.__default_file}"):
+ file: TextIO
+ with open(f"{self.__path}{self.__default_file}") as file:
+ self.__default_values = self.__migrator.migrate_settings(load(file))
+ else:
+ Path(self.__path).mkdir(parents=True, exist_ok=True)
+ self.__default_values = {}
+
+ def __load_values(self) -> None:
+ """
+ Reads the current settings values from the settings file, if it exists.
+
+ :return: None
+ :rtype: None
+ """
+ if isfile(f"{self.__path}{self.__file}"):
+ file: TextIO
+ with open(f"{self.__path}{self.__file}") as file:
+ self.__values = self.__migrator.migrate_settings(load(file))
+ else:
+ Path(self.__path).mkdir(parents=True, exist_ok=True)
+ self.__values = self.__default_values.copy()
+ self.write()
+
+ def write(self) -> None:
+ """
+ Writes the current settings values to the settings file.
+
+ :return: None
+ :rtype: None
+ """
+ try:
+ file: TextIO
+ with open(f"{self.__path}{self.__file}", 'w') as file:
+ dump(self.values, file, indent=4)
+ except NameError:
+ pass
+
+ def __del__(self) -> None:
+ """
+ Writes the current settings values to the settings file before deletion.
+
+ :return: None
+ :rtype: None
+ """
+ self.write()
+ try:
+ file: TextIO
+ with open(f"{self.__path}{self.__default_file}", "w") as file:
+ dump(self.__default_values, file, indent=4)
+ except NameError:
+ pass
+ del self
+
+
+Settings = _Settings()
diff --git a/terminal_interaction.py b/terminal_interaction.py
index f7e7f9b..4f1a793 100644
--- a/terminal_interaction.py
+++ b/terminal_interaction.py
@@ -17,7 +17,7 @@
Contact me: l.krahbichler@proton.me
-File: /terminal_interaction.py
+File: terminal_interaction.py
Created: 01/08/2024
"""
@@ -41,17 +41,11 @@ class Licensing:
Attributes:
__warranty (str): Stores the warranty information extracted from the license file.
__conditions (str): Stores the terms and conditions extracted from the license file.
-
- Methods:
- show_information()
- wait_for_commands()
-
- Please note that this documentation is in reStructuredText format, which is commonly used for Python docstrings.
"""
-
+
__warranty: str = ""
__conditions: str = ""
-
+
@classmethod
def __get_license_file(cls) -> None:
"""
@@ -59,52 +53,54 @@ def __get_license_file(cls) -> None:
and extracts specific sections from it using regular expressions.
:return: None
+ :rtype: None
"""
file: TextIO
- with open("LICENSE", "r") as file:
+ with open("LICENSE") as file:
content: str = file.read()
-
+
# Regex for extracting "TERMS AND CONDITIONS" section
conditions_pattern: Pattern[str] = compile(r"TERMS AND CONDITIONS.*?END OF TERMS AND CONDITIONS", DOTALL)
conditions_match = conditions_pattern.search(content)
if conditions_match:
- cls.__conditions = conditions_match.group(0)
-
+ cls.__conditions = conditions_match.group()
+
# Regex for extracting "Disclaimer of Warranty" section
warranty_pattern = compile(r"15\. Disclaimer of Warranty\..*?(?=16\. Limitation of Liability\.)",
DOTALL)
warranty_match = warranty_pattern.search(content)
if warranty_match:
- cls.__warranty = warranty_match.group(0)
-
+ cls.__warranty = warranty_match.group()
+
@staticmethod
def show_information() -> None:
"""
Display program license information.
:return: None
+ :rtype: None
"""
print(" Copyright (C) 2024 Lukas Krahbichler")
- print("This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.")
+ print("This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.") # noqa
print("This is free software, and you are welcome to redistribute it")
- print("under certain conditions; type `show c' for details.")
-
+ print("under certain conditions; type `show c' for details.") # noqa
+
@classmethod
def wait_for_commands(cls) -> None:
"""
Wait for commands in cmd.
:return: None
+ :rtype: None
"""
cls.__get_license_file()
-
+
while True:
- command = input("> ")
- match command.lower():
- case "show w":
- print(cls.__warranty)
- case "show c":
- print(cls.__conditions)
+ command = input("> ").lower()
+ if command == "show w":
+ print(cls.__warranty)
+ elif command == "show c":
+ print(cls.__conditions)
# Example usage
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..be7cd6a
--- /dev/null
+++ b/test/__init__.py
@@ -0,0 +1,22 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: screen/test/__init__.py
+Created: 03/08/2024
+"""
diff --git a/test/test_screen/__init__.py b/test/test_screen/__init__.py
new file mode 100644
index 0000000..5c3bf07
--- /dev/null
+++ b/test/test_screen/__init__.py
@@ -0,0 +1,22 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: test/screen/__init__.py
+Created: 03/08/2024
+"""
diff --git a/test/test_screen/_test_mouse.py b/test/test_screen/_test_mouse.py
new file mode 100644
index 0000000..e180f20
--- /dev/null
+++ b/test/test_screen/_test_mouse.py
@@ -0,0 +1,176 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: screen/_test_mouse.py
+Created: 03/08/2024
+"""
+
+##################################################
+# Imports #
+##################################################
+
+import unittest
+
+from unittest.mock import call, patch
+from os import remove, removedirs
+
+# noinspection PyProtectedMember
+from screen._mouse import Mouse, MouseEvent
+# noinspection PyProtectedMember
+from settings._settings import _Settings
+
+
+##################################################
+# Code #
+##################################################
+
+class TestMouse(unittest.TestCase):
+ """
+ Unit tests for the Mouse class in the screen._mouse module.
+ """
+
+ def tearDown(self) -> None:
+ """
+ Cleans up the test environment by removing the "settings" directory.
+ If the directory does not exist, no action is taken.
+
+ :return: None
+ :rtype: None
+ """
+ file: str
+ for file in ["settings/version", "settings/settings.json", "settings/default.json", "version"]:
+ try:
+ remove(file)
+ except FileNotFoundError:
+ pass
+ try:
+ removedirs("settings")
+ except OSError:
+ pass
+
+ @patch('screen._mouse.size', return_value=(1920, 1080))
+ @patch('screen._mouse.mouse_click')
+ @patch('screen._mouse.sleep', return_value=None)
+ def test_click_on_circuits(
+ self,
+ mock_sleep: unittest.mock.MagicMock,
+ mock_mouse_click: unittest.mock.MagicMock,
+ mock_size: unittest.mock.MagicMock
+ ) -> None:
+ """
+ Tests the Mouse.click_on_circuits() method to ensure it performs
+ the expected mouse clicks based on provided MouseEvent instances.
+
+ :param mock_sleep: Mock object for the sleep function.
+ :type: unittest.mock.MagicMock
+ :param mock_mouse_click: Mock object for the mouse_click function.
+ :type: unittest.mock.MagicMock
+ :param mock_size: Mock object for the screen size function.
+ :type: unittest.mock.MagicMock
+ :return: None
+ :rtype: None
+ """
+ # Mock settings instance
+ settings = _Settings()
+
+ with patch('screen._mouse.Settings', settings):
+ # Define the mouse events
+ circuits = [
+ MouseEvent(row=0, col=0, clicks=1),
+ MouseEvent(row=1, col=1, clicks=2),
+ MouseEvent(row=2, col=2, clicks=3)
+ ]
+
+ # Call the method
+ Mouse.click_on_circuits(circuits)
+
+ # Expected positions based on the calculations in the click_on_circuits method
+ width, height = mock_size.return_value
+ x_left = (width // 3) + 40
+ x_right = int(width / 1.536) - 30
+ y_top = int(height / 3.85714)
+ y_bot = int(height / 1.30120)
+
+ field_width = (x_right - x_left) // 8
+ field_height = (y_bot - y_top) // 8
+
+ expected_positions = [
+ (x_left + (field_width * 0) + (field_width // 2), y_top + (field_height * 0) + (field_height // 2)),
+ (x_left + (field_width * 1) + (field_width // 2), y_top + (field_height * 1) + (field_height // 2)),
+ (x_left + (field_width * 2) + (field_width // 2), y_top + (field_height * 2) + (field_height // 2))
+ ]
+
+ # Check if mouse_click was called with the correct positions and number of times
+ expected_calls = [
+ call(x=expected_positions[0][0], y=expected_positions[0][1], speed=0),
+ call(x=expected_positions[1][0], y=expected_positions[1][1], speed=0),
+ call(x=expected_positions[1][0], y=expected_positions[1][1], speed=0),
+ call(x=expected_positions[2][0], y=expected_positions[2][1], speed=0),
+ call(x=expected_positions[2][0], y=expected_positions[2][1], speed=0),
+ call(x=expected_positions[2][0], y=expected_positions[2][1], speed=0)
+ ]
+
+ mock_mouse_click.assert_has_calls(expected_calls)
+ self.assertEqual(mock_mouse_click.call_count, len(expected_calls))
+ self.assertEqual(mock_sleep.call_count, len(expected_calls))
+
+ @patch('screen._mouse.size', return_value=(1920, 1080))
+ @patch('screen._mouse.mouse_click')
+ @patch('screen._mouse.sleep', return_value=None)
+ def test_click_on_circuits_with_no_delay(
+ self,
+ mock_sleep: unittest.mock.MagicMock,
+ mock_mouse_click: unittest.mock.MagicMock,
+ _mock_size: unittest.mock.MagicMock
+ ) -> None:
+ """
+ Tests the Mouse.click_on_circuits() method with a click delay of 0.0 seconds.
+ Ensures that no sleep is called and the correct number of mouse clicks are performed.
+
+ :param mock_sleep: Mock object for the sleep function.
+ :type: unittest.mock.MagicMock
+ :param mock_mouse_click: Mock object for the mouse_click function.
+ :type: unittest.mock.MagicMock
+ :param _mock_size: Mock object for the screen size function.
+ :type: unittest.mock.MagicMock
+ :return: None
+ :rtype: None
+ """
+ # Mock settings instance
+ settings = _Settings()
+ settings["mouse"] = {"click_delay": 0.0}
+
+ with patch('screen._mouse.Settings', settings):
+ # Define the mouse events
+ circuits = [
+ MouseEvent(row=0, col=0, clicks=1),
+ MouseEvent(row=1, col=1, clicks=2),
+ MouseEvent(row=2, col=2, clicks=3)
+ ]
+
+ # Call the method
+ Mouse.click_on_circuits(circuits)
+
+ # Check if mouse_click was called with the correct positions and number of times
+ self.assertEqual(mock_mouse_click.call_count, 6)
+ self.assertEqual(mock_sleep.call_count, 0)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test/test_screen/_test_screen_input.py b/test/test_screen/_test_screen_input.py
new file mode 100644
index 0000000..065c9c1
--- /dev/null
+++ b/test/test_screen/_test_screen_input.py
@@ -0,0 +1,171 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: test/screen/_test_screen_input.py
+Created: 04/08/2024
+"""
+
+##################################################
+# Imports #
+##################################################
+
+import numpy as np
+import unittest
+
+from unittest.mock import patch, MagicMock
+from os import remove, removedirs
+
+# noinspection PyProtectedMember
+from screen._screen_input import ScreenInput
+
+
+##################################################
+# Code #
+##################################################
+
+class TestScreenInput(unittest.TestCase):
+ """
+ Unit tests for the ScreenInput class in the screen._screen_input module.
+ """
+
+ def tearDown(self) -> None:
+ """
+ Cleans up the test environment by removing the "settings" directory.
+ If the directory does not exist, no action is taken.
+
+ :return: None
+ :rtype: None
+ """
+ file: str
+ for file in ["settings/version", "settings/settings.json", "settings/default.json", "version"]:
+ try:
+ remove(file)
+ except FileNotFoundError:
+ pass
+ try:
+ removedirs("settings")
+ except OSError:
+ pass
+
+ @patch('screen._screen_input.screenshot', return_value=np.zeros((1080, 1920, 3), dtype=np.uint8))
+ @patch('screen._screen_input.cv2.cvtColor', return_value=np.zeros((1080, 1920), dtype=np.uint8))
+ @patch('screen._screen_input.array', return_value=np.zeros((1080, 1920, 3), dtype=np.uint8))
+ @patch('screen._screen_input.time', return_value=1000)
+ def test_take_screenshot(
+ self,
+ _mock_time: MagicMock,
+ _mock_array: MagicMock,
+ _mock_cv2_cvt_color: MagicMock,
+ _mock_screenshot: MagicMock
+ ) -> None:
+ """
+ Tests the __take_screenshot method to ensure it captures and processes the screenshot correctly.
+
+ :param _mock_time: Mock object for the time function.
+ :type: MagicMock
+ :param _mock_array: Mock object for the numpy array function.
+ :type: MagicMock
+ :param _mock_cv2_cvt_color: Mock object for the cv2.cvtColor function.
+ :type: MagicMock
+ :param _mock_screenshot: Mock object for the screenshot function.
+ :type: MagicMock
+ :return: None
+ :rtype: None
+ """
+ result = ScreenInput._take_screenshot()
+
+ self.assertTrue(result)
+ self.assertIsNotNone(ScreenInput._screenshot)
+ self.assertEqual(ScreenInput._screenshot_timestamp, 1000)
+
+ @patch('screen._screen_input.ScreenInput._take_screenshot', return_value=True)
+ @patch('screen._screen_input.ScreenInput._screenshot', np.zeros((1080, 1920), dtype=np.uint8))
+ def test_pda_screen_crop(self, _mock_take_screenshot: MagicMock) -> None:
+ """
+ Tests the __pda_screen_crop method to ensure it crops the screenshot correctly.
+
+ :param _mock_take_screenshot: Mock object for the __take_screenshot method.
+ :type: MagicMock
+ :return: None
+ :rtype: None
+ """
+ result = ScreenInput._pda_screen_crop()
+
+ self.assertIsNotNone(result)
+ self.assertEqual(result.shape[0], int(1080 / 1.30120) - int(1080 / 3.85714))
+ self.assertEqual(result.shape[1], int(1920 / 1.536) - (1920 // 3))
+
+ @patch('screen._screen_input.ScreenInput._take_screenshot')
+ @patch('screen._screen_input.ScreenInput._pda_screen_crop', return_value=np.zeros((550, 600), dtype=np.uint8))
+ def test_circuits_crop(self, _mock_pda_screen_crop: MagicMock, _mock_take_screenshot: MagicMock) -> None:
+ """
+ Tests the circuits_crop method to ensure it splits the cropped PDA screen into smaller segments correctly.
+
+ :param _mock_pda_screen_crop: Mock object for the __pda_screen_crop method.
+ :type: MagicMock
+ :param _mock_take_screenshot: Mock object for the _take_screenshot method.
+ :type: MagicMock
+ :return: None
+ :rtype: None
+ """
+ result = ScreenInput.circuits_crop()
+
+ self.assertIsNotNone(result)
+ self.assertEqual(len(result), 8)
+ self.assertEqual(len(result[0]), 8)
+ self.assertEqual(result[0][0].shape, (550 // 8, 530 // 8))
+
+ @patch('screen._screen_input.ScreenInput._take_screenshot')
+ @patch('screen._screen_input.ScreenInput.circuits_crop',
+ return_value=[[np.zeros((100, 100), dtype=np.uint8) for _ in range(20)] for _ in range(8)])
+ def test_io_arrows_crop(self, _mock_circuits_crop: MagicMock, _mock_take_screenshot: MagicMock) -> None:
+ """
+ Tests the io_arrows_crop method to ensure it extracts the input/output arrows from the circuits segments.
+
+ :param _mock_circuits_crop: Mock object for the circuits_crop method.
+ :type: MagicMock
+ :return: None
+ :rtype: None
+ """
+ result = ScreenInput.io_arrows_crop()
+
+ self.assertIsNotNone(result)
+ self.assertEqual(len(result), 2)
+ self.assertEqual(len(result[0]), 8)
+ self.assertEqual(result[0][0].shape, (100, 100))
+ self.assertEqual(result[1][0].shape, (100, 100))
+
+ def test_reset(self) -> None:
+ """
+ Tests the reset method to ensure it clears the cached screenshot and timestamp.
+
+ :return: None
+ :rtype: None
+ """
+ ScreenInput._ScreenInput__screenshot = np.zeros((1080, 1920), dtype=np.uint8)
+ ScreenInput._ScreenInput__screenshot_timestamp = 1000.0
+
+ ScreenInput.reset()
+
+ self.assertIsNone(ScreenInput._screenshot)
+ self.assertEqual(ScreenInput._screenshot_timestamp, 0.0)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test/test_settings/__init__.py b/test/test_settings/__init__.py
new file mode 100644
index 0000000..53cd04a
--- /dev/null
+++ b/test/test_settings/__init__.py
@@ -0,0 +1,22 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: test/settings/__init__.py
+Created: 04/08/2024
+"""
diff --git a/test/test_settings/_test_migrator.py b/test/test_settings/_test_migrator.py
new file mode 100644
index 0000000..4ce14e5
--- /dev/null
+++ b/test/test_settings/_test_migrator.py
@@ -0,0 +1,176 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: test/settings/_test_migrator.py
+Created: 04/08/2024
+"""
+
+##################################################
+# Imports #
+##################################################
+
+import unittest
+
+from unittest.mock import patch, mock_open, MagicMock
+from os import remove, removedirs
+from typing import Any
+
+# noinspection PyProtectedMember
+from settings._migrator import Migrator
+
+
+##################################################
+# Code #
+##################################################
+
+class TestMigrator(unittest.TestCase):
+ """
+ Unit tests for the Migrator class in the settings._migrator module.
+ """
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ """
+ Cleans up the test environment by removing the "settings" directory.
+
+ :return: None
+ :rtype: None
+ """
+ file: str
+ for file in ["settings/version", "settings/settings.json", "settings/default.json", "version"]:
+ try:
+ remove(file)
+ except FileNotFoundError:
+ pass
+ try:
+ removedirs("settings")
+ except OSError:
+ pass
+
+ # noinspection PyShadowingNames
+ @patch('settings._migrator.open', new_callable=mock_open, read_data="0.0.0")
+ @patch('settings._migrator.path.isfile', return_value=True)
+ def test_init(
+ self,
+ _mock_isfile: MagicMock,
+ mock_open: MagicMock
+ ) -> None:
+ """
+ Tests the initialization of the Migrator class.
+
+ :param _mock_isfile: Mock object for the path.isfile function.
+ :type: MagicMock
+ :param mock_open: Mock object for the open function with read_data parameter.
+ :type: MagicMock
+ :return: None
+ :rtype: None
+ """
+ migrator: Migrator = Migrator()
+ self.assertEqual(migrator._version, "0.0.0")
+ mock_open.assert_called_with("settings/version")
+
+ def test_migrate_1_0_0(self) -> None:
+ """
+ Tests migration of settings from version 1.0.0 to the latest version.
+
+ :return: None
+ :rtype: None
+ """
+ settings: dict[Any, Any] = {}
+ expected_settings = {
+ "mouse": {
+ "click_delay": 0.02
+ }
+ }
+ migrated_settings: dict[Any, Any] = Migrator._migrate_1_0_0(settings)
+ self.assertEqual(migrated_settings, expected_settings)
+
+ # noinspection PyShadowingNames
+ @patch('settings._migrator.open', new_callable=mock_open, read_data="0.0.0")
+ @patch('settings._migrator.path.isfile', return_value=True)
+ @patch('settings._migrator.Migrator._migrate_1_0_0', side_effect=Migrator._migrate_1_0_0)
+ def test_migrate_settings(
+ self,
+ mock_migrate_1_0_0: MagicMock,
+ _mock_isfile: MagicMock,
+ mock_open: MagicMock
+ ) -> None:
+ """
+ Tests the migrate_settings method of the Migrator class.
+
+ :param mock_migrate_1_0_0: Mock object for the _migrate_1_0_0 method.
+ :type: MagicMock
+ :param _mock_isfile: Mock object for the path.isfile function.
+ :type: MagicMock
+ :param mock_open: Mock object for the open function.
+ :type: MagicMock
+ :return: None
+ :rtype: None
+ """
+ migrator: Migrator = Migrator()
+ settings: dict[Any, Any] = {}
+ expected_settings = {
+ "mouse": {
+ "click_delay": 0.02
+ }
+ }
+ migrated_settings: dict[Any, Any] = migrator.migrate_settings(settings)
+ self.assertEqual(migrated_settings, expected_settings)
+ mock_migrate_1_0_0.assert_called_once()
+
+ def test_compare_versions(self) -> None:
+ """
+ Tests the _compare_versions method of the Migrator class.
+
+ :return: None
+ :rtype: None
+ """
+ self.assertTrue(Migrator._compare_versions("1.0.0", "0.0.0"))
+ self.assertFalse(Migrator._compare_versions("0.0.0", "1.0.0"))
+ self.assertFalse(Migrator._compare_versions("1.0.0", "1.0.0"))
+
+ # noinspection PyShadowingNames
+ @patch('settings._migrator.open', new_callable=mock_open)
+ @patch('settings._migrator.path.isfile', return_value=True)
+ def test_del(
+ self,
+ _mock_isfile: MagicMock,
+ mock_open: MagicMock
+ ) -> None:
+ """
+ Tests the __del__ method of the Migrator class.
+
+ :param _mock_isfile: Mock object for the path.isfile function.
+ :type: MagicMock
+ :param mock_open: Mock object for the open function.
+ :type: MagicMock
+ :return: None
+ :rtype: None
+ """
+ mock_open().read.return_value = "0.0.0"
+
+ migrator: Migrator = Migrator()
+ migrator._migrated_version = "1.0.0"
+ del migrator
+ mock_open.assert_any_call("settings/version", "w")
+ mock_open().write.assert_called_with("1.0.0")
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test/test_settings/_test_settings.py b/test/test_settings/_test_settings.py
new file mode 100644
index 0000000..a6db6df
--- /dev/null
+++ b/test/test_settings/_test_settings.py
@@ -0,0 +1,198 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: test/settings/_test_settings.py
+Created: 04/08/2024
+"""
+
+##################################################
+# Imports #
+##################################################
+
+import unittest
+
+from unittest.mock import patch, mock_open, MagicMock
+from os import remove, removedirs
+
+# noinspection PyProtectedMember
+from settings._settings import _Settings
+
+
+##################################################
+# Code #
+##################################################
+
+class TestSettings(unittest.TestCase):
+ @classmethod
+ def tearDownClass(cls) -> None:
+ """
+ Cleans up the test environment by removing the "settings" directory.
+ If the directory does not exist, no action is taken.
+
+ :return: None
+ :rtype: None
+ """
+ file: str
+ for file in ["settings/version", "settings/settings.json", "settings/default.json", "version"]:
+ try:
+ remove(file)
+ except FileNotFoundError:
+ pass
+ try:
+ removedirs("settings")
+ except OSError:
+ pass
+
+ # noinspection PyShadowingNames
+ @patch('settings._settings.open', new_callable=mock_open, read_data='{"key": "value"}')
+ @patch('settings._settings.isfile', return_value=True)
+ @patch('settings._settings.Path.mkdir')
+ @patch('settings._settings.Migrator.migrate_settings', side_effect=lambda x: x)
+ def test_load_defaults(
+ self,
+ mock_migrate: MagicMock,
+ _mock_mkdir: MagicMock,
+ _mock_isfile: MagicMock,
+ mock_open: MagicMock
+ ) -> None:
+ """
+ Tests loading default settings from the default configuration file.
+
+ :param mock_migrate: Mock object for the Migrator.migrate_settings function.
+ :type: MagicMock
+ :param _mock_mkdir: Mock object for the Path.mkdir function.
+ :type: MagicMock
+ :param _mock_isfile: Mock object for the isfile function.
+ :type: MagicMock
+ :param mock_open: Mock object for the open function.
+ :type: MagicMock
+ :return: None
+ :rtype: None
+ """
+ settings: _Settings = _Settings()
+ self.assertEqual(settings.values, {"key": "value"})
+ mock_open.assert_any_call("settings/default.json")
+ mock_migrate.assert_called()
+
+ # noinspection PyShadowingNames
+ @patch('settings._settings.open', new_callable=mock_open, read_data='{"key": "value"}')
+ @patch('settings._settings.isfile', return_value=True)
+ @patch('settings._settings.Path.mkdir')
+ @patch('settings._settings.Migrator.migrate_settings', side_effect=lambda x: x)
+ def test_load_values(
+ self,
+ mock_migrate: MagicMock,
+ _mock_mkdir: MagicMock,
+ _mock_isfile: MagicMock,
+ mock_open: MagicMock
+ ) -> None:
+ """
+ Tests loading settings from the settings configuration file.
+
+ :param mock_migrate: Mock object for the Migrator.migrate_settings function.
+ :type: MagicMock
+ :param _mock_mkdir: Mock object for the Path.mkdir function.
+ :type: MagicMock
+ :param _mock_isfile: Mock object for the isfile function.
+ :type: MagicMock
+ :param mock_open: Mock object for the open function.
+ :type: MagicMock
+ :return: None
+ :rtype: None
+ """
+ settings: _Settings = _Settings()
+ self.assertEqual(settings.values, {"key": "value"})
+ mock_open.assert_any_call("settings/settings.json")
+ mock_migrate.assert_called()
+
+ # noinspection PyShadowingNames
+ @patch('settings._settings.open', new_callable=mock_open)
+ @patch('settings._settings.dump')
+ @patch('settings._settings.isfile', return_value=False)
+ @patch('settings._settings.Path.mkdir')
+ @patch('settings._settings.Migrator.migrate_settings', side_effect=lambda x: x)
+ def test_write(
+ self,
+ _mock_migrate: MagicMock,
+ _mock_mkdir: MagicMock,
+ _mock_isfile: MagicMock,
+ mock_json_dump: MagicMock,
+ mock_open: MagicMock
+ ) -> None:
+ """
+ Tests writing settings to the configuration file.
+
+ :param _mock_migrate: Mock object for the Migrator.migrate_settings function.
+ :type: MagicMock
+ :param _mock_mkdir: Mock object for the Path.mkdir function.
+ :type: MagicMock
+ :param _mock_isfile: Mock object for the isfile function.
+ :type: MagicMock
+ :param mock_json_dump: Mock object for the json.dump function.
+ :type: MagicMock
+ :param mock_open: Mock object for the open function.
+ :type: MagicMock
+ :return: None
+ :rtype: None
+ """
+ mock_open().read.side_effect = ['{"key": "value"}']
+
+ settings: _Settings = _Settings()
+ settings["key"] = "new_value"
+ mock_open.assert_any_call("settings/settings.json", "w")
+ mock_json_dump.assert_called_with({"key": "new_value"}, mock_open(), indent=4)
+
+ # noinspection PyShadowingNames
+ @patch('settings._settings.open', new_callable=mock_open)
+ @patch('settings._settings.dump')
+ @patch('settings._settings.Path.mkdir')
+ @patch('settings._settings.Migrator.migrate_settings', side_effect=lambda x: x)
+ def test_del(
+ self,
+ _mock_migrate: MagicMock,
+ _mock_mkdir: MagicMock,
+ mock_json_dump: MagicMock,
+ mock_open: MagicMock
+ ) -> None:
+ """
+ Tests deletion of settings and ensures settings files are written.
+
+ :param _mock_migrate: Mock object for the Migrator.migrate_settings function.
+ :type: MagicMock
+ :param _mock_mkdir: Mock object for the Path.mkdir function.
+ :type: MagicMock
+ :param mock_json_dump: Mock object for the json.dump function.
+ :type: MagicMock
+ :param mock_open: Mock object for the open function.
+ :type: MagicMock
+ :return: None
+ :rtype: None
+ """
+ mock_open().read.side_effect = ['{"key": "value"}', '{"key": "value"}']
+
+ # noinspection PyUnusedLocal
+ settings: _Settings = _Settings()
+ settings.__del__()
+ mock_open.assert_any_call("settings/settings.json", "w")
+ mock_open.assert_any_call("settings/default.json", "w")
+ self.assertTrue(mock_json_dump.called)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/timer.py b/timer.py
new file mode 100644
index 0000000..bb999f3
--- /dev/null
+++ b/timer.py
@@ -0,0 +1,93 @@
+"""
+AutoHackerPDA: A program that automates the process of "Hacking PDA" in the game "Thief Simulator".
+Copyright (C) 2024 Lukas Krahbichler
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+Contact me: l.krahbichler@proton.me
+
+File: timer.py
+Created: 03/08/2024
+"""
+
+##################################################
+# Imports #
+##################################################
+
+from typing import Any, Callable, Type
+from time import time
+
+
+##################################################
+# Code #
+##################################################
+
+def apply_to_methods(decorator: Callable[[Callable[..., Any]], Callable[..., Any]]) -> Callable[[Type], Type]:
+ """
+ Class decorator that applies the given decorator to all methods in the class.
+
+ :param decorator: The decorator to apply to each method.
+ :type decorator: Callable[[Callable[..., Any]], Callable[..., Any]]
+ :return: The class with decorated methods.
+ :rtype: Callable[[Type], Type]
+ """
+
+ def class_decorator(cls: Type) -> Type:
+ """
+ Apply a decorator to all methods of a class.
+
+ :return: The class decorator.
+ :rtype: Callable[[Type], Type]
+ """
+ # Iterate through all attributes of the class
+ for attr_name in dir(cls):
+ attr = getattr(cls, attr_name)
+ # Check if the attribute is a method
+ if callable(attr) and not attr_name.startswith("__"):
+ # Decorate the method
+ decorated_method = decorator(attr)
+ setattr(cls, attr_name, decorated_method)
+ return cls
+
+ return class_decorator
+
+
+def timer(func: Callable[..., Any]) -> Callable[..., Any]:
+ """
+ A decorator function that measures the execution time of a given function.
+
+ :param func: The function to be measured.
+ :type func: Callable[[Any], Any]
+ :return: The wrapper function.
+ :rtype: Callable[[Any], Any]
+ """
+
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
+ """
+ Decorator to measure the execution time of a function.
+
+ :param args: The positional arguments to be passed to the function.
+ :type args: list[Any]
+ :param kwargs: The keyword arguments to be passed to the function.
+ :type kwargs: dict[str, Any]
+ :return: The result of the function.
+ :rtype: Any
+ """
+ start = time()
+ result = func(*args, **kwargs)
+ end = time()
+ print(f"Function {func.__name__} ran in: {end - start} seconds")
+ return result
+
+ return wrapper