From 89cac6a0c615ca62adab92895cee0126b755a42d Mon Sep 17 00:00:00 2001 From: Mysaa Java Date: Sat, 26 Sep 2026 02:15:02 +0200 Subject: [PATCH] Adding first apworld --- flake.nix | 9 +- src/main/apworld/__init__.py | 1 + src/main/apworld/archipelago.json | 6 + src/main/apworld/items.py | 63 ++++++++ src/main/apworld/locations.py | 67 +++++++++ src/main/apworld/options.py | 233 ++++++++++++++++++++++++++++++ src/main/apworld/regions.py | 22 +++ src/main/apworld/rules.py | 31 ++++ src/main/apworld/world.py | 62 ++++++++ 9 files changed, 493 insertions(+), 1 deletion(-) create mode 100644 src/main/apworld/__init__.py create mode 100644 src/main/apworld/archipelago.json create mode 100644 src/main/apworld/items.py create mode 100644 src/main/apworld/locations.py create mode 100644 src/main/apworld/options.py create mode 100644 src/main/apworld/regions.py create mode 100644 src/main/apworld/rules.py create mode 100644 src/main/apworld/world.py diff --git a/flake.nix b/flake.nix index f7503eb..f1292c4 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,11 @@ gradle = pkgs.gradle; version = "1.0"; + nodecames-apworld = pkgs.runCommand "nodecames.apworld" {} '' + cp -r ${./src/main/apworld} ./nodecames/ + ${pkgs.zip}/bin/zip -r $out ./nodecames/ + ''; + nodecames = pkgs.buildGradleArtifact { pname = "nodecames"; src = ./.; @@ -38,10 +43,11 @@ mkdir -p .postgres ${pkgs.podman}/bin/podman run -e POSTGRES_USER=nodecames -e POSTGRES_PASSWORD=nodecames-dev -e POSTGRES_DB=nodecames --volume ./.postgres:/var/lib/postgresql -p 127.0.0.1:5432:5432 postgres:18.4 &> .postgres/postgres.log & ''; + in { packages.${system} = { default = self.packages.${system}.nodecames; - inherit nodecames; + inherit nodecames nodecames-apworld; }; apps.${system} = { @@ -59,6 +65,7 @@ vscode = pkgs.vscodium; vscodeExtensions = with pkgs.vscode-extensions; [ redhat.java + ms-python.python vscjava.vscode-java-debug vscjava.vscode-java-test vscjava.vscode-gradle diff --git a/src/main/apworld/__init__.py b/src/main/apworld/__init__.py new file mode 100644 index 0000000..a4d84e7 --- /dev/null +++ b/src/main/apworld/__init__.py @@ -0,0 +1 @@ +from .world import NodecamesWorld diff --git a/src/main/apworld/archipelago.json b/src/main/apworld/archipelago.json new file mode 100644 index 0000000..13830c3 --- /dev/null +++ b/src/main/apworld/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "NodeCames", + "minimum_ap_version": "0.6.8", + "world_version": "1.0.0", + "authors": ["Mysaa"] +} diff --git a/src/main/apworld/items.py b/src/main/apworld/items.py new file mode 100644 index 0000000..ffec7df --- /dev/null +++ b/src/main/apworld/items.py @@ -0,0 +1,63 @@ +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, + "filler": 12 +} +for w in range(1,12+1): + ITEM_NAME_TO_ID[f"unlock_world_{w}"] = 100+w + + +class NodecamesItem(Item): + game = "Nodecames" + +def get_random_filler_item_name(world: NodecamesWorld) -> str: + return "filler" + + +def create_item_with_correct_classification( + world: NodecamesWorld, name: str +) -> NodecamesItem: + return NodecamesItem(name, ItemClassification.progression, ITEM_NAME_TO_ID[name], world.player) + + +def create_all_items(world: NodecamesWorld) -> None: + itempool: list[Item] = [] + + hintItemCount = world.options.max_hint_count - world.options.max_hint_length + itempool += [world.create_item("progressive_hint_count") for _ in range(hintItemCount)] + itempool += [ + world.create_item("zero_hint"), + world.create_item("infinity_hint"), + world.create_item("one_more_guess"), + world.create_item("hint_max_length"), + world.create_item("end_guessing"), + world.create_item("only_one_green_green"), + world.create_item("sudden_death"), + world.create_item("structured_grid"), + world.create_item("hints_in_a_row"), + world.create_item("anyone_starts"), + ] + + to_complete = len(world.multiworld.get_unfilled_locations(world.player)) - len(itempool) + + 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 new file mode 100644 index 0000000..1c44a66 --- /dev/null +++ b/src/main/apworld/locations.py @@ -0,0 +1,67 @@ +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 new file mode 100644 index 0000000..8d8a904 --- /dev/null +++ b/src/main/apworld/options.py @@ -0,0 +1,233 @@ +from dataclasses import dataclass + +from Options import Choice, OptionGroup, PerGameCommonOptions, Range, DefaultOnToggle + + +class WorldsLocked(DefaultOnToggle): + """ + Access to worlds need to be unlocked by an item + """ + + display_name = "Worlds locked" + + +class StartHintCount(Range): + """ + Hint count at the beginning of the game + """ + + display_name = "Start hint count" + + range_start = 1 + range_end = 20 + default = 1 + + +class MaxHintCount(Range): + """ + Maximum hint count we can get + """ + + display_name = "Max hint count" + + range_start = 1 + range_end = 20 + default = 10 + + +class StartHintLength(Range): + """ + Maximal Hint length at the beginning of the game + """ + + display_name = "Start Hint Length" + + range_start = 3 + range_end = 30 + default = 3 + + +class MaxHintLength(Range): + """ + Maximal maximum hint length we can get. Once it is reached, maximal length is now infinity. + """ + + display_name = "Max Hint Length" + + range_start = 3 + range_end = 30 + default = 10 + + +class OnOffDefault(Choice): + option_Off = 0 + option_Item = 1 + option_On = 2 + + default = 1 + + +class ZeroHint(OnOffDefault): + """ + Can give zero as a hint + """ + + display_name = "Hint Count 0" + + +class InfinityHint(OnOffDefault): + """ + Can give infinity as a hint + """ + + display_name = "Hint Count infty" + + +class OneMoreGuess(OnOffDefault): + """ + Can guess one word more than hint count + """ + + display_name = "One More Guess" + + +class EndGuessing(OnOffDefault): + """ + Can end guessing before touching the given hint count + """ + + display_name = "End Guessing" + + +class OnlyOneGreenGreen(OnOffDefault): + """ + Green-Green cards only have to be pointed by one of the players + """ + + display_name = "Only One Green Green" + + +class SuddenDeath(OnOffDefault): + """ + Sudden death triggered when no more hints can be given + """ + + display_name = "Sudden Death" + + +class StructuredGrid(OnOffDefault): + """ + Are front and back colors of the cards linked + """ + + display_name = "Structured Grid" + + +class HintsInARow(OnOffDefault): + """ + Players can give multiple hints in a row + """ + + display_name = "HintsInARow" + + +class AnyoneStarts(OnOffDefault): + """ + Both players can start the game + """ + + display_name = "AnyoneStarts" + + +class WorldCount(Range): + """ + How many worlds to generate + """ + + display_name = "World Count" + + range_start = 1 + range_end = 12 + default = 12 + + +class VictoryCondition(Choice): + """ + What is needed to complete game + """ + + option_AllWorlds = 1 + option_LastWorld = 2 + default = 2 + + +# 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 + victory_condition: VictoryCondition + start_hint_count: StartHintCount + max_hint_count: MaxHintCount + start_hint_length: StartHintLength + max_hint_length: MaxHintLength + zero_hint: ZeroHint + infinity_hint: InfinityHint + one_more_guess: OneMoreGuess + end_guessing: EndGuessing + only_one_green_green: OnlyOneGreenGreen + sudden_death: SuddenDeath + structured_grid: StructuredGrid + hints_in_a_row: HintsInARow + anyone_starts: AnyoneStarts + world_count: WorldCount + + +# 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], + ), + OptionGroup( + "Grid Options", + [ + StartHintCount, + MaxHintCount, + StartHintLength, + MaxHintLength, + ZeroHint, + InfinityHint, + OneMoreGuess, + EndGuessing, + OnlyOneGreenGreen, + SuddenDeath, + StructuredGrid, + HintsInARow, + AnyoneStarts, + ], + ), + OptionGroup("Worlds Options", [WorldCount]), +] + +# Finally, we can define some option presets if we want the player to be able to quickly choose a specific "mode". +option_presets = { + "default": { + "worlds_locked": True, + "start_hint_count": 1, + "max_hint_count": 10, + "start_hint_length": 3, + "max_hint_length": 12, + "zero_hint": 1, + "infinity_hint": 1, + "one_more_guess": 1, + "end_guessing": 1, + "only_one_green_green": 1, + "sudden_death": 1, + "structured_grid": 1, + "hints_in_a_row": 1, + "anyone_starts": 1, + "world_count": 12, + "victory_condition": 2, + }, +} diff --git a/src/main/apworld/regions.py b/src/main/apworld/regions.py new file mode 100644 index 0000000..d9425ba --- /dev/null +++ b/src/main/apworld/regions.py @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..d53c367 --- /dev/null +++ b/src/main/apworld/rules.py @@ -0,0 +1,31 @@ +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) + + +def set_all_entrance_rules(world: NodecamesWorld) -> None: + pass + + +def set_all_location_rules(world: NodecamesWorld) -> None: + pass + + +def set_completion_condition(world: NodecamesWorld) -> None: + + worldLocations = locations.all_completion_items(world) + world.set_completion_rule(HasAll(*worldLocations)) diff --git a/src/main/apworld/world.py b/src/main/apworld/world.py new file mode 100644 index 0000000..5b11c82 --- /dev/null +++ b/src/main/apworld/world.py @@ -0,0 +1,62 @@ +from collections.abc import Mapping +from typing import Any + +from worlds.AutoWorld import World + +from . import items, locations, regions, rules +from . import options as nodecames_options + + +class NodecamesWorld(World): + """ + Nodecames is a word game, inspired by Code Names by Vlaada Chvátil + """ + + game = "Nodecames" + + 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 + + origin_region_name = "Overworld" + + 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) + + def create_items(self) -> None: + items.create_all_items(self) + + # Create an item from name + def create_item(self, name: str) -> items.NodecamesItem: + return items.create_item_with_correct_classification(self, name) + + # Create one filler item + def get_filler_item_name(self) -> str: + return items.get_random_filler_item_name(self) + + # 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", + )