Added logic, can still be locked

This commit is contained in:
Mysaa Java
2026-10-09 14:37:08 +02:00
parent 741f60bb3b
commit d23aedb924
9 changed files with 300 additions and 166 deletions
+17 -1
View File
@@ -52,7 +52,22 @@
TMP_HOME=$(mktemp -d) TMP_HOME=$(mktemp -d)
HOME=$TMP_HOME ${pkgs.archipelago}/bin/archipelago --update_settings HOME=$TMP_HOME ${pkgs.archipelago}/bin/archipelago --update_settings
ln -s ${nodecames-apworld} "$TMP_HOME/.local/share/Archipelago/worlds/nodecames.apworld" 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 { in {
@@ -66,6 +81,7 @@
nodecames = { type = "app"; program = "${nodecames-launcher}/bin/nodecames"; }; nodecames = { type = "app"; program = "${nodecames-launcher}/bin/nodecames"; };
postgres-server = { type = "app"; program = "${postgres-server}/bin/postgres-server"; }; postgres-server = { type = "app"; program = "${postgres-server}/bin/postgres-server"; };
archipelago = { type = "app"; program = "${test-archipelago}/bin/archipelago"; }; archipelago = { type = "app"; program = "${test-archipelago}/bin/archipelago"; };
archipelago-host = { type = "app"; program = "${archipelago-host}/bin/archipelago-host"; };
}; };
devShells.${system} = { devShells.${system} = {
+51
View File
@@ -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)}
+12 -24
View File
@@ -1,46 +1,27 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from BaseClasses import Item, ItemClassification
if TYPE_CHECKING: if TYPE_CHECKING:
from .world import NodecamesWorld from .world import NodecamesWorld
ITEM_NAME_TO_ID = { from BaseClasses import Item, ItemClassification
"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 . import const as c
class NodecamesItem(Item): class NodecamesItem(Item):
game = "Nodecames" game = "Nodecames"
def get_random_filler_item_name(world: NodecamesWorld) -> str: def get_random_filler_item_name(world: NodecamesWorld) -> str:
return "filler" return c.filler_item
def create_item_with_correct_classification( def create_item_with_correct_classification(
world: NodecamesWorld, name: str world: NodecamesWorld, name: str
) -> NodecamesItem: ) -> NodecamesItem:
classification = ItemClassification.progression classification = ItemClassification.progression
if name == "filler": if name == c.filler_item:
classification = ItemClassification.filler 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: 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("hints_in_a_row"),
world.create_item("anyone_starts"), 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) 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)] itempool += [world.create_filler() for _ in range(to_complete)]
world.multiworld.itempool += itempool world.multiworld.itempool += itempool
-67
View File
@@ -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)
+84 -6
View File
@@ -1,7 +1,8 @@
from dataclasses import dataclass 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): class WorldsLocked(DefaultOnToggle):
""" """
@@ -10,6 +11,16 @@ class WorldsLocked(DefaultOnToggle):
display_name = "Worlds locked" 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): class StartHintCount(Range):
""" """
@@ -183,12 +194,29 @@ class VictoryCondition(Choice):
option_LastWorld = 2 option_LastWorld = 2
default = 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. # 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". # This is in the format "option_name_in_snake_case: OptionClassName".
@dataclass @dataclass
class NodecamesOptions(PerGameCommonOptions): class NodecamesOptions(PerGameCommonOptions):
worlds_locked: WorldsLocked worlds_locked: WorldsLocked
hint_word_logic_ratio: HintWordLogicRatio
victory_condition: VictoryCondition victory_condition: VictoryCondition
start_hint_count: StartHintCount start_hint_count: StartHintCount
max_hint_count: MaxHintCount max_hint_count: MaxHintCount
@@ -205,14 +233,14 @@ class NodecamesOptions(PerGameCommonOptions):
structured_grid: StructuredGrid structured_grid: StructuredGrid
hints_in_a_row: HintsInARow hints_in_a_row: HintsInARow
anyone_starts: AnyoneStarts 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. # If we want to group our options by similar type, we can do so as well. This looks nice on the website.
option_groups = [ option_groups = [
OptionGroup( OptionGroup(
"General Options", "General Options",
[WorldsLocked, VictoryCondition], [WorldsLocked, VictoryCondition, HintWordLogicRatio],
), ),
OptionGroup( OptionGroup(
"Grid Options", "Grid Options",
@@ -232,12 +260,13 @@ option_groups = [
AnyoneStarts, 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". # Finally, we can define some option presets if we want the player to be able to quickly choose a specific "mode".
option_presets = { option_presets = {
"default": { "default": {
"hint_word_logic_ratio": 25,
"worlds_locked": True, "worlds_locked": True,
"start_hint_count": 1, "start_hint_count": 1,
"max_hint_count": 10, "max_hint_count": 10,
@@ -254,7 +283,56 @@ option_presets = {
"structured_grid": 1, "structured_grid": 1,
"hints_in_a_row": 1, "hints_in_a_row": 1,
"anyone_starts": 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, "victory_condition": 2,
}, },
} }
-22
View File
@@ -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")
+64 -17
View File
@@ -1,31 +1,78 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING 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: if TYPE_CHECKING:
from .world import NodecamesWorld from .world import NodecamesWorld
def set_all_rules(world: NodecamesWorld) -> None: from rule_builder.options import OptionFilter
set_all_entrance_rules(world) from rule_builder.rules import Has, HasAll, Rule, True_
set_all_location_rules(world) from BaseClasses import Region, Location
set_completion_condition(world) 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: class NodecamesLocation(Location):
pass 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: def can_access_word(options, word):
pass 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) def create_all_regions(world: NodecamesWorld) -> None:
world.set_completion_rule(HasAll(*worldLocations)) #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
+9 -29
View File
@@ -3,9 +3,9 @@ from typing import Any
from worlds.AutoWorld import World from worlds.AutoWorld import World
from . import items, locations, regions, rules from . import items, rules
from . import options as nodecames_options from . import options as nodecames_options
from . import const as c
class NodecamesWorld(World): class NodecamesWorld(World):
""" """
@@ -17,17 +17,14 @@ class NodecamesWorld(World):
options_dataclass = nodecames_options.NodecamesOptions options_dataclass = nodecames_options.NodecamesOptions
options: nodecames_options.NodecamesOptions options: nodecames_options.NodecamesOptions
location_name_to_id = locations.LOCATION_NAME_TO_ID location_name_to_id = c.LOCATION_NAME_TO_ID
item_name_to_id = items.ITEM_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: def create_regions(self) -> None:
regions.create_and_connect_regions(self) rules.create_all_regions(self)
locations.create_all_locations(self)
def set_rules(self) -> None:
rules.set_all_rules(self)
def create_items(self) -> None: def create_items(self) -> None:
items.create_all_items(self) items.create_all_items(self)
@@ -40,27 +37,10 @@ class NodecamesWorld(World):
def get_filler_item_name(self) -> str: def get_filler_item_name(self) -> str:
return items.get_random_filler_item_name(self) return items.get_random_filler_item_name(self)
def generate_output(self, outdir : str) -> None:
# Generates the output file # Generates the output file
def generate_output(self, outdir : str) -> None:
pass pass
# Data sent to the client # Data sent to the client
def fill_slot_data(self) -> Mapping[str, Any]: def fill_slot_data(self) -> Mapping[str, Any]:
return self.options.as_dict( return []
"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",
)
+63
View File
@@ -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