Repository: Tuxemon/Tuxemon

Issue:
Failed to start on windows: OSError: cannot load file I18n
**Describe the bug**
An error occurs when I try to run the "run_tuxemon.exe". Details in the screenshot.

**To Reproduce**
I just used the tuxemon_windows_binary.zip for v0.4.34. The same issues happens when using the current tuxemon-installer.exe 

**Expected behavior**
I would expect the game to launch.

**Screenshots**
![The error](https://user-images.githubusercontent.com/361185/183768448-d65d34d9-a7af-4059-b786-9a5cf1d1136c.png)

Note that neither the folders "C:\hostedtoolcache" nor "D:\a" actually exist.

**Desktop (please complete the following information):**
 - OS: Windows 10
 - Python Version 3.9.13, according to the screenshot.
 - Actually installed python versions on my system are "2.7.18" and "3.10.6"

**Additional context**
Building from source doesn't work for me either, but that's a different issue that I'll have to look into.



Discussion:
@Murmele not sure if this is due to the GitHub actions, or if the script was already broken. Probably the latter.
Sounds like the latter one, because I just copy pasted the build of the windows CI file. I will check out in the CI file if I forgott something. Are you able to reproduce the buildlocally to find out if it is a package issue or an application issue?
I am removing the broken artifacts from the release, for investigating the bug you can get them from here:

https://github.com/Tuxemon/Tuxemon/actions/runs/2810055930
ok thanks!
When I am looking into the executable, l18n is here: mods/tuxemon/l18n/
Tried with wine I get the same error message
![image](https://user-images.githubusercontent.com/10099533/184180103-d6d07a95-978e-4f70-8a35-d21198ae5d87.png)

the l18n is normaly located in .tuxemon/cache or?
When executing the installer, l18n is in .wine/drive_c/Program Files (x86)/Tuxemon/mods/tuxemon/l18n/ 
Yes, l18n is usually within .tuxemon/cache

There are two folders called l18n each containing en_US/LC_MESSAGES and then a single file:

base.po (pre-compiled translations) is within mods/tuxemon, base.mo (compiled translations) is within the cache.

@Murmele could you comment out the automatic windows publishing (leave the build) until somebody fixes this?
@kerizane yes I can do
I looked into it: The reason for the bug is that LIBDIR on Windows is `exe.win-amd64-3.12\lib\tuxemon`, the mods folder is calculated in the code with `mods_folder = os.path.normpath(os.path.join(LIBDIR, "..", "mods"))`, so it searches in `\exe.win-amd64-3.12\lib\mods`, which doesn't exist. The only mods folder on disk that actually exists is `exe.win-amd64-3.12\mods`. So it should be really easy to fix. (Unfortunately I don't understand the code well enough to understand the desired behaviour and submit a PR.)



Patch:
diff --git a/tuxemon/ai.py b/tuxemon/ai.py
index 4c5c09d528..0af7a2452d 100644
--- a/tuxemon/ai.py
+++ b/tuxemon/ai.py
@@ -6,6 +6,7 @@
 import random
 from abc import ABC, abstractmethod
 from dataclasses import dataclass
+from pathlib import Path
 from typing import TYPE_CHECKING, Any, Optional
 
 import yaml
@@ -113,9 +114,9 @@ class AITechniques:
     techniques: dict[str, SingleTechnique]
 
 
-def load_yaml(filepath: str) -> Any:
+def load_yaml(filepath: Path) -> Any:
     try:
-        with open(filepath) as file:
+        with filepath.open() as file:
             return yaml.safe_load(file)
     except FileNotFoundError:
         logger.error(f"Config file not found: {filepath}")
@@ -133,7 +134,7 @@ class AIConfigLoader:
 
     @classmethod
     def get_ai_opponent(cls, filename: str) -> AIOpponent:
-        yaml_path = f"{paths.mods_folder}/{filename}"
+        yaml_path = paths.mods_folder / filename
         if cls._ai_opponent is None:
             raw_map = load_yaml(yaml_path)
 
@@ -150,7 +151,7 @@ def get_ai_opponent(cls, filename: str) -> AIOpponent:
 
     @classmethod
     def get_ai_items(cls, filename: str) -> AIItems:
-        yaml_path = f"{paths.mods_folder}/{filename}"
+        yaml_path = paths.mods_folder / filename
         if cls._ai_items is None:
             raw_map = load_yaml(yaml_path)
             cls._ai_items = AIItems(**raw_map)
@@ -158,7 +159,7 @@ def get_ai_items(cls, filename: str) -> AIItems:
 
     @classmethod
     def get_ai_character(cls, filename: str) -> AITrainers:
-        yaml_path = f"{paths.mods_folder}/{filename}"
+        yaml_path = paths.mods_folder / filename
         if cls._ai_character is None:
             raw_map = load_yaml(yaml_path)
 
@@ -189,7 +190,7 @@ def get_ai_character(cls, filename: str) -> AITrainers:
 
     @classmethod
     def get_ai_techniques(cls, filename: str) -> AITechniques:
-        yaml_path = f"{paths.mods_folder}/{filename}"
+        yaml_path = paths.mods_folder / filename
         if cls._ai_techniques is None:
             raw_map = load_yaml(yaml_path)
 
diff --git a/tuxemon/audio.py b/tuxemon/audio.py
index 92b31f65c0..f7dab68713 100644
--- a/tuxemon/audio.py
+++ b/tuxemon/audio.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import logging
-import os.path
+from pathlib import Path
 from typing import Optional, Protocol
 
 import pygame
@@ -164,11 +164,13 @@ def get_sound_filename(self, slug: str) -> Optional[str]:
         filename = db.lookup_file("sounds", slug)
         filename = transform_resource_filename("sounds", filename)
 
-        if not os.path.exists(filename):
+        path = Path(filename)
+
+        if not path.exists():
             logger.error(f"audio file does not exist: {filename}")
             return None
 
-        return filename
+        return path.as_posix()
 
     def load_sound(
         self, slug: str, value: float = prepare.CONFIG.sound_volume
diff --git a/tuxemon/cli/processor.py b/tuxemon/cli/processor.py
index 8691e16bb1..aa99ce18b9 100644
--- a/tuxemon/cli/processor.py
+++ b/tuxemon/cli/processor.py
@@ -2,9 +2,9 @@
 # Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
 from __future__ import annotations
 
-import os
 import sys
 from collections.abc import Iterable, Sequence
+from pathlib import Path
 from typing import TYPE_CHECKING
 
 from prompt_toolkit import PromptSession
@@ -74,9 +74,9 @@ class CommandProcessor:
     def __init__(self, client: LocalPygameClient, prompt: str = "> ") -> None:
         self.prompt = prompt
         self.client = client
-        folder = os.path.join(os.path.dirname(__file__), "commands")
+        folder = Path(__file__).parent / "commands"
         # TODO: add folder(s) from mods
-        commands = list(self.collect_commands(folder))
+        commands = list(self.collect_commands(folder.as_posix()))
         self.root_command = MetaCommand(commands)
 
     def run(self) -> None:
diff --git a/tuxemon/client.py b/tuxemon/client.py
index 2007f52c86..0f10c94282 100644
--- a/tuxemon/client.py
+++ b/tuxemon/client.py
@@ -6,7 +6,7 @@
 import time
 from collections.abc import Mapping, Sequence
 from enum import Enum
-from os.path import basename
+from pathlib import Path
 from threading import Thread
 from typing import Any, Optional, TypeVar, Union, overload
 
@@ -260,9 +260,7 @@ def get_map_name(self) -> str:
         map_path = self.map_manager.get_map_filepath()
         if map_path is None:
             raise ValueError("Name of the map requested when no map is active")
-
-        # extract map name from path
-        return basename(map_path)
+        return Path(map_path).name
 
     """
     The following methods provide an interface to the state stack
diff --git a/tuxemon/constants/paths.py b/tuxemon/constants/paths.py
index 164578af9c..b0d103c639 100644
--- a/tuxemon/constants/paths.py
+++ b/tuxemon/constants/paths.py
@@ -1,10 +1,10 @@
 # SPDX-License-Identifier: GPL-3.0
 # Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
 import logging
-import os
 import sys
+from pathlib import Path
 
-from tuxemon import platform
+from tuxemon.platform import get_system_storage_dirs, get_user_storage_dir
 
 logger = logging.getLogger(__name__)
 
@@ -18,69 +18,60 @@
 # --- Core Game Paths ---
 
 # LIBDIR is where the tuxemon lib is
-LIBDIR = os.path.normpath(
-    os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
-)
+LIBDIR = Path(__file__).resolve().parent.parent
 logger.debug(f"libdir: {LIBDIR}")
 
 # BASEDIR is where tuxemon was launched from
-BASEDIR = os.path.normpath(sys.path[0])
+BASEDIR = Path(sys.path[0]).resolve()
 logger.debug(f"basedir: {BASEDIR}")
 
 # mods
-mods_folder = os.path.normpath(os.path.join(LIBDIR, "..", "mods"))
+mods_folder = (LIBDIR.parent / "mods").resolve()
 logger.debug(f"mods: {mods_folder}")
 
 # mods subfolders
-mods_subfolders = [
-    f
-    for f in os.listdir(mods_folder)
-    if os.path.isdir(os.path.join(mods_folder, f))
-]
+mods_subfolders = [f.name for f in mods_folder.iterdir() if f.is_dir()]
 logger.debug(f"Mods subfolders: {mods_subfolders}")
 
 # action/condition plugins (eventually move out of lib folder)
-CONDITIONS_PATH = os.path.normpath(os.path.join(LIBDIR, "event/conditions"))
-ACTIONS_PATH = os.path.normpath(os.path.join(LIBDIR, "event/actions"))
+CONDITIONS_PATH = LIBDIR / "event" / "conditions"
+ACTIONS_PATH = LIBDIR / "event" / "actions"
 
-CORE_EFFECT_PATH = os.path.normpath(os.path.join(LIBDIR, "core/effects"))
-CORE_CONDITION_PATH = os.path.normpath(os.path.join(LIBDIR, "core/conditions"))
+CORE_EFFECT_PATH = LIBDIR / "core" / "effects"
+CORE_CONDITION_PATH = LIBDIR / "core" / "conditions"
 
 # --- User Data Paths ---
 
 # main game and config dir
-USER_STORAGE_DIR = (
-    platform.get_user_storage_dir()
-)  # Ensure this doesn't depend on pygame
+# Ensure this doesn't depend on pygame
+USER_STORAGE_DIR = get_user_storage_dir()
 logger.debug(f"userdir: {USER_STORAGE_DIR}")
 
 # config file paths
 CONFIG_FILE = "tuxemon.yaml"
-USER_CONFIG_PATH = os.path.normpath(
-    os.path.join(USER_STORAGE_DIR, CONFIG_FILE)
-)
+USER_CONFIG_PATH = USER_STORAGE_DIR / CONFIG_FILE
 logger.debug(f"user config: {USER_CONFIG_PATH}")
 
 # game data dir
-USER_GAME_DATA_DIR = os.path.normpath(os.path.join(USER_STORAGE_DIR, "data"))
+USER_GAME_DATA_DIR = USER_STORAGE_DIR / "data"
 logger.debug(f"user game data: {USER_GAME_DATA_DIR}")
 
 # game savegame dir
-USER_GAME_SAVE_DIR = os.path.normpath(os.path.join(USER_STORAGE_DIR, "saves"))
+USER_GAME_SAVE_DIR = USER_STORAGE_DIR / "saves"
 logger.debug(f"save games: {USER_GAME_SAVE_DIR}")
 
 # game cache dir
-CACHE_DIR = os.path.normpath(os.path.join(USER_STORAGE_DIR, "cache"))
+CACHE_DIR = USER_STORAGE_DIR / "cache"
 logger.debug(f"cache: {CACHE_DIR}")
 
 # game lang dir
-L18N_MO_FILES = os.path.normpath(os.path.join(CACHE_DIR, "l18n"))
+L18N_MO_FILES = CACHE_DIR / "l18n"
 logger.debug(f"l18: {L18N_MO_FILES}")
 
 # --- System Paths ---
 
 # shared locations
 system_installed_folders = [
-    os.path.normpath(path) for path in platform.get_system_storage_dirs()
+    path.resolve() for path in get_system_storage_dirs()
 ]
 logger.debug(f"system folders: {system_installed_folders}")
diff --git a/tuxemon/core/effects/fishing.py b/tuxemon/core/effects/fishing.py
index 119584c5e4..6b44641317 100644
--- a/tuxemon/core/effects/fishing.py
+++ b/tuxemon/core/effects/fishing.py
@@ -5,6 +5,7 @@
 import logging
 import random
 from dataclasses import dataclass
+from pathlib import Path
 from typing import TYPE_CHECKING, Any, Union
 
 import yaml
@@ -47,9 +48,9 @@ def validate_parameters(self) -> None:
             raise ValueError("Lower bound cannot be greater than upper bound.")
 
 
-def load_yaml(filepath: str) -> Any:
+def load_yaml(filepath: Path) -> Any:
     try:
-        with open(filepath) as file:
+        with filepath.open() as file:
             return yaml.safe_load(file)
     except FileNotFoundError:
         logger.error(f"Config file not found: {filepath}")
@@ -64,7 +65,7 @@ class Loader:
 
     @classmethod
     def get_config_fishing(cls, filename: str) -> dict[str, FishingConfig]:
-        yaml_path = f"{paths.mods_folder}/{filename}"
+        yaml_path = paths.mods_folder / filename
         if not cls._config_fishing:
             raw_map = load_yaml(yaml_path)
             cls._config_fishing = {
diff --git a/tuxemon/db.py b/tuxemon/db.py
index c6820ef442..a3b3fff6b9 100644
--- a/tuxemon/db.py
+++ b/tuxemon/db.py
@@ -5,11 +5,11 @@
 import difflib
 import json
 import logging
-import os
 from collections.abc import Mapping, Sequence
 from dataclasses import dataclass
 from enum import Enum
 from importlib import import_module
+from pathlib import Path
 from typing import Annotated, Any, Literal, Optional, Union, overload
 
 import yaml
@@ -1657,15 +1657,15 @@ def __init__(
         self, active_mods: list[str], base_path: str = "mods"
     ) -> None:
         self.active_mods = active_mods
-        self.base_path = base_path
+        self.base_path = Path(base_path)
 
     def load_metadata(self) -> dict[str, dict[str, Any]]:
         metadata = {}
         for mod_directory in self.active_mods:
-            mod_path = os.path.join(self.base_path, mod_directory, "mod.json")
-            if os.path.exists(mod_path):
+            mod_path = self.base_path / mod_directory / "mod.json"
+            if mod_path.exists():
                 try:
-                    with open(mod_path) as f:
+                    with mod_path.open() as f:
                         metadata[mod_directory] = json.load(f)
                 except json.JSONDecodeError as e:
                     logger.error(
@@ -1716,35 +1716,38 @@ def load(
 
 class DataLoader:
     def __init__(self, path: str, config: DatabaseConfig):
-        self.path = path
+        self.path = Path(path)
         self.config = config
 
     def load_files(self, directory: TableName) -> dict[str, Any]:
         preloaded_data: dict[str, Any] = {}
         extensions = self.config.file_extensions
-        for entry in os.scandir(os.path.join(self.path, directory)):
+        directory_path = self.path / directory
+        for entry in directory_path.iterdir():
             if entry.is_file() and any(
-                entry.name.endswith(ext) for ext in extensions
+                entry.suffix == ext for ext in extensions
             ):
                 try:
-                    with open(entry.path) as fp:
-                        if entry.name.endswith(".json"):
-                            item = json.load(fp)
-                        else:
-                            item = yaml.safe_load(fp)
+                    with entry.open() as fp:
+                        item = (
+                            json.load(fp)
+                            if entry.suffix == ".json"
+                            else yaml.safe_load(fp)
+                        )
+
                     if isinstance(item, list):
                         for sub_item in item:
                             self._load_dict(
-                                sub_item, entry.path, preloaded_data
+                                sub_item, entry.as_posix(), preloaded_data
                             )
                     else:
-                        self._load_dict(item, entry.path, preloaded_data)
+                        self._load_dict(item, entry.as_posix(), preloaded_data)
                 except (
                     json.JSONDecodeError,
                     yaml.YAMLError,
                     FileNotFoundError,
                 ) as e:
-                    logger.error(f"Error loading file '{entry.path}': {e}")
+                    logger.error(f"Error loading file '{entry}': {e}")
         return preloaded_data
 
     def _load_dict(
@@ -1776,7 +1779,6 @@ def __init__(self, config: DatabaseConfig) -> None:
         self.model_map = load_model_map(config.model_map)
         self.loader = ModelLoader(self.model_map)
         self._load_mod_metadata()
-        self.path = ""
         if self.config.mod_tables:
             for mod, tables in self.config.mod_tables.items():
                 if mod in self.config.active_mods:
@@ -1835,8 +1837,10 @@ def _preload_table(
     def _preload_table_from_mod(
         self, table: TableName, mod_directory: str
     ) -> None:
-        self.path = os.path.join(
-            config.mod_base_path, mod_directory, config.mod_db_subfolder
+        self.path = (
+            Path(config.mod_base_path)
+            / mod_directory
+            / config.mod_db_subfolder
         )
         if (
             self.config.mod_versions
@@ -1845,10 +1849,10 @@ def _preload_table_from_mod(
             logger.info(
                 f"Loading mod '{mod_directory}' version {self.config.mod_versions[mod_directory]}"
             )
-        if not os.path.exists(self.path):
+        if not self.path.exists():
             logger.warning(f"Mod directory '{self.path}' not found.")
             return
-        db_path = os.path.join(self.path, table)
+        db_path = self.path / str(table)
         if (
             self.config.mod_table_exclusions
             and mod_directory in self.config.mod_table_exclusions
@@ -1856,8 +1860,8 @@ def _preload_table_from_mod(
         ):
             logger.info(f"Table '{table}' excluded by mod '{mod_directory}'.")
             return
-        if os.path.exists(db_path):
-            data_loader = DataLoader(self.path, self.config)
+        if db_path.exists():
+            data_loader = DataLoader(self.path.as_posix(), self.config)
             if table not in self.preloaded:
                 self.preloaded[table] = {}
             self.preloaded[table].update(data_loader.load_files(table))
@@ -2131,11 +2135,11 @@ def reload(self, table: TableName, validate: bool = True) -> None:
                     if table in tables and mod in self.config.active_mods
                 ]
                 for mod in mods_associated:
-                    mod_path = os.path.join(
-                        self.config.mod_base_path,
-                        mod,
-                        self.config.mod_db_subfolder,
-                    )
+                    mod_path = (
+                        Path(self.config.mod_base_path)
+                        / mod
+                        / self.config.mod_db_subfolder
+                    )  # Using pathlib
                     logger.info(
                         f"Preloading table '{table}' from mod path '{mod_path}'."
                     )
@@ -2223,10 +2227,9 @@ def file(self, file: str) -> bool:
         Returns:
             True if file exists
         """
-
         try:
-            path = prepare.fetch(file)
-            return os.path.exists(path)
+            path = Path(prepare.fetch(file))
+            return path.exists()
         except OSError:
             return False
 
@@ -2275,7 +2278,7 @@ def db_entry(self, table: TableName, slug: str) -> bool:
         return slug in self.db.preloaded[table]
 
 
-path = prepare.fetch(mods_folder, "db_config.json")
+path = prepare.fetch(mods_folder.as_posix(), "db_config.json")
 config = load_config(path)
 # Global database container
 db = ModData(config)
diff --git a/tuxemon/event/actions/celestial_cycles.py b/tuxemon/event/actions/celestial_cycles.py
index 38d46e1e3e..bf80705f9e 100644
--- a/tuxemon/event/actions/celestial_cycles.py
+++ b/tuxemon/event/actions/celestial_cycles.py
@@ -4,6 +4,7 @@
 
 import logging
 from dataclasses import dataclass
+from pathlib import Path
 from typing import Any, final
 
 import yaml
@@ -24,9 +25,9 @@ class CelestialCycle:
     phase_data: list[tuple[int, str]]
 
 
-def load_yaml(filepath: str) -> Any:
+def load_yaml(filepath: Path) -> Any:
     try:
-        with open(filepath) as file:
+        with filepath.open() as file:
             return yaml.safe_load(file)
     except FileNotFoundError:
         logger.error(f"Config file not found: {filepath}")
@@ -41,7 +42,7 @@ class Loader:
 
     @classmethod
     def get_config_celestial_cycle(cls, filename: str) -> list[CelestialCycle]:
-        yaml_path = f"{paths.mods_folder}/{filename}"
+        yaml_path = paths.mods_folder / filename
         if not cls._config_celestial_cycle:
             raw_data = load_yaml(yaml_path)
             cls._config_celestial_cycle = [
diff --git a/tuxemon/event/actions/load_yaml.py b/tuxemon/event/actions/load_yaml.py
index 7a73c2065d..494088f348 100644
--- a/tuxemon/event/actions/load_yaml.py
+++ b/tuxemon/event/actions/load_yaml.py
@@ -3,8 +3,8 @@
 from __future__ import annotations
 
 import logging
-import os
 from dataclasses import dataclass
+from pathlib import Path
 from typing import final
 
 from tuxemon import prepare
@@ -37,14 +37,18 @@ class LoadYamlAction(EventAction):
 
     def start(self, session: Session) -> None:
         client = session.client
-        yaml_path = prepare.fetch("maps", f"{self.file}.yaml")
+        yaml_path = Path(prepare.fetch("maps", f"{self.file}.yaml"))
 
         _events = list(client.map_manager.events)
         _inits = list(client.map_manager.inits)
-        if os.path.exists(yaml_path):
-            yaml_events = YAMLEventLoader().load_events(yaml_path, "event")
+        if yaml_path.exists():
+            yaml_events = YAMLEventLoader().load_events(
+                yaml_path.as_posix(), "event"
+            )
             _events.extend(yaml_events["event"])
-            yaml_inits = YAMLEventLoader().load_events(yaml_path, "init")
+            yaml_inits = YAMLEventLoader().load_events(
+                yaml_path.as_posix(), "init"
+            )
             _inits.extend(yaml_inits["init"])
         else:
             raise ValueError(f"{yaml_path} doesn't exist")
diff --git a/tuxemon/event/eventaction.py b/tuxemon/event/eventaction.py
index 0c3e0628ee..97215a70da 100644
--- a/tuxemon/event/eventaction.py
+++ b/tuxemon/event/eventaction.py
@@ -264,7 +264,7 @@ def cleanup(self, session: Session) -> None:
 class ActionManager:
     def __init__(self) -> None:
         self.actions = load_plugins(
-            ACTIONS_PATH,
+            ACTIONS_PATH.as_posix(),
             "actions",
             interface=EventAction,  # type: ignore[type-abstract]
         )
diff --git a/tuxemon/event/eventcondition.py b/tuxemon/event/eventcondition.py
index 021547510e..64dfdf2f3e 100644
--- a/tuxemon/event/eventcondition.py
+++ b/tuxemon/event/eventcondition.py
@@ -39,7 +39,7 @@ def done(self) -> bool:
 class ConditionManager:
     def __init__(self) -> None:
         self.conditions = load_plugins(
-            CONDITIONS_PATH,
+            CONDITIONS_PATH.as_posix(),
             "conditions",
             interface=EventCondition,
         )
diff --git a/tuxemon/formula.py b/tuxemon/formula.py
index effea62d67..cc76ad9781 100644
--- a/tuxemon/formula.py
+++ b/tuxemon/formula.py
@@ -7,6 +7,7 @@
 import random
 from collections.abc import Sequence
 from dataclasses import dataclass
+from pathlib import Path
 from typing import TYPE_CHECKING, Any, Optional, Union
 
 import yaml
@@ -114,9 +115,9 @@ def validate_multiplier_map(self) -> None:
                 )
 
 
-def load_yaml(filepath: str) -> Any:
+def load_yaml(filepath: Path) -> Any:
     try:
-        with open(filepath) as file:
+        with filepath.open() as file:
             return yaml.safe_load(file)
     except FileNotFoundError:
         logger.error(f"Config file not found: {filepath}")
@@ -135,7 +136,7 @@ class Loader:
 
     @classmethod
     def get_capture_devices(cls, filename: str) -> CaptureDevicesConfig:
-        yaml_path = f"{paths.mods_folder}/{filename}"
+        yaml_path = paths.mods_folder / filename
         if cls._capture_devices is None:
             raw_map = load_yaml(yaml_path)
             items = {}
@@ -177,7 +178,7 @@ def get_capture_devices(cls, filename: str) -> CaptureDevicesConfig:
 
     @classmethod
     def get_config_combat(cls, filename: str) -> CombatConfig:
-        yaml_path = f"{paths.mods_folder}/{filename}"
+        yaml_path = paths.mods_folder / filename
         if cls._config_combat is None:
             raw_map = load_yaml(yaml_path)
             cls._config_combat = CombatConfig(**raw_map)
@@ -185,7 +186,7 @@ def get_config_combat(cls, filename: str) -> CombatConfig:
 
     @classmethod
     def get_config_monster(cls, filename: str) -> MonsterConfig:
-        yaml_path = f"{paths.mods_folder}/{filename}"
+        yaml_path = paths.mods_folder / filename
         if cls._config_monster is None:
             raw_map = load_yaml(yaml_path)
             cls._config_monster = MonsterConfig(**raw_map)
@@ -193,7 +194,7 @@ def get_config_monster(cls, filename: str) -> MonsterConfig:
 
     @classmethod
     def get_config_capture(cls, filename: str) -> CaptureConfig:
-        yaml_path = f"{paths.mods_folder}/{filename}"
+        yaml_path = paths.mods_folder / filename
         if cls._config_capture is None:
             raw_map = load_yaml(yaml_path)
             cls._config_capture = CaptureConfig(**raw_map)
@@ -201,7 +202,7 @@ def get_config_capture(cls, filename: str) -> CaptureConfig:
 
     @classmethod
     def get_range_map(cls, filename: str) -> dict[str, RangeMapEntry]:
-        yaml_path = f"{paths.mods_folder}/{filename}"
+        yaml_path = paths.mods_folder / filename
         if not cls._range_map:
             raw_map = load_yaml(yaml_path)
             cls._range_map = {
diff --git a/tuxemon/graphics.py b/tuxemon/graphics.py
index 9ee90b80dd..bed3c897a8 100644
--- a/tuxemon/graphics.py
+++ b/tuxemon/graphics.py
@@ -9,9 +9,9 @@
 from __future__ import annotations
 
 import logging
-import os
 import re
 from collections.abc import Iterable, Sequence
+from pathlib import Path
 from typing import TYPE_CHECKING, Any, Optional, Protocol, Union
 
 from pygame.color import Color
@@ -201,11 +201,12 @@ def load_animated_sprite(
     """
     anim = []
     for filename in filenames:
-        if os.path.exists(filename):
-            image = load_and_scale(filename, scale)
+        path = Path(filename)
+        if path.exists():
+            image = load_and_scale(path.as_posix(), scale)
             anim.append((image, delay))
         else:
-            logger.error(f"File not found: {filename}")
+            logger.error(f"File not found: {path}")
 
     if not anim:
         raise ValueError("Cannot create animated sprite: no valid frames.")
@@ -259,11 +260,14 @@ def animation_frame_files(directory: str, name: str) -> Sequence[str]:
 
     """
     pattern = re.compile(rf"{name}\.?_?[0-9]+\.png")
-    frames = [
-        os.path.join(directory, filename)
-        for filename in sorted(os.listdir(directory))
-        if pattern.match(filename)
-    ]
+    dir_path = Path(directory)
+    frames = sorted(
+        [
+            file.as_posix()
+            for file in dir_path.iterdir()
+            if file.is_file() and pattern.match(file.name)
+        ]
+    )
     return frames
 
 
diff --git a/tuxemon/headless_client.py b/tuxemon/headless_client.py
index 36a0a47e07..a7e3a0df0c 100644
--- a/tuxemon/headless_client.py
+++ b/tuxemon/headless_client.py
@@ -6,7 +6,7 @@
 import time
 from collections.abc import Mapping, Sequence
 from enum import Enum
-from os.path import basename
+from pathlib import Path
 from typing import Any, Optional, TypeVar, Union, overload
 
 from tuxemon.audio import MusicPlayerState, SoundManager
@@ -195,9 +195,7 @@ def get_map_name(self) -> str:
         map_path = self.map_manager.get_map_filepath()
         if map_path is None:
             raise ValueError("Name of the map requested when no map is active")
-
-        # extract map name from path
-        return basename(map_path)
+        return Path(map_path).name
 
     """
     The following methods provide an interface to the state stack
diff --git a/tuxemon/item/item.py b/tuxemon/item/item.py
index 0b96880050..cb4e8a5b99 100644
--- a/tuxemon/item/item.py
+++ b/tuxemon/item/item.py
@@ -66,11 +66,11 @@ def __init__(self, save_data: Optional[Mapping[str, Any]] = None) -> None:
 
         if Item.effect_manager is None:
             Item.effect_manager = EffectManager(
-                ItemEffect, paths.CORE_EFFECT_PATH
+                ItemEffect, paths.CORE_EFFECT_PATH.as_posix()
             )
         if Item.condition_manager is None:
             Item.condition_manager = ConditionManager(
-                CoreCondition, paths.CORE_CONDITION_PATH
+                CoreCondition, paths.CORE_CONDITION_PATH.as_posix()
             )
 
         self.effects: Sequence[PluginObject] = []
diff --git a/tuxemon/locale.py b/tuxemon/locale.py
index 2e70ba6957..288896bab0 100644
--- a/tuxemon/locale.py
+++ b/tuxemon/locale.py
@@ -4,11 +4,10 @@
 
 import gettext
 import logging
-import os
-import os.path
 from collections.abc import Callable, Generator, Iterable, Mapping, Sequence
 from dataclasses import dataclass
 from gettext import GNUTranslations
+from pathlib import Path
 from typing import Any, Optional
 
 from babel.messages.mofile import write_mo
@@ -45,7 +44,7 @@ class LocaleFinder:
     """
 
     def __init__(self, root_dir: str) -> None:
-        self.root_dir = root_dir
+        self.root_dir = Path(root_dir)
         self.locale_names: set[str] = set()
 
     def search_locales(self) -> Generator[LocaleInfo, Any, None]:
@@ -56,21 +55,24 @@ def search_locales(self) -> Generator[LocaleInfo, Any, None]:
             LocaleInfo: Information about each found locale.
         """
         logger.debug("searching locales...")
-        for locale in os.listdir(self.root_dir):
-            self.locale_names.add(locale)
-            locale_path = os.path.join(self.root_dir, locale)
-            if os.path.isdir(locale_path):
-                for category in os.listdir(locale_path):
-                    category_path = os.path.join(locale_path, category)
-                    if os.path.isdir(category_path):
-                        for name in os.listdir(category_path):
-                            path = os.path.join(category_path, name)
-                            if os.path.isfile(path) and name.endswith(".po"):
-                                domain = name[:-3]
+        for locale_path in self.root_dir.iterdir():
+            if locale_path.is_dir():
+                self.locale_names.add(locale_path.name)
+                for category_path in locale_path.iterdir():
+                    if category_path.is_dir():
+                        for file_path in category_path.iterdir():
+                            if (
+                                file_path.is_file()
+                                and file_path.suffix == ".po"
+                            ):
+                                domain = file_path.stem
                                 info = LocaleInfo(
-                                    locale, category, domain, path
+                                    locale_path.name,
+                                    category_path.name,
+                                    domain,
+                                    file_path.as_posix(),
                                 )
-                                logger.debug(f"found: {info}")
+                                logger.debug(f"Found: {info}")
                                 yield info
 
     def has_locale(self, locale_name: str) -> bool:
@@ -105,13 +107,16 @@ def compile_gettext(self, po_path: str, mo_path: str) -> None:
             po_path: The path to the gettext translation file (.po) to compile.
             mo_path: The path to store the compiled translation file (.mo).
         """
-        mofolder = os.path.dirname(mo_path)
-        os.makedirs(mofolder, exist_ok=True)
-        with open(po_path, encoding="UTF8") as po_file:
+        path = Path(mo_path)
+        mofolder = path.parent
+        mofolder.mkdir(parents=True, exist_ok=True)
+
+        with Path(po_path).open(encoding="UTF8") as po_file:
             catalog = read_po(po_file)
-        with open(mo_path, "wb") as mo_file:
+
+        with path.open("wb") as mo_file:
             write_mo(mo_file, catalog)
-            logger.debug(f"writing l18n mo: {mo_path}")
+            logger.debug(f"writing l18n mo: {path}")
 
     def get_mo_path(self, locale: str, category: str, domain: str) -> str:
         """
@@ -126,9 +131,13 @@ def get_mo_path(self, locale: str, category: str, domain: str) -> str:
             The path to the MO file.
             l18n/locale/LC_category/domain_name.mo
         """
-        return os.path.join(
-            self.cache_dir, LOCALE_DIR, locale, category, domain + ".mo"
-        )
+        return (
+            Path(self.cache_dir)
+            / LOCALE_DIR
+            / locale
+            / category
+            / f"{domain}.mo"
+        ).as_posix()
 
 
 class TranslatorPo:
@@ -172,11 +181,15 @@ def build_translations(self, recompile_translations: bool = False) -> None:
                 translations).
         """
         for info in self.locale_finder.search_locales():
-            mo_path = self.gettext_compiler.get_mo_path(
-                info.locale, info.category, info.domain
+            mo_path = Path(
+                self.gettext_compiler.get_mo_path(
+                    info.locale, info.category, info.domain
+                )
             )
-            if recompile_translations or not os.path.exists(mo_path):
-                self.gettext_compiler.compile_gettext(info.path, mo_path)
+            if recompile_translations or not mo_path.exists():
+                self.gettext_compiler.compile_gettext(
+                    info.path, mo_path.as_posix()
+                )
                 logger.info(f"Built translation file: {mo_path}")
         logger.info("Translation files built successfully")
 
@@ -204,10 +217,13 @@ def load_translator(
             domain: Name of the domain.
         """
         logger.debug(f"loading translator for: {locale_name}")
-        localedir = os.path.join(paths.CACHE_DIR, LOCALE_DIR)
-        fallback = gettext.translation("base", localedir, [FALLBACK_LOCALE])
+        localedir = paths.L18N_MO_FILES
+        fallback = gettext.translation(
+            "base", localedir.as_posix(), [FALLBACK_LOCALE]
+        )
         trans = (
-            self._get_translation(locale_name, domain, localedir) or fallback
+            self._get_translation(locale_name, domain, localedir.as_posix())
+            or fallback
         )
 
         if trans is fallback:
@@ -297,8 +313,10 @@ def has_translation(self, locale_name: str, msgid: str) -> bool:
         Returns:
             True if the translation exists, False otherwise.
         """
-        localedir = os.path.join(paths.CACHE_DIR, LOCALE_DIR)
-        trans = self._get_translation(locale_name, "base", localedir)
+        localedir = paths.L18N_MO_FILES
+        trans = self._get_translation(
+            locale_name, "base", localedir.as_posix()
+        )
         if trans is None:
             return False
         return trans.gettext(msgid) != msgid
@@ -560,6 +578,6 @@ def process_translate_text(
 
 
 locale_finder = LocaleFinder(prepare.fetch("l18n"))
-gettext_compiler = GettextCompiler(paths.CACHE_DIR)
+gettext_compiler = GettextCompiler(paths.CACHE_DIR.as_posix())
 T = TranslatorPo(locale_finder, gettext_compiler)
 T.collect_languages()
diff --git a/tuxemon/log.py b/tuxemon/log.py
index 8a835c257b..dfac90c190 100644
--- a/tuxemon/log.py
+++ b/tuxemon/log.py
@@ -1,12 +1,12 @@
 # SPDX-License-Identifier: GPL-3.0
 # Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
 import logging
-import os
 import subprocess
 import sys
 import time
 import warnings
 from operator import itemgetter
+from pathlib import Path
 
 from tuxemon import prepare
 from tuxemon.constants import paths
@@ -66,14 +66,13 @@ def configure() -> None:
             logger.addHandler(log_strm)
 
             if config.log_to_file:
-                log_dir = os.path.realpath(f"{paths.USER_STORAGE_DIR}/logs")
-                os.makedirs(
-                    log_dir, exist_ok=True
-                )  # creates the directory if it does not exists.
+                log_dir = paths.USER_STORAGE_DIR / "logs"
+                log_dir.mkdir(parents=True, exist_ok=True)
+
                 if config.log_keep_max > 0:
                     log_dir_files = {
                         entry.name: entry.stat().st_mtime
-                        for entry in os.scandir(log_dir)
+                        for entry in log_dir.iterdir()
                         if entry.is_file()
                     }
                     sorted_files = sorted(
@@ -82,19 +81,17 @@ def configure() -> None:
 
                     if len(sorted_files) > config.log_keep_max:
                         for x in range(config.log_keep_max, len(sorted_files)):
-                            filename = sorted_files[x][0]
-                            file_path = os.path.join(log_dir, filename)
-                            os.remove(file_path)
+                            Path(log_dir / sorted_files[x][0]).unlink()
 
-                    formatted_time = time.strftime(
-                        "%Y-%m-%d_%Hh%Mm%Ss", time.localtime()
-                    )
-                    log_file = logging.FileHandler(
-                        os.path.join(log_dir, f"{formatted_time}.log")
-                    )
-                    log_file.setFormatter(log_formatter)
-                    log_file.setLevel(log_level)
-                    logger.addHandler(log_file)
+                formatted_time = time.strftime(
+                    "%Y-%m-%d_%Hh%Mm%Ss", time.localtime()
+                )
+                log_file = logging.FileHandler(
+                    log_dir / f"{formatted_time}.log"
+                )
+                log_file.setFormatter(log_formatter)
+                log_file.setLevel(log_level)
+                logger.addHandler(log_file)
 
             loggers[logger_name] = logger
 
diff --git a/tuxemon/map_manager.py b/tuxemon/map_manager.py
index 2a515df22e..2a716bd172 100644
--- a/tuxemon/map_manager.py
+++ b/tuxemon/map_manager.py
@@ -23,9 +23,9 @@ class MapType:
 
 
 def load_map_types(filename: str) -> list[MapType]:
-    yaml_path = f"{paths.mods_folder}/{filename}"
     """Loads map types from a YAML file and returns a list of MapType instances."""
-    with open(yaml_path, encoding="utf-8") as file:
+    yaml_path = paths.mods_folder / filename
+    with yaml_path.open(encoding="utf-8") as file:
         data = yaml.safe_load(file)
         return [MapType(**entry) for entry in data.get("map_types", [])]
 
diff --git a/tuxemon/map_view.py b/tuxemon/map_view.py
index 8b5b2673aa..b94db0f2f6 100644
--- a/tuxemon/map_view.py
+++ b/tuxemon/map_view.py
@@ -3,10 +3,10 @@
 from __future__ import annotations
 
 import logging
-import os
 from dataclasses import dataclass
 from enum import Enum
 from itertools import chain
+from pathlib import Path
 from typing import TYPE_CHECKING, Optional
 
 import pygame
@@ -195,11 +195,13 @@ def _load_standing_sprites(self, template: NpcTemplateModel) -> None:
                     if is_interactive_object
                     else f"{template.sprite_name}_{facing.value}.png"
                 )
-                path = os.path.join(
-                    "sprites_obj" if is_interactive_object else "sprites",
-                    filename,
+                path = (
+                    Path("sprites_obj" if is_interactive_object else "sprites")
+                    / filename
+                )
+                sprite_dict[facing] = load_and_scale_with_cache(
+                    path.as_posix()
                 )
-                sprite_dict[facing] = load_and_scale_with_cache(path)
             standing_sprite_cache[template.sprite_name] = sprite_dict
         else:
             logger.info(
diff --git a/tuxemon/menu/input.py b/tuxemon/menu/input.py
index 54c9fdcef2..58f51d98c6 100644
--- a/tuxemon/menu/input.py
+++ b/tuxemon/menu/input.py
@@ -22,8 +22,8 @@
 
 
 def load_names(file_path: str) -> Any:
-    yaml_path = f"{paths.mods_folder}/{file_path}"
-    with open(yaml_path) as file:
+    yaml_path = paths.mods_folder / file_path
+    with yaml_path.open() as file:
         return yaml.safe_load(file)
 
 
diff --git a/tuxemon/mod_manager/__init__.py b/tuxemon/mod_manager/__init__.py
index 079abd8724..5d2ff60299 100644
--- a/tuxemon/mod_manager/__init__.py
+++ b/tuxemon/mod_manager/__init__.py
@@ -1,13 +1,11 @@
 # SPDX-License-Identifier: GPL-3.0
 # Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
-import configparser
 import json
 import logging
 import os
-import pathlib
 import shutil
-import urllib.request
 import zipfile
+from pathlib import Path
 from typing import Any, Optional
 
 import requests
@@ -28,18 +26,18 @@ def sanitize_paths(path: str) -> str:
 
 class Manager:
     def __init__(
-        self, *other_urls: Any, default_to_cache: bool = True
+        self, *other_urls: str, default_to_cache: bool = True
     ) -> None:
         """
         (basic) Mod managment library.
         """
 
-        if len(other_urls) == 0:
-            other_urls = ["http://127.0.0.1:5000"]
+        if not other_urls:
+            other_urls = ("http://127.0.0.1:5000",)
 
-        self.packages_path = os.path.join(paths.CACHE_DIR, "packages")
+        self.packages_path = paths.CACHE_DIR / "packages"
 
-        self.url = other_urls
+        self.url: tuple[str, ...] = other_urls
         self.packages: list[Any] = []
 
         if default_to_cache:
@@ -48,12 +46,12 @@ def __init__(
     def write_to_cache(self) -> None:
         """Writes self.packages to the cache file"""
 
-        with open(self.packages_path, "w") as file:
+        with self.packages_path.open("w") as file:
             file.write(json.dumps(self.packages, indent=4))
 
     def read_from_cache(self) -> Any:
         """Read self.packages from the cache file"""
-        with open(self.packages_path) as file:
+        with self.packages_path.open() as file:
             return json.loads(file.read())
 
     def cache_to_pkglist(self) -> None:
@@ -134,8 +132,8 @@ def download_package(
             + f"/packages/{author}/{name}/releases/{release}/download"
         )
 
-        filename = os.path.join(
-            paths.CACHE_DIR, f"downloaded_packages/{name}.{release}.zip"
+        filename = (
+            paths.CACHE_DIR / f"downloaded_packages/{name}.{release}.zip"
         )
 
         logging.info(f"Downloading release {release} of {author}/{name}")
@@ -145,19 +143,19 @@ def download_package(
             file_size = r.headers["content-length"]
             downloaded_size = 0
             r.raise_for_status()
-            with open(filename, "wb") as file:
+            with filename.open("wb") as file:
                 for chunk in r.iter_content(chunk_size=8096):
                     file.write(chunk)
                     downloaded_size += 8096
                     logger.debug(f"Downloaded {downloaded_size} bytes")
 
-        outfolder = os.path.join(paths.BASEDIR, "mods", name)
-
-        self.write_package_to_list(os.path.relpath(outfolder), name)
+        outfolder = paths.BASEDIR / "mods" / name
+        outfolder.mkdir(parents=True, exist_ok=True)
+        self.write_package_to_list(outfolder.as_posix(), name)
 
         if not dont_extract:
             logging.info("Extracting...")
-            self.install_local_package(filename, name=name)
+            self.install_local_package(filename.as_posix(), name=name)
 
         if install_deps:
             # This function calls download_package, might cause issues
@@ -179,8 +177,7 @@ def install_dependencies(
         **args: Any,
     ) -> None:
         """Recursively resolve dependencies and symlink them"""
-        logger.debug(author, name, repo)
-        # Request dependencies for specified package
+        logger.debug(f"Resolving dependencies for {author}/{name} from {repo}")
         r = requests.get(
             f"{repo}/api/packages/{author}/{name}/dependencies/?only_hard=1"
         )
@@ -188,16 +185,15 @@ def install_dependencies(
             raise ValueError(
                 f"Requested {r.url}, received status code {r.status_code}"
             )
-        logger.debug(r.text, author, name)
+        logger.debug(f"Dependency data received: {r.text}")
         dep_list = r.json()
         # Resolve dependencies
         for dependency in dep_list:
             for entry in dep_list[dependency]:
                 for package in entry["packages"]:
                     package = sanitize_paths(package)
-                    if os.path.exists(
-                        os.path.join(paths.BASEDIR, "mods", package)
-                    ):
+                    package_path = paths.BASEDIR / "mods" / package
+                    if package_path.exists():
                         continue
                     if package == "default":
                         continue
@@ -244,7 +240,7 @@ def get_package_repo(self, name: str) -> Any:
     def write_package_to_list(self, path_to_folder: str, name: str) -> None:
         """Writes specified package to the package list"""
         # Write the absolute path to the list
-        with open(paths.USER_GAME_DATA_DIR + "/package.list", "w+") as file:
+        with (paths.USER_GAME_DATA_DIR / "package.list").open("w+") as file:
             if not len(file.read()) == 0:
                 before = json.loads(file.read())
             else:
@@ -256,14 +252,14 @@ def write_package_to_list(self, path_to_folder: str, name: str) -> None:
 
     def read_package_from_list(self, name: str) -> Any:
         """Reads path of the specified mod"""
-        with open(paths.USER_GAME_DATA_DIR + "/package.list") as file:
+        with (paths.USER_GAME_DATA_DIR / "package.list").open() as file:
             data = file.read()
             return json.loads(data)[name]
 
     def remove_package_from_list(self, name: str) -> None:
         """Removes specified package from the package list"""
         # Write the absolute path to the list
-        with open(paths.USER_GAME_DATA_DIR + "/package.list", "r+") as file:
+        with (paths.USER_GAME_DATA_DIR / "package.list").open("r+") as file:
             data = file.read()
             if not len(data) == 0:
                 before = json.loads(data)
@@ -293,16 +289,16 @@ def install_local_package(
     ) -> None:
         """
         Installs local packages.
-        Based on the download_package function, but without the downloads
+        Based on the download_package function, but without the downloads.
         """
-        outfolder = os.path.join(paths.BASEDIR, "mods")
-        self.write_package_to_list(os.path.relpath(outfolder), name)
+        outfolder = paths.BASEDIR / "mods"
+        outfolder.mkdir(parents=True, exist_ok=True)
+        self.write_package_to_list(outfolder.as_posix(), name)
         with zipfile.ZipFile(filename) as zipf:
-            free = shutil.disk_usage(os.getcwd()).free
-            # get the filesize, based on https://stackoverflow.com/a/39953116/14590202
+            free_space = shutil.disk_usage(Path.cwd()).free
             zipsize = sum(zinfo.file_size for zinfo in zipf.filelist)
-            if zipsize > free:
+            if zipsize > free_space:
                 raise OSError(
-                    f"Zip contents are bigger than available disk space ({zipsize} > {free})"
+                    f"Zip contents are bigger than available disk space ({zipsize} > {free_space})"
                 )
-            zipf.extractall(path=os.path.join(outfolder, name))
+            zipf.extractall(path=outfolder / name)
diff --git a/tuxemon/mod_manager/tuxemoncontentserver.py b/tuxemon/mod_manager/tuxemoncontentserver.py
index 2e1a228f5e..f06a60a55e 100644
--- a/tuxemon/mod_manager/tuxemoncontentserver.py
+++ b/tuxemon/mod_manager/tuxemoncontentserver.py
@@ -6,7 +6,6 @@
 """
 
 import json
-import os
 import urllib.request
 from typing import Any
 
@@ -46,9 +45,8 @@ def download_package(
         release = str(release).replace(char, "_")
 
     url = str(repo) + f"/packages/{name}/releases/{str(release)}/download"
-    filename = os.path.join(
-        paths.CACHE_DIR,
-        f"downloaded_packages/{name}.{release}.zip",
+    filename = (
+        paths.CACHE_DIR / "downloaded_packages" / f"{name}.{release}.zip"
     )
 
     # Apparently this function is ported from urllib from python2.
@@ -56,13 +54,13 @@ def download_package(
     # https://docs.python.org/3/library/urllib.request.html#urllib.request.urlretrieve
     urllib.request.urlretrieve(url, filename=filename)
 
-    outfolder = os.path.join(paths.BASEDIR, "mods", f"{name}")
+    outfolder = paths.BASEDIR / "mods" / name
 
     self.write_package_to_list(outfolder, name)
 
     if not dont_extract:
         self.extract(filename, outfolder)
-        with open(f"{outfolder}/meta.json", "w") as metafile:
+        with (outfolder / "meta.json").open("w") as metafile:
             meta = self.get_package_info(name, repo)
             metafile.write(json.dumps(meta, indent=4, sort_keys=False))
 
@@ -118,8 +116,8 @@ def install_dependencies(
         )
 
         # Symlink deps
-        mainfolder = os.path.join(paths.BASEDIR, "mods", name)
-        depfolder = os.path.join(paths.BASEDIR, "mods", pack)
-        symlink_missing(mainfolder, depfolder)
+        mainfolder = paths.BASEDIR / "mods" / name
+        depfolder = paths.BASEDIR / "mods" / pack
+        symlink_missing(mainfolder.as_posix(), depfolder)
     else:
         pass
diff --git a/tuxemon/platform/__init__.py b/tuxemon/platform/__init__.py
index 324c94d351..d7087a303f 100644
--- a/tuxemon/platform/__init__.py
+++ b/tuxemon/platform/__init__.py
@@ -1,5 +1,7 @@
+# SPDX-License-Identifier: GPL-3.0
+# Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
 """
-Put platform specific fixes here
+Platform-specific implementations and configurations.
 """
 
 from __future__ import annotations
@@ -9,24 +11,31 @@
 from collections.abc import Sequence
 from pathlib import Path
 
-__all__ = ("android", "init", "mixer", "get_user_storage_dir")
+__all__ = (
+    "init",
+    "mixer",
+    "get_user_storage_dir",
+    "get_system_storage_dirs",
+    "is_android",
+)
 
 logger = logging.getLogger(__name__)
 
 _pygame = False
-android = None
+android_module = None
 
 try:
     import android
     import android.mixer as android_mixer
 
     mixer = android_mixer
+    android_module = android
 except ImportError:
     pass
 else:
     logger.info("Using Android mixer")
 
-if android is None:
+if android_module is None:
     try:
         import pygame.mixer as pygame_mixer
 
@@ -39,9 +48,15 @@
 
 
 def init() -> None:
-    """Must be called before pygame.init() to enable low latency sound."""
-    # reduce sound latency.  the pygame defaults were ok for 2001,
-    # but these values are more acceptable for faster computers
+    """
+    Initializes the sound system to enable low latency sound.
+
+    This function must be called before pygame.init() to take effect.
+    It adjusts the sound settings to reduce latency, making it more
+    suitable for modern computers. The default Pygame settings were
+    optimized for slower computers in 2001, but these updated values
+    provide better performance on faster machines.
+    """
     if _pygame:
         logger.debug("pre-init pygame mixer")
         try:
@@ -50,54 +65,26 @@ def init() -> None:
             logger.error(f"Failed to initialize Pygame mixer: {e}")
 
 
-def get_user_storage_dir() -> str:
+def get_user_storage_dir() -> Path:
     """
     Returns the user storage directory.
     Mutable storage for things like config, save games, mods, cache.
     """
     if is_android():
-        paths = _get_android_storage_paths()
-        if paths:
-            return paths[0]
-    return str(Path.home() / ".tuxemon")
+        if android_module is not None:
+            return Path(
+                android_module.context.getExternalFilesDir(None).getPath()
+            )
+    logger.error("Android module is not available or not running on Android")
+    return Path.home() / ".tuxemon"
 
 
 def is_android() -> bool:
     """Checks if the platform is Android."""
-    try:
-        import android
-
-        return True
-    except ImportError:
-        return False
-
+    return android_module is not None
 
-def _get_android_storage_paths() -> list[str]:
-    """Helper function to get Android storage paths."""
-    paths: list[str] = []
 
-    if is_android():
-        paths.extend(_get_android_storage_paths())
-    else:
-        paths.append("/usr/share/tuxemon/")
-        paths.append("/usr/local/share/tuxemon/")
-        try:
-            xdg_data_dirs = os.environ.get("XDG_DATA_DIRS", "")
-            if xdg_data_dirs:
-                for data_dir in xdg_data_dirs.split(":"):
-                    path = Path(data_dir) / "tuxemon"
-                    if path.exists():
-                        paths.append(str(path))
-                    else:
-                        logger.debug(f"Checking XDG data directory: {path}")
-
-        except Exception as e:
-            logger.error(f"Error handling XDG_DATA_DIRS: {e}")
-
-    return paths
-
-
-def get_system_storage_dirs() -> Sequence[str]:
+def get_system_storage_dirs() -> Sequence[Path]:
     """
     Returns a sequence of system storage directories.
     Should be immutable storage for things like system installed code/mods.
@@ -105,24 +92,29 @@ def get_system_storage_dirs() -> Sequence[str]:
     The primary user of this storage are packages for operating systems
     that will install the mods into a folder like /usr/share/tuxemon.
     """
-    paths: list[str] = []
+    paths: list[Path] = []
+
+    if not is_android():
+        paths.extend(
+            [
+                Path("/usr/share/tuxemon/"),
+                Path("/usr/local/share/tuxemon/"),
+            ]
+        )
 
-    if is_android():
-        paths.extend(_get_android_storage_paths())
-    else:
-        paths.append("/usr/share/tuxemon/")
-        paths.append("/usr/local/share/tuxemon/")
         try:
             xdg_data_dirs = os.environ.get("XDG_DATA_DIRS", "")
             if xdg_data_dirs:
                 for data_dir in xdg_data_dirs.split(":"):
                     path = Path(data_dir) / "tuxemon"
                     if path.exists():
-                        paths.append(str(path))
+                        paths.append(path)
                     else:
                         logger.debug(f"Checking XDG data directory: {path}")
 
         except Exception as e:
             logger.error(f"Error handling XDG_DATA_DIRS: {e}")
 
+    # You need to implement the android logic here
+    # For now, I'm just returning the paths
     return paths
diff --git a/tuxemon/plugin.py b/tuxemon/plugin.py
index 46b9a579f0..ca41b8e1ba 100644
--- a/tuxemon/plugin.py
+++ b/tuxemon/plugin.py
@@ -6,10 +6,10 @@
 import importlib.util
 import inspect
 import logging
-import os
 import sys
 from abc import ABC, abstractmethod
 from collections.abc import Iterable, Mapping, Sequence
+from pathlib import Path
 from types import ModuleType
 from typing import (
     ClassVar,
@@ -79,16 +79,17 @@ def discover_plugins(self) -> list[str]:
         """Discovers plugin modules from the file system."""
         modules = []
         for folder in self.folders:
-            if not os.path.exists(folder):
-                logger.warning(f"Folder {folder} does not exist")
+            folder_path = Path(folder)
+            if not folder_path.exists():
+                logger.warning(f"Folder {folder_path} does not exist")
                 continue
 
-            module_path = self._get_module_path(folder)
+            module_path = self._get_module_path(folder_path)
             modules.extend(
                 [
-                    f"{module_path}.{os.path.splitext(f)[0]}"
-                    for f in os.listdir(folder)
-                    if f.endswith(self.file_extensions)
+                    f"{module_path}.{file.stem}"
+                    for file in folder_path.iterdir()
+                    if file.suffix in self.file_extensions and file.is_file()
                 ]
             )
         return modules
@@ -97,15 +98,18 @@ def set_folders(self, folders: list[str]) -> None:
         """Sets the folders to search for plugins."""
         self.folders = folders
 
-    def _get_module_path(self, folder: str) -> str:
+    def _get_module_path(self, folder: Path) -> str:
         """Converts a folder path to a module path using pathlib."""
-        folder = folder.replace("\\", "/")
-        match = folder[folder.rfind(self.folder_name) :]
-        if not match:
+        folder = folder.resolve()
+
+        if self.folder_name not in folder.parts:
             raise RuntimeError(
                 f"Unable to determine plugin module path for: {folder}"
             )
-        return match.replace("/", ".")
+
+        match_index = folder.parts.index(self.folder_name)
+        module_parts = folder.parts[match_index:]
+        return ".".join(module_parts)
 
 
 class PluginLoader:
diff --git a/tuxemon/prepare.py b/tuxemon/prepare.py
index a46581d73a..73eda3270c 100644
--- a/tuxemon/prepare.py
+++ b/tuxemon/prepare.py
@@ -7,8 +7,8 @@
 from __future__ import annotations
 
 import logging
-import os.path
 import re
+from pathlib import Path
 from typing import TYPE_CHECKING
 
 import yaml
@@ -36,27 +36,24 @@
 ]
 
 # Create game dir if missing
-if not os.path.isdir(paths.USER_STORAGE_DIR):
-    os.makedirs(paths.USER_STORAGE_DIR)
+paths.USER_STORAGE_DIR.mkdir(parents=True, exist_ok=True)
 
 # Create game data dir if missing
-if not os.path.isdir(paths.USER_GAME_DATA_DIR):
-    os.makedirs(paths.USER_GAME_DATA_DIR)
+paths.USER_GAME_DATA_DIR.mkdir(parents=True, exist_ok=True)
 
 # Create game savegame dir if missing
-if not os.path.isdir(paths.USER_GAME_SAVE_DIR):
-    os.makedirs(paths.USER_GAME_SAVE_DIR)
+paths.USER_GAME_SAVE_DIR.mkdir(parents=True, exist_ok=True)
 
 # Generate default config
 config.generate_default_config()
 
 # Read "tuxemon.yaml" config from disk, update and write back
-CONFIG = config.TuxemonConfig(paths.USER_CONFIG_PATH)
+CONFIG = config.TuxemonConfig(paths.USER_CONFIG_PATH.as_posix())
 
 # Starting map
 STARTING_MAP = "start_"
 
-with open(paths.USER_CONFIG_PATH, "w") as fp:
+with paths.USER_CONFIG_PATH.open("w") as fp:
     yaml.dump(CONFIG.config, fp, default_flow_style=False, indent=4)
 
 # Set up the screen size and caption
@@ -330,7 +327,7 @@
     SCALE = 1
 
 # Reference user save dir
-SAVE_PATH = os.path.join(paths.USER_GAME_SAVE_DIR, "slot")
+SAVE_PATH = paths.USER_GAME_SAVE_DIR / "slot"
 SAVE_METHOD = "JSON"
 # SAVE_METHOD = "CBOR"
 
@@ -361,10 +358,10 @@ def pygame_init() -> None:
     pg.init()
     pg.display.set_caption(CONFIG.window_caption)
 
-    from tuxemon import platform
+    from tuxemon.platform import is_android
 
     fullscreen = pg.FULLSCREEN if CONFIG.fullscreen else 0
-    if platform.android:
+    if is_android():
         fullscreen = pg.FULLSCREEN
     flags = pg.HWSURFACE | pg.DOUBLEBUF | fullscreen
 
@@ -406,26 +403,26 @@ def init() -> None:
 # eventually, this should be configured at game launch, or in a config file instead
 # of looking all over creation for the required files.
 def fetch(*args: str) -> str:
-    relative_path = os.path.join(*args)
+    relative_path = Path(*args)
 
     for mod_name in CONFIG.mods:
         # when assets are in folder with the source
-        path = os.path.join(paths.mods_folder, mod_name, relative_path)
+        path = paths.mods_folder / mod_name / relative_path
         logger.debug(f"searching asset: {path}")
-        if os.path.exists(path):
-            return path
+        if path.exists():
+            return path.as_posix()
 
-        # when assets are in a system path (like for os packages and android)
+        # when assets are in a system path (like for OS packages and Android)
         for root_path in paths.system_installed_folders:
-            path = os.path.join(root_path, "mods", mod_name, relative_path)
+            path = root_path / "mods" / mod_name / relative_path
             logger.debug(f"searching asset: {path}")
-            if os.path.exists(path):
-                return path
+            if path.exists():
+                return path.as_posix()
 
-        # mods folder is in same folder as the launch script
-        path = os.path.join(paths.BASEDIR, "mods", mod_name, relative_path)
+        # mods folder is in the same folder as the launch script
+        path = paths.BASEDIR / "mods" / mod_name / relative_path
         logger.debug(f"searching asset: {path}")
-        if os.path.exists(path):
-            return path
+        if path.exists():
+            return path.as_posix()
 
-    raise OSError(f"cannot load file {relative_path}")
+    raise OSError(f"Cannot load file {relative_path}")
diff --git a/tuxemon/rumble/__init__.py b/tuxemon/rumble/__init__.py
index 1ee7d0b4e1..44f984ec43 100644
--- a/tuxemon/rumble/__init__.py
+++ b/tuxemon/rumble/__init__.py
@@ -2,7 +2,6 @@
 # Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
 import logging
 import os
-from ctypes import CDLL, LibraryLoader
 from typing import Optional, Union
 
 from tuxemon.rumble.tools import Rumble, find_library
diff --git a/tuxemon/save.py b/tuxemon/save.py
index 2b578e1c9f..d404fdc643 100644
--- a/tuxemon/save.py
+++ b/tuxemon/save.py
@@ -94,7 +94,7 @@ def _get_save_extension() -> str:
 
 def get_save_path(slot: int) -> str:
     extension = _get_save_extension()
-    return f"{prepare.SAVE_PATH}{slot}.{extension}"
+    return f"{prepare.SAVE_PATH.as_posix()}{slot}.{extension}"
 
 
 def json_action(
diff --git a/tuxemon/state.py b/tuxemon/state.py
index 8238b9372b..737df8f1db 100644
--- a/tuxemon/state.py
+++ b/tuxemon/state.py
@@ -359,7 +359,7 @@ def auto_state_discovery(self) -> None:
 
         TODO: this functionality duplicates the plugin code.
         """
-        state_folder = Path(paths.LIBDIR) / Path(*self.package.split(".")[1:])
+        state_folder = paths.LIBDIR / Path(*self.package.split(".")[1:])
         exclude_endings = {".py", ".pyc", ".pyo"}
         exclude_names = {"__pycache__"}
 
diff --git a/tuxemon/states/persistance/save_menu.py b/tuxemon/states/persistance/save_menu.py
index f39dfb48f0..3351cb5a9b 100644
--- a/tuxemon/states/persistance/save_menu.py
+++ b/tuxemon/states/persistance/save_menu.py
@@ -3,8 +3,8 @@
 from __future__ import annotations
 
 import logging
-import os
 from base64 import b64decode
+from pathlib import Path
 from typing import TYPE_CHECKING, Optional
 
 from pygame import SRCALPHA
@@ -44,8 +44,9 @@ def __init__(self, selected_index: Optional[int] = None) -> None:
     def create_menu_item(
         self, slot_rect: Rect, slot_index: int, selectable: bool = True
     ) -> MenuItem[None]:
-        save_path = get_save_path(slot_index)
-        if os.path.exists(save_path):
+        save_path = Path(get_save_path(slot_index))
+
+        if save_path.exists():
             image = self.render_slot(slot_rect, slot_index)
             return MenuItem(image, T.translate("menu_save"), None, None, True)
         else:
@@ -197,10 +198,11 @@ def delete_save_slot(slot_num: int) -> bool:
     Returns:
         bool: True if the save file was deleted successfully, False otherwise.
     """
-    save_path = get_save_path(slot_num)
-    if os.path.exists(save_path):
+    save_path = Path(get_save_path(slot_num))
+
+    if save_path.exists():
         try:
-            os.remove(save_path)
+            save_path.unlink()
             logger.info(
                 f"Save slot {slot_num} deleted successfully at path {save_path}."
             )
diff --git a/tuxemon/states/phone/phone_map.py b/tuxemon/states/phone/phone_map.py
index d5b4cb48d7..60310884db 100644
--- a/tuxemon/states/phone/phone_map.py
+++ b/tuxemon/states/phone/phone_map.py
@@ -5,6 +5,7 @@
 import logging
 from collections.abc import Callable
 from dataclasses import dataclass
+from pathlib import Path
 from typing import TYPE_CHECKING, Any, Optional
 
 import pygame_menu
@@ -30,9 +31,9 @@ class NuPhoneMapConfig:
     map_data: list[tuple[float, float, str]]
 
 
-def load_yaml(filepath: str) -> Any:
+def load_yaml(filepath: Path) -> Any:
     try:
-        with open(filepath) as file:
+        with filepath.open() as file:
             return yaml.safe_load(file)
     except FileNotFoundError:
         logger.error(f"Config file not found: {filepath}")
@@ -47,7 +48,7 @@ class Loader:
 
     @classmethod
     def get_config_nuphone_map(cls, filename: str) -> NuPhoneMapConfig:
-        yaml_path = f"{paths.mods_folder}/{filename}"
+        yaml_path = paths.mods_folder / filename
         if not cls._config_nuphone_map:
             raw_data = load_yaml(yaml_path)
             if not isinstance(raw_data, dict):
diff --git a/tuxemon/status/status.py b/tuxemon/status/status.py
index 0614dee65d..3a3a65078c 100644
--- a/tuxemon/status/status.py
+++ b/tuxemon/status/status.py
@@ -76,11 +76,11 @@ def __init__(self, save_data: Optional[Mapping[str, Any]] = None) -> None:
 
         if Status.effect_manager is None:
             Status.effect_manager = EffectManager(
-                StatusEffect, paths.CORE_EFFECT_PATH
+                StatusEffect, paths.CORE_EFFECT_PATH.as_posix()
             )
         if Status.condition_manager is None:
             Status.condition_manager = ConditionManager(
-                CoreCondition, paths.CORE_CONDITION_PATH
+                CoreCondition, paths.CORE_CONDITION_PATH.as_posix()
             )
 
         self.effects: Sequence[PluginObject] = []
diff --git a/tuxemon/technique/technique.py b/tuxemon/technique/technique.py
index 9c9992d7e5..31573abd3f 100644
--- a/tuxemon/technique/technique.py
+++ b/tuxemon/technique/technique.py
@@ -72,11 +72,11 @@ def __init__(self, save_data: Optional[Mapping[str, Any]] = None) -> None:
 
         if Technique.effect_manager is None:
             Technique.effect_manager = EffectManager(
-                TechEffect, paths.CORE_EFFECT_PATH
+                TechEffect, paths.CORE_EFFECT_PATH.as_posix()
             )
         if Technique.condition_manager is None:
             Technique.condition_manager = ConditionManager(
-                CoreCondition, paths.CORE_CONDITION_PATH
+                CoreCondition, paths.CORE_CONDITION_PATH.as_posix()
             )
 
         self.effects: Sequence[PluginObject] = []


Test patch:
diff --git a/scripts/test_actions.py b/scripts/test_actions.py
index 3e94e7158c..ffaab170d3 100644
--- a/scripts/test_actions.py
+++ b/scripts/test_actions.py
@@ -1,7 +1,6 @@
 """
 Script to validate map actions
 """
-from pathlib import Path
 from unittest.mock import MagicMock
 
 from tuxemon.constants import paths
@@ -21,9 +20,7 @@
 loader.image_loader = MagicMock()
 
 for mod_name in CONFIG.mods:
-    for path in (
-        Path().joinpath(paths.mods_folder, mod_name, "maps").glob("*.tmx")
-    ):
+    for path in (paths.mods_folder / mod_name / "maps").glob("*.tmx"):
         txmn_map = loader.load(str(path))
         for event in txmn_map.events:
             for act in event.acts:
diff --git a/tests/tuxemon/test_core_manager.py b/tests/tuxemon/test_core_manager.py
index 9a6a472873..2f267abf7f 100644
--- a/tests/tuxemon/test_core_manager.py
+++ b/tests/tuxemon/test_core_manager.py
@@ -2,6 +2,7 @@
 # Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
 import tempfile
 import unittest
+from pathlib import Path
 from unittest.mock import MagicMock, patch
 
 from tuxemon.core.core_manager import (
@@ -15,8 +16,8 @@
 
 class TestCoreManager(unittest.TestCase):
     def setUp(self):
-        self.temp_dir = tempfile.TemporaryDirectory()
-        self.path = self.temp_dir.name
+        self.temp_dir = Path(tempfile.mkdtemp())
+        self.path = self.temp_dir / "tuxemon"
         self.category = "category"
         self.plugin_interface = MagicMock(spec=PluginObject)
         self.manager = CoreManager(
@@ -24,7 +25,10 @@ def setUp(self):
         )
 
     def tearDown(self):
-        self.temp_dir.cleanup()
+        for item in self.temp_dir.iterdir():
+            if item.is_dir():
+                item.rmdir()
+        self.temp_dir.rmdir()
 
     @patch("tuxemon.plugin.load_plugins")
     def test_load_plugins(self, mock_load_plugins):
@@ -77,15 +81,18 @@ def test_parse_object_condition(self):
 class TestEffectManager(unittest.TestCase):
     def setUp(self):
         self.effect_class = MagicMock(spec=PluginObject)
-        self.temp_dir = tempfile.TemporaryDirectory()
-        self.path = self.temp_dir.name
+        self.temp_dir = Path(tempfile.mkdtemp())
+        self.path = self.temp_dir / "tuxemon"
         self.category = "effects"
         self.manager = EffectManager(
             self.effect_class, self.path, self.category
         )
 
     def tearDown(self):
-        self.temp_dir.cleanup()
+        for item in self.temp_dir.iterdir():
+            if item.is_dir():
+                item.rmdir()
+        self.temp_dir.rmdir()
 
     def test_parse_effects(self):
         effect_instance = MagicMock(spec=PluginObject)
@@ -100,15 +107,18 @@ def test_parse_effects(self):
 class TestConditionManager(unittest.TestCase):
     def setUp(self):
         self.condition_class = MagicMock(spec=PluginObject)
-        self.temp_dir = tempfile.TemporaryDirectory()
-        self.path = self.temp_dir.name
+        self.temp_dir = Path(tempfile.mkdtemp())
+        self.path = self.temp_dir / "tuxemon"
         self.category = "conditions"
         self.manager = ConditionManager(
             self.condition_class, self.path, self.category
         )
 
     def tearDown(self):
-        self.temp_dir.cleanup()
+        for item in self.temp_dir.iterdir():
+            if item.is_dir():
+                item.rmdir()
+        self.temp_dir.rmdir()
 
     def test_parse_conditions(self):
         condition_instance = MagicMock(spec=PluginObject)
diff --git a/tests/tuxemon/test_event_loader_map.py b/tests/tuxemon/test_event_loader_map.py
index 37869e379d..89ea3910b0 100644
--- a/tests/tuxemon/test_event_loader_map.py
+++ b/tests/tuxemon/test_event_loader_map.py
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: GPL-3.0
 # Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
-import os
 import unittest
+from pathlib import Path
 
 from tuxemon.map_loader import YAMLEventLoader, parse_yaml
 
@@ -10,8 +10,8 @@ class TestYAMLEventLoader(unittest.TestCase):
 
     def setUp(self):
         self.loader = YAMLEventLoader()
-        self.valid_yaml_path = os.path.join(
-            "tests/tuxemon", "test_event_loader_map.yaml"
+        self.valid_yaml_path = (
+            Path("tests/tuxemon") / "test_event_loader_map.yaml"
         )
 
     def test_parse_yaml_success(self):
diff --git a/tests/tuxemon/test_folder_maps_tmx.py b/tests/tuxemon/test_folder_maps_tmx.py
index 2ef1df6fdd..71a34504f9 100644
--- a/tests/tuxemon/test_folder_maps_tmx.py
+++ b/tests/tuxemon/test_folder_maps_tmx.py
@@ -1,10 +1,10 @@
 # SPDX-License-Identifier: GPL-3.0
 # Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
-import os
 import re
 import unittest
 import xml.etree.ElementTree as ET
 from collections.abc import Generator
+from pathlib import Path
 from typing import Any
 
 from tuxemon import prepare
@@ -26,9 +26,10 @@ def expand_expected_scenarios() -> None:
 
 
 def get_tmx_files(folder_path: str) -> Generator[str, Any, None]:
-    for file in os.listdir(folder_path):
-        if file.endswith(".tmx"):
-            yield os.path.join(folder_path, file)
+    folder = Path(folder_path)
+    for file in folder.iterdir():
+        if file.suffix == ".tmx" and file.is_file():
+            yield file.as_posix()
 
 
 def load_tmx_files(folder_path: str) -> dict[str, ET.Element]:
@@ -40,7 +41,7 @@ def load_tmx_files(folder_path: str) -> dict[str, ET.Element]:
 
 
 def to_basename(filepath: str) -> str:
-    return os.path.basename(filepath)
+    return Path(filepath).name
 
 
 def _is_object_property(
@@ -245,14 +246,12 @@ def test_tileset_source(self) -> None:
                 and "source" in tileset_element.attrib
             ):
                 tileset_source = tileset_element.attrib["source"]
-                base_path = prepare.fetch("gfx")
-                merged_path = os.path.realpath(
-                    os.path.join(base_path, tileset_source)
-                )
+                base_path = Path(prepare.fetch("gfx"))
+                merged_path = (base_path / tileset_source).resolve()
                 msg = (
                     f"Source '{merged_path}' doesn't exist {to_basename(path)}"
                 )
-                self.assertTrue(os.path.isfile(merged_path), msg)
+                self.assertTrue(merged_path.is_file(), msg)
 
     def test_layer_number(self) -> None:
         missing_layers = []
diff --git a/tests/tuxemon/test_folder_maps_yaml.py b/tests/tuxemon/test_folder_maps_yaml.py
index 24f64ecca6..c9cab4c720 100644
--- a/tests/tuxemon/test_folder_maps_yaml.py
+++ b/tests/tuxemon/test_folder_maps_yaml.py
@@ -1,8 +1,8 @@
 # SPDX-License-Identifier: GPL-3.0
 # Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
-import os
 import unittest
 from collections.abc import Generator
+from pathlib import Path
 from typing import Any
 
 import yaml
@@ -33,9 +33,10 @@ def expand_expected_scenarios() -> None:
 
 
 def get_yaml_files(folder_path: str) -> Generator[str, Any, None]:
-    for file in os.listdir(folder_path):
-        if file.endswith(".yaml"):
-            yield os.path.join(folder_path, file)
+    folder = Path(folder_path)
+    for file in folder.iterdir():
+        if file.suffix == ".yaml" and file.is_file():
+            yield file.as_posix()
 
 
 def load_yaml_files(folder_path: str) -> dict[str, dict]:
@@ -62,7 +63,7 @@ def test_yaml_event_name_length(self):
                 self.assertLessEqual(
                     len(event_name),
                     MAX_LENGTH_NAME,
-                    f"Event name {event_name} exceeds {MAX_LENGTH_NAME} characters: {os.path.basename(path)}",
+                    f"Event name {event_name} exceeds {MAX_LENGTH_NAME} characters: {Path(path).name}",
                 )
 
     def test_yaml_event_labels(self):
@@ -72,7 +73,7 @@ def test_yaml_event_labels(self):
                     self.assertIn(
                         name,
                         YAML_ATTR,
-                        f"Event name {name} isn't among {YAML_ATTR}: {os.path.basename(path)}",
+                        f"Event name {name} isn't among {YAML_ATTR}: {Path(path).name}",
                     )
 
     def test_yaml_event_type(self):
@@ -81,7 +82,7 @@ def test_yaml_event_type(self):
                 self.assertIn(
                     event_data["type"],
                     YAML_TYPES,
-                    f"Event type {event_data['type']} isn't among {YAML_TYPES}: {os.path.basename(path)}",
+                    f"Event type {event_data['type']} isn't among {YAML_TYPES}: {Path(path).name}",
                 )
 
     def test_yaml_event_x_coordinate(self):
@@ -91,7 +92,7 @@ def test_yaml_event_x_coordinate(self):
                     self.assertIsInstance(
                         event_data["x"],
                         int,
-                        f"Value of 'x' should be an integer: {os.path.basename(path)}",
+                        f"Value of 'x' should be an integer: {Path(path).name}",
                     )
 
     def test_yaml_event_y_coordinate(self):
@@ -101,7 +102,7 @@ def test_yaml_event_y_coordinate(self):
                     self.assertIsInstance(
                         event_data["y"],
                         int,
-                        f"Value of 'y' should be an integer: {os.path.basename(path)}",
+                        f"Value of 'y' should be an integer: {Path(path).name}",
                     )
 
     def test_yaml_event_width(self):
@@ -111,7 +112,7 @@ def test_yaml_event_width(self):
                     self.assertIsInstance(
                         event_data["width"],
                         int,
-                        f"Value of 'width' should be an integer: {os.path.basename(path)}",
+                        f"Value of 'width' should be an integer: {Path(path).name}",
                     )
 
     def test_yaml_event_height(self):
@@ -121,7 +122,7 @@ def test_yaml_event_height(self):
                     self.assertIsInstance(
                         event_data["height"],
                         int,
-                        f"Value of 'height' should be an integer: {os.path.basename(path)}",
+                        f"Value of 'height' should be an integer: {Path(path).name}",
                     )
 
     def test_actions_structure(self):
@@ -132,7 +133,7 @@ def test_actions_structure(self):
                         self.assertIsInstance(
                             action,
                             str,
-                            f"Action in event should be a string: {os.path.basename(path)}",
+                            f"Action in event should be a string: {Path(path).name}",
                         )
 
     def test_actions_teleport(self):
@@ -146,7 +147,7 @@ def test_actions_teleport(self):
                                 prepare.fetch(FOLDER, params[0])
                             except OSError:
                                 self.fail(
-                                    f"Map '{params[0]}' does not exist in object {action} at {os.path.basename(path)}"
+                                    f"Map '{params[0]}' does not exist in object {action} at {Path(path).name}"
                                 )
 
     def test_conditions_structure(self):
@@ -157,7 +158,7 @@ def test_conditions_structure(self):
                         self.assertIsInstance(
                             condition,
                             str,
-                            f"Condition in event should be a string: {os.path.basename(path)}",
+                            f"Condition in event should be a string: {Path(path).name}",
                         )
 
     def test_conditions_operator(self):
@@ -167,5 +168,5 @@ def test_conditions_operator(self):
                     for condition in event_data["conditions"]:
                         self.assertTrue(
                             condition.lower().startswith(("is ", "not ")),
-                            f"Condition '{condition}' should start with 'is' or 'not': {os.path.basename(path)}",
+                            f"Condition '{condition}' should start with 'is' or 'not': {Path(path).name}",
                         )
diff --git a/tests/tuxemon/test_fusion.py b/tests/tuxemon/test_fusion.py
index d8325c5c81..9bc2be03e5 100644
--- a/tests/tuxemon/test_fusion.py
+++ b/tests/tuxemon/test_fusion.py
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: GPL-3.0
 # Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
-import os
 import unittest
+from pathlib import Path
 
 from PIL import Image
 
@@ -23,8 +23,9 @@ def setUp(self):
         self.body.tertiary_colors = [(0, 255, 0)]  # Green
 
     def tearDown(self):
-        if os.path.exists(self.image_path):
-            os.remove(self.image_path)
+        image_path = Path(self.image_path)
+        if image_path.exists():
+            image_path.unlink()
 
     def test_replace_color(self):
         result_image = replace_color(self.image, (255, 0, 0), (0, 0, 255))
diff --git a/tests/tuxemon/test_jsons_monster.py b/tests/tuxemon/test_jsons_monster.py
index 784e6ab433..bbff6e4554 100644
--- a/tests/tuxemon/test_jsons_monster.py
+++ b/tests/tuxemon/test_jsons_monster.py
@@ -1,8 +1,8 @@
 # SPDX-License-Identifier: GPL-3.0
 # Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
 import json
-import os
 import unittest
+from pathlib import Path
 from typing import Any
 
 from tuxemon import prepare
@@ -13,11 +13,10 @@
 
 def process_json_data(directory: str) -> list[dict[str, Any]]:
     data_list = []
-    directory = f"{prepare.fetch('db')}/{directory}/"
-    for filename in os.listdir(directory):
-        if filename.endswith(".json"):
-            filepath = os.path.join(directory, filename)
-            with open(filepath, "r") as f:
+    directory_path = Path(prepare.fetch("db")) / directory
+    for file in directory_path.iterdir():
+        if file.suffix == ".json" and file.is_file():
+            with file.open("r") as f:
                 data_list.append(json.load(f))
     return data_list
 
diff --git a/tests/tuxemon/test_jsons_technique.py b/tests/tuxemon/test_jsons_technique.py
index 636391be7e..148148ae09 100644
--- a/tests/tuxemon/test_jsons_technique.py
+++ b/tests/tuxemon/test_jsons_technique.py
@@ -1,8 +1,8 @@
 # SPDX-License-Identifier: GPL-3.0
 # Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
 import json
-import os
 import unittest
+from pathlib import Path
 from typing import Any
 
 from tuxemon import prepare
@@ -17,11 +17,10 @@
 
 def process_json_data(directory: str) -> list[dict[str, Any]]:
     data_list = []
-    directory = f"{prepare.fetch('db')}/{directory}/"
-    for filename in os.listdir(directory):
-        if filename.endswith(".json"):
-            filepath = os.path.join(directory, filename)
-            with open(filepath, "r") as f:
+    directory_path = Path(prepare.fetch("db")) / directory
+    for file in directory_path.iterdir():
+        if file.suffix == ".json" and file.is_file():
+            with file.open("r") as f:
                 data_list.append(json.load(f))
     return data_list
 
diff --git a/tests/tuxemon/test_paths.py b/tests/tuxemon/test_paths.py
new file mode 100644
index 0000000000..229db5c469
--- /dev/null
+++ b/tests/tuxemon/test_paths.py
@@ -0,0 +1,56 @@
+# SPDX-License-Identifier: GPL-3.0
+# Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
+import unittest
+from pathlib import Path
+
+from tuxemon.constants.paths import (
+    ACTIONS_PATH,
+    BASEDIR,
+    CACHE_DIR,
+    CONDITIONS_PATH,
+    CORE_CONDITION_PATH,
+    CORE_EFFECT_PATH,
+    L18N_MO_FILES,
+    LIBDIR,
+    USER_CONFIG_PATH,
+    USER_GAME_DATA_DIR,
+    USER_GAME_SAVE_DIR,
+    mods_folder,
+)
+
+
+class TestCorePaths(unittest.TestCase):
+
+    def test_libdir(self):
+        self.assertTrue(LIBDIR.exists())
+
+    def test_basedir(self):
+        self.assertTrue(BASEDIR.exists())
+
+    def test_mods_folder(self):
+        folder = LIBDIR.parent / "mods"
+        self.assertTrue(folder.exists())
+        self.assertEqual(folder, mods_folder)
+
+    def test_event_paths(self):
+        self.assertTrue(CONDITIONS_PATH.exists())
+        self.assertTrue(ACTIONS_PATH.exists())
+
+    def test_core_paths(self):
+        self.assertTrue(CORE_EFFECT_PATH.exists())
+        self.assertTrue(CORE_CONDITION_PATH.exists())
+
+    def test_user_config_path(self):
+        self.assertTrue(USER_CONFIG_PATH.parent.exists())
+
+    def test_user_game_data_dir(self):
+        self.assertTrue(USER_GAME_DATA_DIR.exists())
+
+    def test_user_game_save_dir(self):
+        self.assertTrue(USER_GAME_SAVE_DIR.exists())
+
+    def test_cache_dir(self):
+        self.assertTrue(CACHE_DIR.exists())
+
+    def test_l18n_mo_files(self):
+        self.assertTrue(L18N_MO_FILES.exists())
diff --git a/tests/tuxemon/test_sprite_renderer.py b/tests/tuxemon/test_sprite_renderer.py
index 288ab8a9e2..192660a450 100644
--- a/tests/tuxemon/test_sprite_renderer.py
+++ b/tests/tuxemon/test_sprite_renderer.py
@@ -1,6 +1,6 @@
 # SPDX-License-Identifier: GPL-3.0
 # Copyright (c) 2014-2025 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
-import os
+from pathlib import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
@@ -116,26 +116,26 @@ def adventurer_loading_paths(self, mock_load_and_scale):
         mock_load_and_scale.return_value = Surface((1, 1))
         self.sprite_controller.load_sprites(self.npc.template)
         expected_paths = [
-            os.path.join("sprites", "adventurer_front.png"),
-            os.path.join("sprites", "adventurer_back.png"),
-            os.path.join("sprites", "adventurer_left.png"),
-            os.path.join("sprites", "adventurer_right.png"),
-            os.path.join("sprites", "adventurer_front_walk.000.png"),
-            os.path.join("sprites", "adventurer_front.png"),
-            os.path.join("sprites", "adventurer_front_walk.001.png"),
-            os.path.join("sprites", "adventurer_front.png"),
-            os.path.join("sprites", "adventurer_back_walk.000.png"),
-            os.path.join("sprites", "adventurer_back.png"),
-            os.path.join("sprites", "adventurer_back_walk.001.png"),
-            os.path.join("sprites", "adventurer_back.png"),
-            os.path.join("sprites", "adventurer_left_walk.000.png"),
-            os.path.join("sprites", "adventurer_left.png"),
-            os.path.join("sprites", "adventurer_left_walk.001.png"),
-            os.path.join("sprites", "adventurer_left.png"),
-            os.path.join("sprites", "adventurer_right_walk.000.png"),
-            os.path.join("sprites", "adventurer_right.png"),
-            os.path.join("sprites", "adventurer_right_walk.001.png"),
-            os.path.join("sprites", "adventurer_right.png"),
+            Path("sprites") / "adventurer_front.png",
+            Path("sprites") / "adventurer_back.png",
+            Path("sprites") / "adventurer_left.png",
+            Path("sprites") / "adventurer_right.png",
+            Path("sprites") / "adventurer_front_walk.000.png",
+            Path("sprites") / "adventurer_front.png",
+            Path("sprites") / "adventurer_front_walk.001.png",
+            Path("sprites") / "adventurer_front.png",
+            Path("sprites") / "adventurer_back_walk.000.png",
+            Path("sprites") / "adventurer_back.png",
+            Path("sprites") / "adventurer_back_walk.001.png",
+            Path("sprites") / "adventurer_back.png",
+            Path("sprites") / "adventurer_left_walk.000.png",
+            Path("sprites") / "adventurer_left.png",
+            Path("sprites") / "adventurer_left_walk.001.png",
+            Path("sprites") / "adventurer_left.png",
+            Path("sprites") / "adventurer_right_walk.000.png",
+            Path("sprites") / "adventurer_right.png",
+            Path("sprites") / "adventurer_right_walk.001.png",
+            Path("sprites") / "adventurer_right.png",
         ]
         actual_paths = [
             call[0][0] for call in mock_load_and_scale.call_args_list