From d23aedb924c9d79070e0b12c9ef34b9ccf03e8bc Mon Sep 17 00:00:00 2001 From: Mysaa Java Date: Fri, 9 Oct 2026 14:37:08 +0200 Subject: [PATCH] Added logic, can still be locked --- flake.nix | 18 +++++- src/main/apworld/const.py | 51 ++++++++++++++++ src/main/apworld/items.py | 36 ++++-------- src/main/apworld/locations.py | 67 --------------------- src/main/apworld/options.py | 90 +++++++++++++++++++++++++++-- src/main/apworld/regions.py | 22 ------- src/main/apworld/rules.py | 81 ++++++++++++++++++++------ src/main/apworld/world.py | 38 +++--------- src/main/resources/apworld/test.yml | 63 ++++++++++++++++++++ 9 files changed, 300 insertions(+), 166 deletions(-) create mode 100644 src/main/apworld/const.py delete mode 100644 src/main/apworld/locations.py delete mode 100644 src/main/apworld/regions.py create mode 100644 src/main/resources/apworld/test.yml diff --git a/flake.nix b/flake.nix index 77b2580..7f48f49 100644 --- a/flake.nix +++ b/flake.nix @@ -52,7 +52,22 @@ TMP_HOME=$(mktemp -d) HOME=$TMP_HOME ${pkgs.archipelago}/bin/archipelago --update_settings ln -s ${nodecames-apworld} "$TMP_HOME/.local/share/Archipelago/worlds/nodecames.apworld" - HOME=$TMP_HOME ${pkgs.archipelago}/bin/archipelago + chmod +w "$TMP_HOME/.local/share/Archipelago/Players/" + ln -s ${./src/main/resources/apworld/test.yml} "$TMP_HOME/.local/share/Archipelago/Players/Tester.yaml" + HOME=$TMP_HOME ${pkgs.archipelago}/bin/archipelago "$@" + ''; + }; + archipelago-host = pkgs.writeShellApplication { + name = "archipelago-host"; + text = '' + TMP_HOME=$(mktemp -d) + HOME=$TMP_HOME ${pkgs.archipelago}/bin/archipelago --update_settings + ln -s ${nodecames-apworld} "$TMP_HOME/.local/share/Archipelago/worlds/nodecames.apworld" + chmod +w "$TMP_HOME/.local/share/Archipelago/Players/" + ln -s ${./src/main/resources/apworld/test.yml} "$TMP_HOME/.local/share/Archipelago/Players/Tester.yaml" + HOME=$TMP_HOME ${pkgs.archipelago}/bin/archipelago Generate + GENFILE=$(find "$TMP_HOME/.local/share/Archipelago/output/" -name "*.zip") + HOME=$TMP_HOME ${pkgs.archipelago}/bin/archipelago Host "$GENFILE" ''; }; in { @@ -66,6 +81,7 @@ nodecames = { type = "app"; program = "${nodecames-launcher}/bin/nodecames"; }; postgres-server = { type = "app"; program = "${postgres-server}/bin/postgres-server"; }; archipelago = { type = "app"; program = "${test-archipelago}/bin/archipelago"; }; + archipelago-host = { type = "app"; program = "${archipelago-host}/bin/archipelago-host"; }; }; devShells.${system} = { diff --git a/src/main/apworld/const.py b/src/main/apworld/const.py new file mode 100644 index 0000000..83f9149 --- /dev/null +++ b/src/main/apworld/const.py @@ -0,0 +1,51 @@ +import re + +MAX_WORLD_COUNT=100 +MAX_WORD_PER_WORLD=99 + +def parseWordDistribution(s): + components = s.split(';') + out = {} + for comp in components: + p = re.match("^([GWB])([GWB])([0-9]+)$", comp) + if not p: + raise ValueError("Invalid component of word distribution") + colora = p.group(1) + colorb = p.group(2) + count = int(p.group(3)) + if (colora + colorb in out): + raise ValueError(f"Component {colora}{colorb} defined twice") + out[colora + colorb] = count + return out + +filler_item = "filler" + +ITEM_NAME_TO_ID = { + "progressive_hint_count" : 1, + "zero_hint" : 2, + "infinity_hint" : 3, + "one_more_guess" : 4, + "hint_max_length" : 5, + "end_guessing" : 6, + "only_one_green_green" : 7, + "sudden_death" : 8, + "structured_grid" : 9, + "hints_in_a_row" : 10, + "anyone_starts" : 11, + "progressive_hint_word_count": 12, + "progressive_hint_word_length": 13, + "filler": 14 +} +for w in range(MAX_WORLD_COUNT): + ITEM_NAME_TO_ID[f"unlock_world_{w+1}"] = 100+w + +def location_id_of_world_and_word(world, word): + return 10000 + (world-1)*(MAX_WORD_PER_WORLD+1) + word +def world_and_word_of_location(id): + world = (id - 10000)//(MAX_WORD_PER_WORLD+1) + 1 + word = (id - 10000) % (MAX_WORD_PER_WORLD+1) + return (world, word) +def location_name_of_world_and_word(world, word): + return f"World {world} word {word}" + +LOCATION_NAME_TO_ID = {location_name_of_world_and_word(world,word) : location_id_of_world_and_word(world,word) for world in range(1,MAX_WORLD_COUNT+1) for word in range(0,MAX_WORD_PER_WORLD+1)} \ No newline at end of file diff --git a/src/main/apworld/items.py b/src/main/apworld/items.py index 0980540..49eee5d 100644 --- a/src/main/apworld/items.py +++ b/src/main/apworld/items.py @@ -1,46 +1,27 @@ from __future__ import annotations from typing import TYPE_CHECKING - -from BaseClasses import Item, ItemClassification - if TYPE_CHECKING: from .world import NodecamesWorld -ITEM_NAME_TO_ID = { - "progressive_hint_count" : 1, - "zero_hint" : 2, - "infinity_hint" : 3, - "one_more_guess" : 4, - "hint_max_length" : 5, - "end_guessing" : 6, - "only_one_green_green" : 7, - "sudden_death" : 8, - "structured_grid" : 9, - "hints_in_a_row" : 10, - "anyone_starts" : 11, - "progressive_hint_word_count": 12, - "progressive_hint_word_length": 13, - "filler": 14 -} -for w in range(1,12+1): - ITEM_NAME_TO_ID[f"unlock_world_{w}"] = 100+w +from BaseClasses import Item, ItemClassification +from . import const as c class NodecamesItem(Item): game = "Nodecames" def get_random_filler_item_name(world: NodecamesWorld) -> str: - return "filler" + return c.filler_item def create_item_with_correct_classification( world: NodecamesWorld, name: str ) -> NodecamesItem: classification = ItemClassification.progression - if name == "filler": + if name == c.filler_item: classification = ItemClassification.filler - return NodecamesItem(name, classification, ITEM_NAME_TO_ID[name], world.player) + return NodecamesItem(name, classification, c.ITEM_NAME_TO_ID[name], world.player) def create_all_items(world: NodecamesWorld) -> None: @@ -64,9 +45,16 @@ def create_all_items(world: NodecamesWorld) -> None: world.create_item("hints_in_a_row"), world.create_item("anyone_starts"), ] + for wi,wn in enumerate(world.options.worlds.keys()): + w = world.options.worlds[wn] + if w['locked']: + itempool.append(world.create_item(f"unlock_world_{wi}")) + to_complete = len(world.multiworld.get_unfilled_locations(world.player)) - len(itempool) + print(to_complete,"items to fill") + itempool += [world.create_filler() for _ in range(to_complete)] world.multiworld.itempool += itempool diff --git a/src/main/apworld/locations.py b/src/main/apworld/locations.py deleted file mode 100644 index 1c44a66..0000000 --- a/src/main/apworld/locations.py +++ /dev/null @@ -1,67 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from BaseClasses import ItemClassification, Location - -from . import items - -if TYPE_CHECKING: - from .world import NodecamesWorld - - -GREEN_COUNT_PER_WORLD = [6, 9, 9, 12, 16, 16, 20, 25, 25, 30, 36, 36] - -def worldName(index): - return f"{(index//3)+1}-{(index%3)+1}" - -LOCATION_NAME_TO_ID = {} -LOCATION_NAME_FROM_WORLD = {} -id = 0 -for w in range(len(GREEN_COUNT_PER_WORLD)): - LOCATION_NAME_FROM_WORLD[w] = [] - for i in range(1, GREEN_COUNT_PER_WORLD[w]): - name = f"World {worldName(w)} word {i}" - id += 1 - LOCATION_NAME_FROM_WORLD[w].append(name) - LOCATION_NAME_TO_ID[name] = id - name = f"Complete {worldName(w)}" - id += 1 - LOCATION_NAME_FROM_WORLD[w].append(name) - LOCATION_NAME_TO_ID[name] = id - - -class NodecamesLocation(Location): - game = "Nodecames" - - -def create_all_locations(world: NodecamesWorld) -> None: - create_world_locations(world) - create_events(world) - - -def create_world_locations(world: NodecamesWorld) -> None: - overworld = world.get_region("Overworld") - - for w in LOCATION_NAME_FROM_WORLD: - for l in range(len(LOCATION_NAME_FROM_WORLD[w])-1): - name = LOCATION_NAME_FROM_WORLD[w][l] - loc = NodecamesLocation( - world.player, - name, - LOCATION_NAME_TO_ID[name], - overworld, - ) - overworld.locations.append(loc) - -def all_completion_items(world: NodecamesWorld) -> list[str]: - return [LOCATION_NAME_FROM_WORLD[w][-1]+" item" for w in LOCATION_NAME_FROM_WORLD] - -def create_events(world: NodecamesWorld) -> None: - overworld = world.get_region("Overworld") - for w in LOCATION_NAME_FROM_WORLD: - overworld.add_event( - LOCATION_NAME_FROM_WORLD[w][-1], - LOCATION_NAME_FROM_WORLD[w][-1]+" item", - location_type = NodecamesLocation, - item_type = items.NodecamesItem) diff --git a/src/main/apworld/options.py b/src/main/apworld/options.py index 4ee9633..665775a 100644 --- a/src/main/apworld/options.py +++ b/src/main/apworld/options.py @@ -1,7 +1,8 @@ from dataclasses import dataclass -from Options import Choice, OptionGroup, PerGameCommonOptions, Range, DefaultOnToggle - +from Options import Choice, OptionGroup, PerGameCommonOptions, Range, DefaultOnToggle, OptionDict +from schema import Schema, And +from . import const as c class WorldsLocked(DefaultOnToggle): """ @@ -10,6 +11,16 @@ class WorldsLocked(DefaultOnToggle): display_name = "Worlds locked" +class HintWordLogicRatio(Range): + """ + How many hints per word count in the grid is needed as a minimum. + Used to compute the logic + Divide by 10 + """ + range_start = 10 + range_end = 80 + display_name = "Hint Word Logic Ration" + class StartHintCount(Range): """ @@ -183,12 +194,29 @@ class VictoryCondition(Choice): option_LastWorld = 2 default = 2 +class WorldsOption(OptionDict): + """ + Which nodecames worlds will be in the game and their options + + It takes the following parameters: + + locked: wether the world is unlocked by default, or needs an item to be unlocked + word_distribution: a string describing the distributions of colors on the grid + """ + display_name = "Worlds" + schema = Schema({ + str: { + "locked": bool, + "word_distribution": And(str, c.parseWordDistribution), + "required": bool + }}) # We must now define a dataclass inheriting from PerGameCommonOptions that we put all our options in. # This is in the format "option_name_in_snake_case: OptionClassName". @dataclass class NodecamesOptions(PerGameCommonOptions): worlds_locked: WorldsLocked + hint_word_logic_ratio: HintWordLogicRatio victory_condition: VictoryCondition start_hint_count: StartHintCount max_hint_count: MaxHintCount @@ -205,14 +233,14 @@ class NodecamesOptions(PerGameCommonOptions): structured_grid: StructuredGrid hints_in_a_row: HintsInARow anyone_starts: AnyoneStarts - world_count: WorldCount + worlds: WorldsOption # If we want to group our options by similar type, we can do so as well. This looks nice on the website. option_groups = [ OptionGroup( "General Options", - [WorldsLocked, VictoryCondition], + [WorldsLocked, VictoryCondition, HintWordLogicRatio], ), OptionGroup( "Grid Options", @@ -232,12 +260,13 @@ option_groups = [ AnyoneStarts, ], ), - OptionGroup("Worlds Options", [WorldCount]), + OptionGroup("Worlds Options", [WorldsOption]), ] # Finally, we can define some option presets if we want the player to be able to quickly choose a specific "mode". option_presets = { "default": { + "hint_word_logic_ratio": 25, "worlds_locked": True, "start_hint_count": 1, "max_hint_count": 10, @@ -254,7 +283,56 @@ option_presets = { "structured_grid": 1, "hints_in_a_row": 1, "anyone_starts": 1, - "world_count": 12, + "worlds": { + "1-1": { + "locked": False, + "word_distribution": "GG1;GW2;GB0;WG2;WW1;WB0;BG0;BW0;BB0" + }, + "1-2": { + "locked": True, + "word_distribution": "GG2;GW1;GB1;WG1;WW2;WB0;BG1;BW0;BB1" + }, + "1-3": { + "locked": True, + "word_distribution": "GG2;GW1;GB1;WG1;WW2;WB0;BG1;BW0;BB1" + }, + "2-1": { + "locked": True, + "word_distribution": "GG2;GW1;GB1;WG1;WW3;WB1;BG1;BW1;BB1" + }, + "2-2": { + "locked": True, + "word_distribution": "GG2;GW2;GB1;WG2;WW5;WB1;BG1;BW1;BB1" + }, + "2-3": { + "locked": True, + "word_distribution": "GG2;GW2;GB1;WG2;WW5;WB1;BG1;BW1;BB1" + }, + "3-1": { + "locked": True, + "word_distribution": "GG2;GW4;GB1;WG4;WW5;WB1;BG1;BW1;BB1" + }, + "3-2": { + "locked": True, + "word_distribution": "GG3;GW5;GB1;WG5;WW7;WB1;BG1;BW1;BB1" + }, + "3-3": { + "locked": True, + "word_distribution": "GG3;GW5;GB1;WG5;WW7;WB1;BG1;BW1;BB1" + }, + "4-1": { + "locked": True, + "word_distribution": "GG3;GW5;GB2;WG5;WW7;WB2;BG2;BW2;BB1" + }, + "4-2": { + "locked": True, + "word_distribution": "GG5;GW6;GB2;WG6;WW9;WB2;BG2;BW2;BB2" + }, + "4-3": { + "locked": True, + "word_distribution": "GG5;GW6;GB2;WG6;WW9;WB2;BG2;BW2;BB2" + } + }, "victory_condition": 2, }, } diff --git a/src/main/apworld/regions.py b/src/main/apworld/regions.py deleted file mode 100644 index d9425ba..0000000 --- a/src/main/apworld/regions.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from BaseClasses import Entrance, Region - -if TYPE_CHECKING: - from .world import NodecamesWorld - - -def create_and_connect_regions(world: NodecamesWorld) -> None: - create_all_regions(world) - connect_regions(world) - - -def create_all_regions(world: NodecamesWorld) -> None: - overworld = Region("Overworld", world.player, world.multiworld) - world.multiworld.regions += [overworld] - - -def connect_regions(world: NodecamesWorld) -> None: - overworld = world.get_region("Overworld") diff --git a/src/main/apworld/rules.py b/src/main/apworld/rules.py index d53c367..e83ae42 100644 --- a/src/main/apworld/rules.py +++ b/src/main/apworld/rules.py @@ -1,31 +1,78 @@ from __future__ import annotations from typing import TYPE_CHECKING - -from rule_builder.options import OptionFilter -from rule_builder.rules import Has, HasAll, Rule - -from . import locations - if TYPE_CHECKING: from .world import NodecamesWorld -def set_all_rules(world: NodecamesWorld) -> None: - set_all_entrance_rules(world) - set_all_location_rules(world) - set_completion_condition(world) +from rule_builder.options import OptionFilter +from rule_builder.rules import Has, HasAll, Rule, True_ +from BaseClasses import Region, Location +from . import const as c +import math +from . import items +def print_graph(world : NodecamesWorld): + for region in world.multiworld.regions: + print(region) + for exit in region.exits: + print("->",exit,"--",exit.access_rule) -def set_all_entrance_rules(world: NodecamesWorld) -> None: - pass +class NodecamesLocation(Location): + game = "Nodecames" +def total_word_count(distribution): + return distribution['GB']+distribution['BG']+distribution['GW']+distribution['WG']+distribution['GG'] -def set_all_location_rules(world: NodecamesWorld) -> None: - pass +def can_access_word(options, word): + ratio = options.hint_word_logic_ratio/10 + min_hint_count = math.ceil(word/ratio) + min_hint_word_count = math.ceil(word/min_hint_count) # min_hint_count * min_hint_word_count >= word + return Has("progressive_hint_word_count", count=max(min_hint_word_count-options.start_hint_word_count,0)) & Has("progressive_hint_count", count=max(min_hint_count-options.start_hint_count,0)) -def set_completion_condition(world: NodecamesWorld) -> None: +def can_access_world(options, wn, wi): + if options.worlds[wn]['locked']: + return Has(f"unlock_world_{wi}") + else: + return True_() - worldLocations = locations.all_completion_items(world) - world.set_completion_rule(HasAll(*worldLocations)) +def create_all_regions(world: NodecamesWorld) -> None: + #TODO make a helper to add new regions + owmap = Region("Map", world.player, world.multiworld) + + wregions = dict() + toComplete = list() + + for wi,wn in enumerate(world.options.worlds.keys()): + w = world.options.worlds[wn] + distrib = c.parseWordDistribution(w['word_distribution']) + wregions[wn] = list() + for i in range(total_word_count(distrib)+1): + name = c.location_name_of_world_and_word(wi+1,i) + wregions[wn].append(Region(name, world.player, world.multiworld)) + if i>0: + wregions[wn][i-1].connect(wregions[wn][i], rule = can_access_word(world.options, i)) + else: + owmap.connect(wregions[wn][i], rule = can_access_world(world.options, wn, wi)) + loc = NodecamesLocation( + world.player, + name, + c.LOCATION_NAME_TO_ID[name], + wregions[wn][i] + ) + wregions[wn][i].locations.append(loc) + wregions[wn][-1].add_event( + f"World {wn} complete", + f"World {wn} completion item", + location_type = NodecamesLocation, + item_type = items.NodecamesItem) + if w['required']: + toComplete.append(f"World {wn} completion item") + + world.set_completion_rule(HasAll(*toComplete)) + + allregions = [owmap] + [wrxv for wrv in wregions.values() for wrxv in wrv] + print(f"Generated {len(allregions)} regions") + print(f"Generated {sum([len(reg.locations) for reg in allregions])} locations") + world.multiworld.regions += allregions \ No newline at end of file diff --git a/src/main/apworld/world.py b/src/main/apworld/world.py index f033b0d..59ad285 100644 --- a/src/main/apworld/world.py +++ b/src/main/apworld/world.py @@ -3,9 +3,9 @@ from typing import Any from worlds.AutoWorld import World -from . import items, locations, regions, rules +from . import items, rules from . import options as nodecames_options - +from . import const as c class NodecamesWorld(World): """ @@ -17,17 +17,14 @@ class NodecamesWorld(World): options_dataclass = nodecames_options.NodecamesOptions options: nodecames_options.NodecamesOptions - location_name_to_id = locations.LOCATION_NAME_TO_ID - item_name_to_id = items.ITEM_NAME_TO_ID + location_name_to_id = c.LOCATION_NAME_TO_ID + item_name_to_id = c.ITEM_NAME_TO_ID - origin_region_name = "Overworld" + origin_region_name = "Map" + # Create regions, locations and rules def create_regions(self) -> None: - regions.create_and_connect_regions(self) - locations.create_all_locations(self) - - def set_rules(self) -> None: - rules.set_all_rules(self) + rules.create_all_regions(self) def create_items(self) -> None: items.create_all_items(self) @@ -40,27 +37,10 @@ class NodecamesWorld(World): def get_filler_item_name(self) -> str: return items.get_random_filler_item_name(self) + # Generates the output file def generate_output(self, outdir : str) -> None: - # Generates the output file pass # Data sent to the client def fill_slot_data(self) -> Mapping[str, Any]: - return self.options.as_dict( - "worlds_locked", - "start_hint_count", - "max_hint_count", - "start_hint_length", - "max_hint_length", - "zero_hint", - "infinity_hint", - "one_more_guess", - "end_guessing", - "only_one_green_green", - "sudden_death", - "structured_grid", - "hints_in_a_row", - "anyone_starts", - "world_count", - "victory_condition", - ) + return [] diff --git a/src/main/resources/apworld/test.yml b/src/main/resources/apworld/test.yml new file mode 100644 index 0000000..d0f99d9 --- /dev/null +++ b/src/main/resources/apworld/test.yml @@ -0,0 +1,63 @@ +name: Tester +game: nodecames +nodecames: + progression_balancing: normal + accessibility: full + hint_word_logic_ratio: 25 + + start_hint_count: 1 + max_hint_count: 10 + start_hint_length: 3 + max_hint_length: 12 + start_hint_word_count: 1 + max_hint_word_count: 16 + + worlds: + 1-1: + locked: false + word_distribution: "GG1;GW2;GB0;WG2;WW1;WB0;BG0;BW0;BB0" + required: false + 1-2: + locked: true + word_distribution: "GG2;GW1;GB1;WG1;WW2;WB0;BG1;BW0;BB1" + required: false + 1-3: + locked: true + word_distribution: "GG2;GW1;GB1;WG1;WW2;WB0;BG1;BW0;BB1" + required: true + 2-1: + locked: true + word_distribution: "GG2;GW1;GB1;WG1;WW3;WB1;BG1;BW1;BB1" + required: false + 2-2: + locked: true + word_distribution: "GG2;GW2;GB1;WG2;WW5;WB1;BG1;BW1;BB1" + required: false + 2-3: + locked: true + word_distribution: "GG2;GW2;GB1;WG2;WW5;WB1;BG1;BW1;BB1" + required: true + 3-1: + locked: true + word_distribution: "GG2;GW4;GB1;WG4;WW5;WB1;BG1;BW1;BB1" + required: false + 3-2: + locked: true + word_distribution: "GG3;GW5;GB1;WG5;WW7;WB1;BG1;BW1;BB1" + required: false + 3-3: + locked: true + word_distribution: "GG3;GW5;GB1;WG5;WW7;WB1;BG1;BW1;BB1" + required: true + 4-1: + locked: true + word_distribution: "GG3;GW5;GB2;WG5;WW7;WB2;BG2;BW2;BB1" + required: false + 4-2: + locked: true + word_distribution: "GG5;GW6;GB2;WG6;WW9;WB2;BG2;BW2;BB2" + required: false + 4-3: + locked: true + word_distribution: "GG5;GW6;GB2;WG6;WW9;WB2;BG2;BW2;BB2" + required: true \ No newline at end of file