diff --git a/README.md b/README.md
index 608af1313c2f..25d7611ccf4f 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,7 @@ Currently, the following games are supported:
* Celeste (Open World)
* Choo-Choo Charles
* APQuest
+* BK Simulator
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS
index e4ef3fe73e28..34c8e0565fcd 100644
--- a/docs/CODEOWNERS
+++ b/docs/CODEOWNERS
@@ -25,6 +25,9 @@
# Aquaria
/worlds/aquaria/ @tioui
+# BK Simulator
+/worlds/bksim @EmilyV99
+
# Blasphemous
/worlds/blasphemous/ @TRPG0
diff --git a/worlds/bksim/.gitignore b/worlds/bksim/.gitignore
new file mode 100644
index 000000000000..ed8ebf583f77
--- /dev/null
+++ b/worlds/bksim/.gitignore
@@ -0,0 +1 @@
+__pycache__
\ No newline at end of file
diff --git a/worlds/bksim/__init__.py b/worlds/bksim/__init__.py
new file mode 100644
index 000000000000..c9b71f86a7b4
--- /dev/null
+++ b/worlds/bksim/__init__.py
@@ -0,0 +1 @@
+from .world import BKSimWorld as BKSimWorld
diff --git a/worlds/bksim/archipelago.json b/worlds/bksim/archipelago.json
new file mode 100644
index 000000000000..2b9d9038bd87
--- /dev/null
+++ b/worlds/bksim/archipelago.json
@@ -0,0 +1,5 @@
+{
+ "game": "BKSimulator",
+ "authors": ["EmilyV99"],
+ "world_version": "0.1.2"
+}
diff --git a/worlds/bksim/common.py b/worlds/bksim/common.py
new file mode 100644
index 000000000000..a00c5a41fad1
--- /dev/null
+++ b/worlds/bksim/common.py
@@ -0,0 +1,17 @@
+from enum import StrEnum
+
+game_name = 'BKSimulator'
+
+
+class RID(StrEnum):
+ HOME = 'Home'
+ SUNNY = 'Sunny'
+ RAINY = 'Rainy'
+ SNOWY = 'Snowy'
+
+
+class ITEM(StrEnum):
+ SHOES = 'Better Shoes'
+ BOOTS = 'Snow Boots'
+ NEWLOC = 'New Location'
+ TOY = 'Plastic Toy'
diff --git a/worlds/bksim/docs/en_BKSimulator.md b/worlds/bksim/docs/en_BKSimulator.md
new file mode 100644
index 000000000000..46e353187922
--- /dev/null
+++ b/worlds/bksim/docs/en_BKSimulator.md
@@ -0,0 +1,27 @@
+# BK Simulator
+
+## What is BK Simulator?
+You have to walk to BK, in various weather conditions. Each time you reach BK is a check. Collect every check (and return home!) to goal.
+
+
+
+## Settings
+[Options Page](../player-options)
+- You choose how many locations are in the game. Collecting ALL of them is required as the goal.
+- You can adjust the distance to the nearest BK at the start. This will heavily control the speed / length of the game.
+- You can also adjust the speed gained for each speed upgrade.
+- You can set a percentage of your Shoe/Boot upgrades to be replaced with filler. (Higher values will be fulfilled as much as is possible without breaking logic requirements)
+
+## Items / Logic
+Items:
+- Shoe upgrades allow you to walk faster
+- Snow Boots allow you to walk in snow (and walk faster in snow)
+- Opening a new BK location cuts the distance you need to walk in half
+
+Logic:
+- Sunny weather is the standard, and where you should start
+- Rainy weather is slower to walk in, and expects that you have more upgrades than sunny weather expects
+- Snowy weather requires snow boots to walk in
+- Higher-numbered checks will expect gradually more items to become in-logic.
+- "Sphere 1" includes the first 2 Sunny checks only.
+
diff --git a/worlds/bksim/docs/setup_en.md b/worlds/bksim/docs/setup_en.md
new file mode 100644
index 000000000000..ec4e92bd6b50
--- /dev/null
+++ b/worlds/bksim/docs/setup_en.md
@@ -0,0 +1,32 @@
+# BK Simulator Setup Guide
+
+## Required Software
+
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest), to generate or use text client.
+
+## How to play
+
+- Open the game, connect to your slot.
+- Choose which weather to walk to BK in.
+- Each time you reach BK, you send a check. You do still need to walk home after, before you can start your next trip!
+
+
+
+## Settings
+- You choose how many locations are in the game. Collecting ALL of them is required as the goal.
+- You can adjust the distance to the nearest BK at the start. This will heavily control the speed / length of the game.
+- You can also adjust the speed gained for each speed upgrade.
+- You can set a percentage of your Shoe/Boot upgrades to be replaced with filler. (Higher values will be fulfilled as much as is possible without breaking logic requirements)
+
+## Items / Logic
+Items:
+- Shoe upgrades allow you to walk faster
+- Snow Boots allow you to walk in snow (and walk faster in snow)
+- Opening a new BK location cuts the distance you need to walk in half
+
+Logic:
+- Sunny weather is the standard, and where you should start
+- Rainy weather is slower to walk in, and expects that you have more upgrades than sunny weather expects
+- Snowy weather requires snow boots to walk in
+- Higher-numbered checks will expect gradually more items to become in-logic.
+- "Sphere 1" includes the first 2 Sunny checks only.
diff --git a/worlds/bksim/items.py b/worlds/bksim/items.py
new file mode 100644
index 000000000000..914bfc5cf9ab
--- /dev/null
+++ b/worlds/bksim/items.py
@@ -0,0 +1,87 @@
+from __future__ import annotations
+from math import floor, ceil
+from BaseClasses import Item, ItemClassification
+from .common import *
+import typing
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from .world import BKSimWorld
+
+
+class BKSim_Item(Item):
+ game = game_name
+
+
+class ItemInfo(typing.NamedTuple):
+ name: ITEM
+ flag: ItemClassification
+
+
+item_table = [
+ ItemInfo(ITEM.SHOES, ItemClassification.progression),
+ ItemInfo(ITEM.BOOTS, ItemClassification.progression),
+ ItemInfo(ITEM.NEWLOC, ItemClassification.progression),
+ ItemInfo(ITEM.TOY, ItemClassification.filler),
+]
+item_name_to_id = {str(name): num for num, (name, _) in enumerate(item_table, 1)}
+
+
+def get_item_counts(world: BKSimWorld) -> list[int]:
+ multiworld = world.multiworld
+ player = world.player
+ options = world.options
+
+ newloc_count: int = 0
+ shoe_count: int = 0
+ boot_count: int = 0
+
+ for item in multiworld.precollected_items[player]:
+ if item.name == ITEM.NEWLOC:
+ newloc_count += 1
+ elif item.name == ITEM.SHOES:
+ shoe_count += 1
+ elif item.name == ITEM.BOOTS:
+ boot_count += 1
+
+ extra_filler_rate: float = max(0, min(100, options.extra_filler_rate.value)) / 100.0
+ per_weather_locs: int = int(options.locs_per_weather.value)
+ total_locs: int = per_weather_locs * 3
+ newloc_items: int = max(0, min(2, per_weather_locs) - newloc_count)
+ snow_items: int = per_weather_locs
+ shoe_items: int = per_weather_locs
+ toy_items: int = per_weather_locs - newloc_items
+ if extra_filler_rate > 0.0: # replace shoes/boots with filler at the specified rate, but don't break logic
+ shoe_items = max(0, (int(floor(per_weather_locs - 1) / 2)) + 1 - shoe_count,
+ ceil(shoe_items * (1.0 - extra_filler_rate)))
+ snow_items = max(0, (int(floor(per_weather_locs - 1) / 2)) + 1 - boot_count,
+ ceil(snow_items * (1.0 - extra_filler_rate)))
+ toy_items += (per_weather_locs - shoe_items) + (per_weather_locs - snow_items)
+
+ # ensure proper item count
+ toy_items += total_locs - (newloc_items + snow_items + shoe_items + toy_items)
+
+ return [shoe_items, snow_items, newloc_items, toy_items]
+
+
+def create_items(world: BKSimWorld) -> None:
+ multiworld = world.multiworld
+ player = world.player
+
+ counts: list[int] = get_item_counts(world)
+ itempool = []
+ for q in range(len(item_table)):
+ data: ItemInfo = item_table[q]
+ count: int = counts[q]
+ itempool += [BKSim_Item(str(data.name), data.flag, q + 1, player) for _ in range(count)]
+ multiworld.itempool += itempool
+
+
+def create_item(name: str, player: int) -> BKSim_Item:
+ itemid = item_name_to_id[name]
+ _, flag = item_table[itemid - 1]
+ return BKSim_Item(name, flag, itemid, player)
+
+
+def create_event_item(event: str, player: int) -> BKSim_Item:
+ return BKSim_Item(event, ItemClassification.progression, None, player)
diff --git a/worlds/bksim/locations.py b/worlds/bksim/locations.py
new file mode 100644
index 000000000000..62efbe19330e
--- /dev/null
+++ b/worlds/bksim/locations.py
@@ -0,0 +1,27 @@
+from typing import NamedTuple, Optional
+from BaseClasses import Location, Region
+from .common import *
+
+
+class LocInfo(NamedTuple):
+ name: str
+ region_id: RID
+ index: int
+
+
+class BKSim_Location(Location):
+ game = game_name
+ info: Optional[LocInfo]
+
+ # override constructor to automatically mark event locations as such, and handle LocInfo
+ def __init__(self, player: int, name: str, code: Optional[int], parent: Region, info: Optional[LocInfo] = None) -> None:
+ super(BKSim_Location, self).__init__(player, name, code, parent)
+ self.event = code is None
+ self.info = info
+
+
+location_table = [LocInfo("Sunny %d" % (idx + 1), RID.SUNNY, idx) for idx in range(0,100)]
+location_table += [LocInfo("Rainy %d" % (idx + 1), RID.RAINY, idx) for idx in range(0,100)]
+location_table += [LocInfo("Snowy %d" % (idx + 1), RID.SNOWY, idx) for idx in range(0,100)]
+
+location_name_to_id = {loc.name: num for num,loc in enumerate(location_table,1)}
diff --git a/worlds/bksim/options.py b/worlds/bksim/options.py
new file mode 100644
index 000000000000..a045e7519a55
--- /dev/null
+++ b/worlds/bksim/options.py
@@ -0,0 +1,67 @@
+from dataclasses import dataclass
+from Options import Range, PerGameCommonOptions
+
+
+class LocationsPerWeather(Range):
+ """The total number of locations per type of weather. (There are 3 weather types)"""
+ display_name = "Locations Per Weather"
+ range_start = 1
+ range_end = 100
+ default = 3
+
+
+class StartDistance(Range):
+ """The distance to the closest BK at the start.
+ Each 'New Location' upgrade opens a location halfway between your house and the current closest location."""
+ display_name = "Start Distance"
+ range_start = 50
+ range_end = 5000
+ default = 300
+
+
+class SpeedPerUpgrade(Range):
+ """The amount of speed gained for each shoe upgrade.
+ Snow boots give half this speed in the snow."""
+ display_name = "Speed Per Upgrade"
+ range_start = 1
+ range_end = 100
+ default = 2
+
+
+class ExtraFillerRate(Range):
+ """This percent of shoe/boot upgrades will attempt to be turned into filler.
+ The number of upgrades will not be reduced below the amount logically required."""
+ display_name = "Extra Filler Rate"
+ range_start = 0
+ range_end = 100
+ default = 0
+
+
+@dataclass
+class BKSim_Options(PerGameCommonOptions):
+ locs_per_weather: LocationsPerWeather
+ start_distance: StartDistance
+ speed_per_upgrade: SpeedPerUpgrade
+ extra_filler_rate: ExtraFillerRate
+
+
+options_presets = {
+ "Quick": {
+ "locs_per_weather": 1,
+ "start_distance": 100,
+ "speed_per_upgrade": 5,
+ "extra_filler_rate": 0,
+ },
+ "Marathon": {
+ "locs_per_weather": 1,
+ "start_distance": 5000,
+ "speed_per_upgrade": 1,
+ "extra_filler_rate": 0,
+ },
+ "Masochist": {
+ "locs_per_weather": 100,
+ "start_distance": 5000,
+ "speed_per_upgrade": 1,
+ "extra_filler_rate": 100,
+ },
+}
diff --git a/worlds/bksim/regions.py b/worlds/bksim/regions.py
new file mode 100644
index 000000000000..22b2a2d2b2af
--- /dev/null
+++ b/worlds/bksim/regions.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+from BaseClasses import Region
+from .locations import location_table, BKSim_Location
+from .common import *
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from .world import BKSimWorld
+
+
+def create_regions(world: BKSimWorld) -> None:
+ multiworld = world.multiworld
+ player = world.player
+
+ for rid in RID:
+ multiworld.regions.append(Region(str(rid), player, multiworld))
+
+ locs_per_weather = world.options.locs_per_weather.value
+ for locid, locinfo in enumerate(location_table, 1):
+ if locinfo.index >= locs_per_weather:
+ continue
+ region = world.get_region(locinfo.region_id)
+ if region:
+ region.locations.append(BKSim_Location(player, locinfo.name, locid, region, locinfo))
diff --git a/worlds/bksim/rules.py b/worlds/bksim/rules.py
new file mode 100644
index 000000000000..c9c5d86dfcdf
--- /dev/null
+++ b/worlds/bksim/rules.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+from math import floor
+from ..generic.Rules import set_rule
+from .common import *
+from .locations import BKSim_Location
+import typing
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from .world import BKSimWorld
+
+
+def set_rules(world: BKSimWorld) -> None:
+ multiworld = world.multiworld
+ player = world.player
+ options = world.options
+
+ world.get_region(RID.HOME).connect(connecting_region=world.get_region(RID.SUNNY))
+ world.get_region(RID.HOME).connect(connecting_region=world.get_region(RID.RAINY))
+ world.get_region(RID.HOME).connect(connecting_region=world.get_region(RID.SNOWY), rule=lambda state: state.has(ITEM.BOOTS, player))
+
+ locs_list: typing.Iterable[BKSim_Location] = typing.cast(typing.Iterable[BKSim_Location], multiworld.get_locations(player))
+ loc_count = options.locs_per_weather.value
+ max_rules = []
+ for loc in locs_list:
+ if loc.info.region_id == RID.SUNNY:
+ if loc.info.index == 0:
+ continue
+ tmp_rule = lambda state, idx=loc.info.index: state.has(ITEM.SHOES, player, floor(idx / 2))
+ if loc.info.index == loc_count - 1:
+ max_rules.append(tmp_rule)
+ set_rule(loc, tmp_rule)
+ elif loc.info.region_id == RID.RAINY:
+ tmp_rule = lambda state, idx=loc.info.index: (state.has(ITEM.SHOES, player, floor(idx / 2) + 1) and state.has(
+ ITEM.NEWLOC, player)) or state.has(ITEM.SHOES, player, idx)
+ if loc.info.index == loc_count - 1:
+ max_rules.append(tmp_rule)
+ set_rule(loc, tmp_rule)
+ elif loc.info.region_id == RID.SNOWY:
+ tmp_rule = lambda state, idx=loc.info.index: state.has(ITEM.BOOTS, player, floor(idx / 2) + 1)
+ if loc.info.index == loc_count - 1:
+ max_rules.append(tmp_rule)
+ set_rule(loc, tmp_rule)
+
+ multiworld.completion_condition[player] = lambda state: all(rule(state) for rule in max_rules)
diff --git a/worlds/bksim/web_world.py b/worlds/bksim/web_world.py
new file mode 100644
index 000000000000..fed7a6cda0c4
--- /dev/null
+++ b/worlds/bksim/web_world.py
@@ -0,0 +1,16 @@
+from worlds.AutoWorld import WebWorld
+from . import options
+from BaseClasses import Tutorial
+
+
+class BKSimWebWorld(WebWorld):
+ options_presets = options.options_presets
+ setup_en = Tutorial(
+ "BKSim Setup",
+ "How to set up BK Simulator for Archipelago",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["Emily"]
+ )
+ tutorials = [setup_en]
diff --git a/worlds/bksim/world.py b/worlds/bksim/world.py
new file mode 100644
index 000000000000..605ed11cdf08
--- /dev/null
+++ b/worlds/bksim/world.py
@@ -0,0 +1,47 @@
+from typing import Any
+
+from BaseClasses import Region, MultiWorld
+from worlds.AutoWorld import World
+from . import locations, items, regions, rules, options, web_world
+from .common import *
+
+
+class BKSimWorld(World):
+ """
+ BK Simulator is a simple game where you walk to BK and back.
+ """
+
+ game = game_name
+ topology_present = False
+ web = web_world.BKSimWebWorld()
+ options_dataclass = options.BKSim_Options
+ options: options.BKSim_Options
+ location_name_to_id = locations.location_name_to_id
+ item_name_to_id = items.item_name_to_id
+
+ origin_region_name = str(RID.HOME)
+
+ def __init__(self, multiworld: MultiWorld, player: int):
+ super().__init__(multiworld, player)
+
+ def create_regions(self) -> None:
+ regions.create_regions(self)
+
+ def create_items(self) -> None:
+ items.create_items(self)
+
+ def create_item(self, name: str) -> items.BKSim_Item:
+ return items.create_item(name, self.player)
+
+ def set_rules(self) -> None:
+ rules.set_rules(self)
+
+ def fill_slot_data(self) -> dict[str, Any]:
+ return {
+ 'LocsPerWeather': self.options.locs_per_weather.value,
+ 'StartDistance': self.options.start_distance.value,
+ 'SpeedPerUpgrade': self.options.speed_per_upgrade.value,
+ }
+
+ def get_region(self, region_name: str) -> Region:
+ return self.multiworld.get_region(region_name, self.player)