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)
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} = {
+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 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
-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 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,
},
}
-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 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
+9 -29
View File
@@ -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)
def generate_output(self, outdir : str) -> None:
# Generates the output file
def generate_output(self, outdir : str) -> None:
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 []
+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