Repository: gerbiljames/Archipelago-Crystal
Pull request #82: Move Randomizer
Author: login: M3tin
Labels: name: enhancement
description: New feature or request
Created: 2025-03-21T20:31:24Z | Merged: 2025-03-24T13:31:29Z

## What does this add?
Move Randomizer option to the game. Move Types Randomization and Move Value Randomization works independent of eachother.

## Why do you want it to be added?
Fun chaotic option to have 

## Was this tested yet?
Yes by manual Rom Generation (will use next time)


Diff:
diff --git a/worlds/pokemon_crystal/__init__.py b/worlds/pokemon_crystal/__init__.py
index e5133530ec1b..56718a8a5fdc 100644
--- a/worlds/pokemon_crystal/__init__.py
+++ b/worlds/pokemon_crystal/__init__.py
@@ -9,12 +9,12 @@
 from worlds.AutoWorld import World, WebWorld
 from .client import PokemonCrystalClient
 from .data import PokemonData, TrainerData, MiscData, TMHMData, data as crystal_data, \
-    WildData, StaticPokemon, MusicData
+    WildData, StaticPokemon, MusicData, MoveData
 from .items import PokemonCrystalItem, create_item_label_to_code_map, get_item_classification, \
     ITEM_GROUPS, item_const_name_to_id, item_const_name_to_label
 from .locations import create_locations, PokemonCrystalLocation, create_location_label_to_id_map
 from .misc import misc_activities, get_misc_spoiler_log
-from .moves import randomize_tms
+from .moves import randomize_tms, randomize_move_values, randomize_move_types
 from .music import randomize_music
 from .options import PokemonCrystalOptions, JohtoOnly, RandomizeBadges, Goal, HMBadgeRequirements, Route32Condition
 from .phone import generate_phone_traps
@@ -72,6 +72,7 @@ class PokemonCrystalWorld(World):
 
     free_fly_location: int
     map_card_fly_location: int
+    generated_moves=Dict[str, MoveData]
     generated_pokemon: Dict[str, PokemonData]
     generated_starters: Tuple[List[str], List[str], List[str]]
     generated_starter_helditems: Tuple[str, str, str]
@@ -233,6 +234,7 @@ def pre_fill(self) -> None:
 
     def generate_output(self, output_directory: str) -> None:
 
+        self.generated_moves = copy.deepcopy(crystal_data.moves)
         self.generated_pokemon = copy.deepcopy(crystal_data.pokemon)
         self.generated_starters = (["CYNDAQUIL", "QUILAVA", "TYPHLOSION"],
                                    ["TOTODILE", "CROCONAW", "FERALIGATR"],
@@ -249,6 +251,12 @@ def generate_output(self, output_directory: str) -> None:
         self.generated_phone_indices = []
         self.generated_wooper = "WOOPER"
 
+        if self.options.randomize_move_values.value:
+             randomize_move_values(self)
+ 
+        if self.options.randomize_move_types.value:
+             randomize_move_types(self)
+
         randomize_pokemon(self)
 
         if self.options.randomize_starters.value:
diff --git a/worlds/pokemon_crystal/moves.py b/worlds/pokemon_crystal/moves.py
index 2358717b11ab..22c0553ba3ca 100644
--- a/worlds/pokemon_crystal/moves.py
+++ b/worlds/pokemon_crystal/moves.py
@@ -34,13 +34,13 @@ def randomize_learnset(world: "PokemonCrystalWorld", pkmn_name):
 
     # All moves available at Lv.1 that do damage (and don't faint the user)
     start_attacking = [learnset for learnset in new_learnset if
-                       crystal_data.moves[learnset.move].power > 0
-                       # This list is the damaging moves that should be ignored for checking starting attacking moves
-                       and learnset.move not in ["EXPLOSION", "SELFDESTRUCT", "STRUGGLE", "SNORE", "DREAM_EATER"]
+                       world.generated_moves[learnset.move].power > 0
+                       #This list is the damaging moves that should be ignored for checking starting attacking moves
+                       and learnset.move not in ["EXPLOSION", "SELFDESTRUCT", "STRUGGLE", "SNORE","DREAM_EATER"] 
                        and learnset.level == 1]
 
-    if not start_attacking:  # if there are no attacking moves at Lv.1, add one
-        new_learnset[0] = LearnsetData(1, get_random_move(world, attacking=True))
+    if not len(start_attacking):  # if there are no attacking moves at Lv.1, add one
+        new_learnset[0] = LearnsetData(1, get_random_move(world, attacking=True)) #overwrites whatever the 1st move is
 
     return new_learnset
 
@@ -58,7 +58,7 @@ def get_random_move(world: "PokemonCrystalWorld", move_type=None, attacking=None
                  and move_name not in ["STRUGGLE", "BEAT_UP", "NO_MOVE"]
                  and move_name not in existing_moves
                  and (not move_type or move_data.type == move_type)]
-
+    
     if attacking is not None:
         move_pool = [move_name for move_name in move_pool if crystal_data.moves[move_name].power > 0
                      and move_name not in existing_moves]
@@ -95,11 +95,14 @@ def get_tmhm_compatibility(world: "PokemonCrystalWorld", pkmn_name):
 
 
 def randomize_tms(world: "PokemonCrystalWorld"):
-    global_move_pool = [move_data for move_name, move_data in copy.deepcopy(crystal_data.moves).items() if
-                        not move_data.is_hm and move_name not in ["ROCK_SMASH", "NO_MOVE", "STRUGGLE"]]
+  
+    global_move_pool = [move_data for move_name, move_data in world.generated_moves.items() if
+                 not move_data.is_hm and move_name not in ["ROCK_SMASH", "NO_MOVE", "STRUGGLE"]]
+  
     filtered_move_pool = []
 
     # remove every move from filtered_move_pool that is in the blocklist
+
     if world.options.move_blocklist:
         filtered_move_pool = [move for move in global_move_pool if move.name not in world.options.move_blocklist]
 
@@ -124,3 +127,36 @@ def get_random_move_from_learnset(world: "PokemonCrystalWorld", pokemon, level):
     # exclude beat up as it can softlock the game if an enemy trainer uses it
     move_pool += move_pool + [move for move in world.generated_pokemon[pokemon].tm_hm if move != "BEAT_UP"]
     return world.random.choice(move_pool)
+
+def randomize_move_values(world: "PokemonCrystalWorld"):
+    acc100 = 70 # I need a value for this I can take this from YAML
+    for move_name, move_data in world.generated_moves.items():
+        if move_name in ["NO_MOVE", "CURSE"]:
+            continue
+        new_power = move_data.power
+        new_acc = move_data.accuracy
+        new_pp = move_data.pp
+        if new_power > 1: #dont touch status OHKO or Special Calculated Damage Moves POWER and ACCURACY (I will change this later)
+            if world.options.randomize_move_values == 1:
+                new_power = int(new_power*(world.random.random()+0.5))
+                if new_power > 255: new_power = 255
+                new_pp=new_pp+world.random.choice([-10,-5,0,5,10])
+                if new_pp < 5: new_pp = 5
+                if new_pp > 40: new_pp = 40
+            else:
+                new_power = world.random.randint(20,150)
+                new_pp =world.random.randint(5,40)
+            if world.options.randomize_move_values == 3: 
+                if world.random.randint(1,100) <= acc100:
+                    new_acc = 255
+                else:
+                    new_acc = world.random.randint(76,255) # 30 is 76,5 so actual lowest accuracy is a bit lower than 30
+        world.generated_moves[move_name]=world.generated_moves[move_name]._replace(power=new_power,accuracy=new_acc,pp=new_pp)
+
+def randomize_move_types(world: "PokemonCrystalWorld"):
+    data_types = copy.deepcopy(crystal_data.types)
+    for move_name, move_data in world.generated_moves.items():
+        if move_name in ["NO_MOVE", "CURSE"]:
+            continue
+        new_type = world.random.choice(data_types)
+        world.generated_moves[move_name]=world.generated_moves[move_name]._replace(type=new_type)
\ No newline at end of file
diff --git a/worlds/pokemon_crystal/options.py b/worlds/pokemon_crystal/options.py
index 10fb7c059c58..461719d2f1f7 100644
--- a/worlds/pokemon_crystal/options.py
+++ b/worlds/pokemon_crystal/options.py
@@ -224,6 +224,34 @@ class LearnsetTypeBias(Range):
     range_start = -1
     range_end = 100
 
+class RandomizeMoveValues(Choice):
+    """
+    Restricted: Generates values based on vanilla
+    multiplies Power of each move with a random number between 0,5 to 1,5 and
+    Adds or substract 0, 5 or 10 from original PP | Min 5 Max 40
+ 
+    Full Exclude Accuracy: Fully randomizes move Power and PP
+    Randomizes each move's Power [20-150], PP [5-40] linearly. All possible values have the same weight.
+     
+    Full: Previous + also randomizes Accuracy
+    Accuracy has a flat chance of %70 for being % 100 accurate if not it is again linear distributed between 30-100. 
+    For now it does not randomize Accuracy of OHKO moves, status moves (eg. Toxic) and unique damage moves (eg. Seismic Toss)
+    """
+    display_name = "Randomize Move Values"
+    default = 0
+    option_vanilla = 0
+    option_restricted = 1
+    option_full_exclude_accuracy = 2
+    option_full = 3
+ 
+class RandomizeMoveTypes(Toggle):
+    """
+    Randomizes each move's Type
+    """
+    display_name = "Randomize Move Types"
+    default = 0
+    option_vanilla = 0
+    option_random_types = 1
 
 class RandomizeTMMoves(Toggle):
     """
@@ -560,6 +588,8 @@ class PokemonCrystalOptions(PerGameCommonOptions):
     randomize_trainer_parties: RandomizeTrainerParties
     randomize_learnsets: RandomizeLearnsets
     learnset_type_bias: LearnsetTypeBias
+    randomize_move_values: RandomizeMoveValues
+    randomize_move_types: RandomizeMoveTypes
     randomize_tm_moves: RandomizeTMMoves
     tm_compatibility: TMCompatibility
     hm_compatibility: HMCompatibility
diff --git a/worlds/pokemon_crystal/rom.py b/worlds/pokemon_crystal/rom.py
index acc1dd3a3c2f..c340c46400f0 100644
--- a/worlds/pokemon_crystal/rom.py
+++ b/worlds/pokemon_crystal/rom.py
@@ -191,6 +191,19 @@ def generate_output(world: "PokemonCrystalWorld", output_directory: str, patch:
         # 0xC9 = ret
         write_bytes(patch, [0xC9], data.rom_addresses["AP_Setting_FruitTreesReset"])
 
+    for move_name, move in world.generated_moves.items(): #effect modification is also possible but not included
+        if move_name in ["NO_MOVE", "CURSE"]:
+            continue
+        address = data.rom_addresses["AP_MoveData_Type_" + move_name]
+        move_type_id = [data.type_ids[move.type] ]
+        write_bytes(patch, move_type_id, address)  # uses same type id conversion that pkmn type randomizer
+        address = data.rom_addresses["AP_MoveData_Power_" + move_name]
+        write_bytes(patch, [move.power], address)  # power 20-150
+        address = data.rom_addresses["AP_MoveData_Accuracy_" + move_name]
+        write_bytes(patch, [move.accuracy], address)  # accuracy 30-100
+        address = data.rom_addresses["AP_MoveData_PP_" + move_name]
+        write_bytes(patch, [move.pp], address)  # 5-40 PP
+
     for pkmn_name, pkmn_data in world.generated_pokemon.items():
         if world.options.randomize_types.value:
             address = data.rom_addresses["AP_Stats_Types_" + pkmn_name]


Reviews:
user: login: gerbiljames
body: Great stuff! Just the conflicts to resolve and the one change in the option comment and I'm good to merge,
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-23T19:37:33Z
user: login: M3tin
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-24T11:06:52Z
user: login: gerbiljames
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-24T13:31:17Z

Review comments:
diff_hunk: @@ -224,6 +224,37 @@ class LearnsetTypeBias(Range):
     range_start = -1
     range_end = 100
 
+class RandomizeMoveValues(Choice):
+    """
+    Restricted: Generates values based on vanilla
+    multiplies Power of each move with a random number between 0,5 to 1,5 and
+    Adds or substract 0, 5 or 10 from original PP | Min 5 Max 40
+ 
+    Full Exclude Accuracy: Fully randomizes move Power and PP
+    Randomizes each move's Power [20-150], PP [5-40] linearly. All possible values have the same weight.
+     
+    Full: Previous + also randomizes Accuracy
+    Accuracy has a flat chance of %70 for being % 100 accurate if not it is again linear distributed between 30-100. 
+    For now it does not randomize Accuracy of OHKO moves, status moves (eg. Toxic) and unique damage moves (eg. Seismic Toss)
+         
+    WARNING: Right now there is no method of checking Accuracy in game. It is a planned feature.
path: worlds/pokemon_crystal/options.py
user: login: gerbiljames
body: You can delete this, I added a way to see move accuracy in game!
created_at: 2025-03-23T19:33:27Z
author_association: OWNER
diff_hunk: @@ -224,6 +224,37 @@ class LearnsetTypeBias(Range):
     range_start = -1
     range_end = 100
 
+class RandomizeMoveValues(Choice):
+    """
+    Restricted: Generates values based on vanilla
+    multiplies Power of each move with a random number between 0,5 to 1,5 and
+    Adds or substract 0, 5 or 10 from original PP | Min 5 Max 40
+ 
+    Full Exclude Accuracy: Fully randomizes move Power and PP
+    Randomizes each move's Power [20-150], PP [5-40] linearly. All possible values have the same weight.
+     
+    Full: Previous + also randomizes Accuracy
+    Accuracy has a flat chance of %70 for being % 100 accurate if not it is again linear distributed between 30-100. 
+    For now it does not randomize Accuracy of OHKO moves, status moves (eg. Toxic) and unique damage moves (eg. Seismic Toss)
+         
+    WARNING: Right now there is no method of checking Accuracy in game. It is a planned feature.
path: worlds/pokemon_crystal/options.py
user: login: M3tin
body: Removed it while resolving the conflicts, did not do a new commit.
created_at: 2025-03-24T11:06:52Z
author_association: NONE

PR comments:
user: login: M3tin
created_at: 2025-03-24T10:36:16Z
body: As I am resolving the conglicts I saw a problem on get_random_move function. The requirsive part should remove the Type limit as well. And I think if a player puts a blocklist that move should be removed from the game. If we cannot find a move we should remove the type restriction not the block list restriction.
author_association: NONE
user: login: gerbiljames
created_at: 2025-03-24T10:39:21Z
body: > As I am resolving the conglicts I saw a problem on get_random_move function. The requirsive part should remove the Type limit as well. And I think if a player puts a blocklist that move should be removed from the game. If we cannot find a move we should remove the type restriction not the block list restriction.

We need to handle the case where the user blocklists every move (I know users won't, but it still needs to be handled)
author_association: OWNER
user: login: M3tin
created_at: 2025-03-24T10:49:42Z
body: Then shall we remove the Move Type restriction first, then the blocklist?
author_association: NONE