diff --git a/README.md b/README.md index 21a6faaa2698..25c41103583c 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ Currently, the following games are supported: * Kingdom Hearts 1 * Mega Man 2 * Yacht Dice +* ChecksMate * Faxanadu * Saving Princess diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 1aec57fc90f6..217c72fdfa0f 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -42,6 +42,9 @@ # ChecksFinder /worlds/checksfinder/ @SunCatMC +# ChecksMate Chess +/worlds/checksmate/ @chesslogic + # Clique /worlds/clique/ @ThePhar diff --git a/worlds/checksmate/Items.py b/worlds/checksmate/Items.py new file mode 100644 index 000000000000..e6c6c6157105 --- /dev/null +++ b/worlds/checksmate/Items.py @@ -0,0 +1,97 @@ +import math +from typing import Dict, NamedTuple, Optional, List, Union + +from BaseClasses import Item, ItemClassification + + +class CMItem(Item): + game: str = "ChecksMate" + + +class CMItemData(NamedTuple): + code: Optional[int] + classification: ItemClassification + quantity: float = 1 # maximum, not guaranteed + material: int = 0 # pawns=100, minor=300, major=500, queen=900 + # for each given parent item, the maximum number of child items which may be present + parents: List[List[Union[str, int]]] = [] + + +item_table = { + "Play as White": CMItemData(4_901_000, ItemClassification.progression, material=50), + "Progressive Engine ELO Lobotomy": CMItemData(4_901_001, ItemClassification.useful, quantity=5), + # TODO: stop counting material if the board fills up with 23 pieces+pawns + "Progressive Pawn": CMItemData(4_901_002, ItemClassification.progression, quantity=40, material=100), + "Progressive Pawn Forwardness": CMItemData(4_901_003, ItemClassification.filler, quantity=13, parents=[ + ["Progressive Pawn", 3]]), + # Bishops and Knights are worth 3.25 to 3.5, but some minor pieces are worse, so we assume 3.0 conservatively + "Progressive Minor Piece": CMItemData(4_901_004, ItemClassification.progression, quantity=15, material=300), + # Rooks are worth 5.25 to 5.5, but many major pieces are worse, so we assume 4.85, which stays under 5.0 + "Progressive Major Piece": CMItemData(4_901_005, ItemClassification.progression, quantity=11, material=485), + # Queen pieces are pretty good, and even the weak ones are pretty close, so queens can stay 9.0 (but not 10.0) + "Progressive Major To Queen": CMItemData(4_901_006, ItemClassification.progression, quantity=9, material=415, + parents=[["Progressive Major Piece", 1]]), + "Victory": CMItemData(4_901_009, ItemClassification.progression), + "Super-Size Me": CMItemData(4_901_010, ItemClassification.progression, quantity=0), # :) + # TODO: implement extra moves + # "Progressive Enemy Pawn": CMItemData(4_907, ItemClassification.trap, quantity=8), + # "Progressive Enemy Piece": CMItemData(4_908, ItemClassification.trap, quantity=7), + # "Progressive Opening Move": CMItemData(4_013, ItemClassification.useful, quantity=3), + + # Players have 3 pockets, which can be empty, or hold a pawn, minor piece, major piece, or queen. + # Collected pocket items are distributed randomly to the 3 pockets in the above order. + # Pocket pawns are playable onto home row instead of making a move + # Pocket pieces start as minor pieces (e.g. Knight) - they upgrade in both Gem cost and type + # Piece upgrades turn minor pieces into major pieces or major pieces into Queen - implementation may decide + "Progressive Pocket": CMItemData(4_901_020, ItemClassification.progression, quantity=12, material=110), + + # Gems are a way to generate filler items and limit use of Pocket items + # Gems are generated 1/turn and Pocket pieces cost 1 Gem per their material value + # Turn off Pocket entirely to hide this item. + "Progressive Pocket Gems": CMItemData(4_901_023, ItemClassification.filler, quantity=math.inf), + # Allows the player to deploy pocket items one rank further from the home row, but not the opponent's home row + "Progressive Pocket Range": CMItemData(4_901_024, ItemClassification.filler, quantity=6), + + "Progressive King Promotion": CMItemData(4_901_025, ItemClassification.progression, quantity=2, material=350), + # Material is really about your ability to get checks, so here is the material value of a Commoner, but the AI gets + # pretty confused when a royal piece isn't subject to check/mate, so this is a more powerful item than indicated for + # the purpose of Checkmate Maxima. TODO: Consider adding a property "tactics", used for some complex locations. + "Progressive Consul": CMItemData(4_901_026, ItemClassification.progression, quantity=2, material=325), + + # 4_901_030 - 4_901_044 are unused, previously N + # TODO: implement castling rule & guarantee major piece on that side for Locations + # "Play 00 Castle": CMItemData(4_014, ItemClassification.progression), + # "Play 000 Castle": CMItemData(4_015, ItemClassification.progression), + # TODO: consider breaking passant into individual pawns, or progressive for outer..center pawns + # "Play En Passant": CMItemData(4_011, ItemClassification.progression), + + # == Possible pocket implementation == + # "Progressive Pocket Pawn": CMItemData(4_021, ItemClassification.progression, quantity=3, material=90), + # "Progressive Pocket Pawn to Piece": CMItemData(4_022, ItemClassification.progression, quantity=3, material=190, + # parents=["Progressive Pocket Pawn"]), + # "Progressive Pocket Piece Promotion": parents=["Progressive Pocket Pawn to Piece", + # "Progressive Pocket Pawn"]), + # == End possible pocket implementation == +} + +lookup_id_to_name: Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} + +material_items: Dict[str, CMItemData] = { + item: item_data for (item, item_data) in item_table.items() if item_data.material > 0} +progression_items: Dict[str, CMItemData] = { + item: item_data for (item, item_data) in item_table.items() if + item_data.classification == ItemClassification.progression} +useful_items: Dict[str, CMItemData] = { + item: item_data for (item, item_data) in item_table.items() if + item_data.classification == ItemClassification.useful} +filler_items: Dict[str, CMItemData] = { + item: item_data for (item, item_data) in item_table.items() if + item_data.classification == ItemClassification.filler} +item_name_groups = { + # "Pawn": {"Pawn A", "Pawn B", "Pawn C", "Pawn D", "Pawn E", "Pawn F", "Pawn G", "Pawn H"}, + # "Enemy Pawn": {"Enemy Pawn A", "Enemy Pawn B", "Enemy Pawn C", "Enemy Pawn D", + # "Enemy Pawn E", "Enemy Pawn F", "Enemy Pawn G", "Enemy Pawn H"}, + # "Enemy Piece": {"Enemy Piece A", "Enemy Piece B", "Enemy Piece C", "Enemy Piece D", + # "Enemy Piece F", "Enemy Piece G", "Enemy Piece H"}, + "Chessmen": {"Progressive Pawn", "Progressive Minor Piece", "Progressive Major Piece", "Progressive Consul"}, +} diff --git a/worlds/checksmate/Locations.py b/worlds/checksmate/Locations.py new file mode 100644 index 000000000000..055bfef325cb --- /dev/null +++ b/worlds/checksmate/Locations.py @@ -0,0 +1,153 @@ +from enum import Enum +from typing import Dict, NamedTuple, Optional + +from BaseClasses import Location + + +class CMLocation(Location): + game: str = "ChecksMate" + + +class Tactic(Enum): + Fork = 0 + Turns = 1 + + +class CMLocationData(NamedTuple): + code: Optional[int] + # suggested material required to perform task. generally an upper-end estimate. used to: + # a. capture individual pieces + # b. capture series of pieces and pawns within 1 game + # c. fork/pin + material_expectations: int + # material in grand chess mode + material_expectations_grand: int + chessmen_expectations: int = 0 + is_tactic: Optional[Tactic] = None + + +location_table = { + # capture individual pieces and pawns + # AI prefers not to use edge pawns early - thus they stay defended longer + "Capture Pawn A": CMLocationData(4_902_000, 490, 810), + "Capture Pawn B": CMLocationData(4_902_001, 340, 610), + # AI prefers to open queenside as developing queen has more tempo + "Capture Pawn C": CMLocationData(4_902_002, 220, 460), + "Capture Pawn D": CMLocationData(4_902_003, 100, 320), + "Capture Pawn E": CMLocationData(4_902_004, 100, 120), + "Capture Pawn F": CMLocationData(4_902_005, 340, 120), + "Capture Pawn G": CMLocationData(4_902_006, 390, 420), + # AI prefers not to use edge pawns early - thus they stay defended longer + "Capture Pawn H": CMLocationData(4_902_007, 490, 660), + "Capture Pawn I": CMLocationData(4_902_101, -1, 810), + "Capture Pawn J": CMLocationData(4_902_102, -1, 890), + # bishops are less deployable than knights, and rooks are even more stuck back there + "Capture Piece Queen's Rook": CMLocationData(4_902_008, 900, 1650), + "Capture Piece Queen's Knight": CMLocationData(4_902_010, 700, 1200), + "Capture Piece Queen's Bishop": CMLocationData(4_902_012, 1040, 1200), + "Capture Piece Queen": CMLocationData(4_902_014, 1300, 1900), + "Checkmate Minima": CMLocationData(4_902_098, 4020, 4020), # (this is the game's goal / completion condition) + "Checkmate Maxima": CMLocationData(4_902_099, -1, 6020), # (this is the game's goal / completion condition) + # AI prefers not to open kingside as developing queen has more tempo + "Capture Piece King's Bishop": CMLocationData(4_902_013, 1140, 1400), + "Capture Piece King's Knight": CMLocationData(4_902_011, 1040, 1400), + "Capture Piece King's Rook": CMLocationData(4_902_009, 1240, 2050), + "Capture Piece Queen's Attendant": CMLocationData(4_902_109, -1, 1950), + "Capture Piece King's Attendant": CMLocationData(4_902_110, -1, 2030), + # some first locations + # for strategic analysis see: https://en.wikipedia.org/wiki/Bongcloud_Attack + "King to E2/E7 Early": CMLocationData(4_902_015, 0, 0), + "King to Center": CMLocationData(4_902_016, 50, 50), + "King to A File": CMLocationData(4_902_017, 0, 50), + "King Captures Anything": CMLocationData(4_902_018, 150, 350), + "King to Back Rank": CMLocationData(4_902_019, 2250, 5150), # requires reaching a rather late-game state + # capture series of pieces and pawns within 1 game + "Capture 2 Pawns": CMLocationData(4_902_020, 750, 1150, 1), + "Capture 3 Pawns": CMLocationData(4_902_021, 1450, 1950, 2), + "Capture 4 Pawns": CMLocationData(4_902_022, 2240, 2740, 3), + "Capture 5 Pawns": CMLocationData(4_902_023, 2620, 3020, 4), + "Capture 6 Pawns": CMLocationData(4_902_024, 2975, 3375, 5), + "Capture 7 Pawns": CMLocationData(4_902_025, 3255, 3555, 6), + "Capture 8 Pawns": CMLocationData(4_902_026, 3545, 4145, 7), + "Capture 9 Pawns": CMLocationData(4_902_120, -1, 4645, 8), + "Capture 10 Pawns": CMLocationData(4_902_121, -1, 5245, 9), + # Specific pieces should not be guaranteed to be accessible early, so we add +4 material (1piece+1pawn more) + "Capture 2 Pieces": CMLocationData(4_902_027, 1450, 3000, 1), + "Capture 3 Pieces": CMLocationData(4_902_028, 2100, 3400, 2), + "Capture 4 Pieces": CMLocationData(4_902_029, 2770, 3750, 3), + "Capture 5 Pieces": CMLocationData(4_902_030, 2950, 4150, 4), + "Capture 6 Pieces": CMLocationData(4_902_031, 3300, 4500, 5), + "Capture 7 Pieces": CMLocationData(4_902_032, 3750, 4900, 6), + "Capture 8 Pieces": CMLocationData(4_902_122, -1, 5200, 7), + "Capture 9 Pieces": CMLocationData(4_902_123, -1, 5400, 8), + "Capture 2 Of Each": CMLocationData(4_902_033, 1900, 3750, 3), + "Capture 3 Of Each": CMLocationData(4_902_034, 2350, 4250, 5), + "Capture 4 Of Each": CMLocationData(4_902_035, 2750, 4650, 7), + "Capture 5 Of Each": CMLocationData(4_902_036, 3100, 5000, 9), + "Capture 6 Of Each": CMLocationData(4_902_037, 3500, 5400, 11), + "Capture 7 Of Each": CMLocationData(4_902_038, 3850, 5650, 13), + "Capture 8 Of Each": CMLocationData(4_902_130, -1, 5850, 15), + "Capture 9 Of Each": CMLocationData(4_902_131, -1, 5950, 17), + "Capture Everything": CMLocationData(4_902_039, 4020, 6050, -1), + "Capture Any 2": CMLocationData(4_902_070, 750, 850, 1), + "Capture Any 3": CMLocationData(4_902_071, 1150, 1350, 2), + "Capture Any 4": CMLocationData(4_902_072, 1850, 2050, 3), + "Capture Any 5": CMLocationData(4_902_073, 2150, 2750, 4), + "Capture Any 6": CMLocationData(4_902_074, 2300, 3100, 5), + "Capture Any 7": CMLocationData(4_902_075, 2550, 3450, 6), + "Capture Any 8": CMLocationData(4_902_076, 2800, 3700, 7), + "Capture Any 9": CMLocationData(4_902_077, 3100, 4050, 8), + "Capture Any 10": CMLocationData(4_902_078, 3400, 4400, 9), + "Capture Any 11": CMLocationData(4_902_079, 3650, 4650, 10), + "Capture Any 12": CMLocationData(4_902_080, 3850, 4750, 11), + "Capture Any 13": CMLocationData(4_902_081, 3950, 5050, 12), + "Capture Any 14": CMLocationData(4_902_082, 4000, 5400, 13), + "Capture Any 15": CMLocationData(4_902_083, -1, 5650, 14), + "Capture Any 16": CMLocationData(4_902_084, -1, 5750, 15), + "Capture Any 17": CMLocationData(4_902_085, -1, 5850, 16), + "Capture Any 18": CMLocationData(4_902_086, -1, 5900, 17), + "Current Objective: Survive 3 Turns": CMLocationData(4_902_140, 0, 0, 0, is_tactic=Tactic.Turns), + "Current Objective: Survive 5 Turns": CMLocationData(4_902_141, 200, 330, 2, is_tactic=Tactic.Turns), + "Current Objective: Survive 10 Turns": CMLocationData(4_902_142, 2500, 4500, 9, is_tactic=Tactic.Turns), + "Current Objective: Survive 20 Turns": CMLocationData(4_902_143, 3800, 5800, 15, is_tactic=Tactic.Turns), + # some easier interaction moves + "Threaten Pawn": CMLocationData(4_902_040, 0, 0), + "Threaten Minor": CMLocationData(4_902_041, 200, 400), + "Threaten Major": CMLocationData(4_902_042, 300, 500), + "Threaten Queen": CMLocationData(4_902_043, 300, 500), + "Threaten King": CMLocationData(4_902_044, 1000, 1800), + # special moves and tactics + # TODO: Getting a french move on the AI occurs seldom - maybe I can tweak the evaluation or something? + # "French Move": CMLocationData(4_902_050, 0), + "Fork, Sacrificial": CMLocationData(4_902_052, 700, 1100, 6, is_tactic=Tactic.Fork), + "Fork, Sacrificial Triple": CMLocationData(4_902_053, 1700, 2700, 9, is_tactic=Tactic.Fork), + # AI really hates getting royal forked + "Fork, Sacrificial Royal": CMLocationData(4_902_054, 3200, 5200, 12, is_tactic=Tactic.Fork), + "Fork, True": CMLocationData(4_902_055, 2550, 4550, 10, is_tactic=Tactic.Fork), + "Fork, True Triple": CMLocationData(4_902_056, 3450, 5450, 12, is_tactic=Tactic.Fork), + # I sincerely believe this should be filler + "Fork, True Royal": CMLocationData(4_902_057, 4020, 6020, 14, is_tactic=Tactic.Fork), + "O-O Castle": CMLocationData(4_902_058, 0, 0, 2), + "O-O-O Castle": CMLocationData(4_902_059, 0, 0, 2), + # "Discovered Attack": CMLocationData(4_902_060, 0), + # "Pin": CMLocationData(4_902_061, 600), + # "Skewer": CMLocationData(4_902_062, 600), + # "Pawn Promotion": CMLocationData(4_902_063, 3000), + # "Multiple Queens": CMLocationData(4_902_064, 3900), + # goal 1+ requires that you successively checkmate your opponent as they gain material + +} + +lookup_id_to_name: Dict[int, str] = {data.code: item_name for item_name, data in location_table.items() if data.code} + +piece_names_small = ["Queen's Rook", "Queen's Knight", "Queen's Bishop", "Queen", + "King's Rook", "King's Knight", "King's Bishop"] +piece_names = ["Queen's Rook", "Queen's Knight", "Queen's Bishop", "Queen", + "King's Rook", "King's Knight", "King's Bishop", + "Queen's Attendant", "King's Attendant"] + +highest_chessmen_requirement_small = max([ + location_table[location].chessmen_expectations for location in location_table if + location_table[location].material_expectations != -1]) +highest_chessmen_requirement = max([ + location_table[location].chessmen_expectations for location in location_table]) diff --git a/worlds/checksmate/Options.py b/worlds/checksmate/Options.py new file mode 100644 index 000000000000..10d2e40c8b1f --- /dev/null +++ b/worlds/checksmate/Options.py @@ -0,0 +1,396 @@ +from dataclasses import dataclass +from typing import Callable, Dict + +from Options import Range, Option, Choice, NamedRange, ItemDict, PerGameCommonOptions, OptionSet, DeathLink + + +class Goal(Choice): + """ + How victory is defined. + + Single: Your opponent starts with an army of 7 pieces and 8 pawns. You have a king. Finding checkmate is your goal. + To get there, find checks, mate! + + Ordered Progressive: When you deliver checkmate, you instead graduate to a Super-Sized board. Your goal is to + checkmate again, on that board! + + Progressive: As Ordered Progressive, but the board grows larger when someone sends you your Super-Sized board. + + Super: You skip the 8x8 board immediately. Nearly equivalent to adding Super-Size Me to your Start Inventory. + """ + display_name = "Goal" + option_single = 0 + option_ordered_progressive = 1 + option_progressive = 2 + option_super = 3 + default = 1 + + +class Difficulty(Choice): + """ + Which kinds of checks to expect of the player. In general, this mostly affects later checks (like Checkmate Maxima, + the victory condition). + + Grandmaster: All checks are baseline. You will generally hope for equal material, and may find yourself struggling. + You will have about the same material as the AI for Checkmate Maxima to be considered in logic. + + Daily: The player may expect some difficulty with early checks, but complex game states will be relaxed. You will + have about an extra Bishop and an extra Pawn for Checkmate Maxima to be considered in logic. + + Bullet: All checks are relaxed. Material expectations are raised, so the player will have more material earlier. You + will have about an extra Rook and an extra Bishop for Checkmate Maxima to be considered in logic. + + Relaxed: Most checks require almost twice as much material, so the player will have overwhelming forces. You will + have about an extra Queen and an extra Rook and an extra Pawn for Checkmate Maxima to be considered in logic. + """ + display_name = "Difficulty" + option_grandmaster = 0 + option_daily = 1 + option_bullet = 2 + option_relaxed = 3 + default = 1 + + +class EnableTactics(Choice): + """ + All: Adds the "Fork" and "Play Turns" locations to the pool. (This adds 10 locations and items.) + + Turns: Adds the "Play Turns" locations to the pool. (This adds 4 locations and items.) + + None: Neither "Fork" nor "Play Turns" locations will be in the pool. + """ + display_name = "Enable Tactics" + option_all = 0 + option_turns = 1 + option_none = 2 + default = 1 + + +class PieceLocations(Choice): + """ + When you start a new match, chooses how to distribute player pieces. + + Chaos: Puts pieces on the first rank until it's full, and pawns on second rank until it's full. + Changes every match - your games won't preserve starting position. Plays more like Chess960. + + Stable: As Chaos, but doesn't change between matches. + """ + display_name = "Piece Locations" + option_chaos = 0 + option_stable = 1 + # option_ordered = 2 + default = 0 + + +class PieceTypes(Choice): + """ + When you start a new match, chooses the player's piece types (such as whether a minor piece is a Knight or Bishop). + + Chaos: Chooses random valid options. + + Stable: As Chaos, but doesn't change between matches. You'll only ever add or upgrade pieces. + """ + display_name = "Piece Types" + option_chaos = 0 + option_stable = 1 + # option_book = 2 + default = 1 + + +class EarlyMaterial(Choice): + """ + Guarantees that a King move directly onto the second rank within the first few moves will provide a piece or pawn + (chessman). When this option is set, this location (Move King E2/E7 Early) overrides any exclusion. + + Four other Bongcloud moves also involve the King, but are not altered by this option. (A File: Move to the leftmost + File; Capture: Any capturing move; Center: Move to any of the center 4 squares; Promotion: Move to enemy back rank) + + Pawn, Minor, Major: You will get an early chessman of the specified type (i.e. a pawn, minor piece, or major piece). + + Piece: You will get an early minor or major piece. + + Any: You will get an early chessman. + """ + display_name = "Early Material" + option_off = 0 + option_pawn = 1 + option_minor = 2 + option_major = 3 + option_piece = 4 + option_any = 5 + default = 0 + + +class MaximumEnginePenalties(Range): + """ + The number of times the engine will receive a reduction to their skill level. These reductions are currently named + "Progressive ELO Engine Lobotomy," and each level reduces the AI's access to both analysis and information. + """ + display_name = "Maximum Engine Penalties" + range_start = 0 + range_end = 5 + default = 5 + + +class MaximumPocket(Range): + """ + The number of Progressive Pocket Pieces the game is allowed to add to the multiworld. + + Each Progressive Pocket Piece will improve your 1st, 2nd, or 3rd pocket slot up to 4 times, from Nothing to Pawn, to + Minor Piece (like a pocket Knight), to Major Piece (like a pocket Rook), to Queen. + + This option does not alter filler item distribution. (Even if you have 0 Progressive Pockets, the item pool may + contain Progressive Pocket Gems and Progressive Pocket Range.) + + Pocket Pieces are inspired by the Dutch game of paard in de zak (pocket knight). + """ + display_name = "Maximum Pocket" + range_start = 0 + range_end = 12 + default = 12 + + +class MaximumKings(Range): + """ + How many Royal pieces (Kings) to place, which must all be captured before one experiences defeat. + + The player always starts with 1 King, but may find Progressive Consuls if this is set higher than 1. Progressive + Consuls add additional Kings to the player's starting board. + """ + display_name = "Maximum Kings" + range_start = 1 + range_end = 3 + default = 1 + + +class FairyKings(Range): + """ + Whether to use fairy king upgrades, such as the Knight's moves. Adding multiple upgrades to the pool will allow your + King to become a hyper-powerful invented piece if all upgrades are collected. + """ + display_name = "Fairy Kings" + range_start = 0 + range_end = 2 + default = 0 + + +class FairyChessPieces(Choice): + """ + Which collection of fairy pieces to allow, if any. Choose FIDE to disable fairy chess pieces. Choose Configure to + disable this option in favor of the more precise "Fairy Chess Pieces Configure" option. + + FIDE: The default, which only allows the standard pieces defined by FIDE (Queen, Rook, Knight, Bishop). + + Betza: Adds the pieces from Ralph Betza's "Chess With Different Armies", being the Remarkable Rookies, Colorbound + Clobberers, and Nutty Knights. + + Full: Adds every implemented army, including Eurasian and custom pieces. The Cannon and Vao capture by jumping over + an intervening chessman. + + Configure: Allows you to specify your own pieces using the "Fairy Chess Pieces Configure" option. + """ + display_name = "Fairy Chess Pieces" + option_fide = 0 + option_betza = 1 + option_full = 2 + option_configure = 3 + default = 0 + + +class FairyChessPiecesConfigure(OptionSet): + """ + THIS OPTION IS INCOMPATIBLE WITH "Fairy Chess Pieces". Set that option to "Configure" to use this option. + + Whether to use fairy chess pieces. Most pieces below are from Ralph Betza's Chess with Different Armies. If omitted, + the default allows for all following fairy chess pieces, as well as the standard pieces defined by FIDE. + + FIDE: Contains the standard chess pieces, consisting of the Bishop, Knight, Rook, and Queen. + + Rookies: Adds the CwDA army inspired by Rooks, the Remarkable Rookies. The Half-Duck castles rather than the Short + Rook. + + Clobberers: Adds the CwDA army inspired by Bishops, the Colorbound Clobberers. Fad and Bede may both castle. + + Nutty: Adds the CwDA army inspired by Knights, the Nutty Knights. + + Cannon: Adds the Rook-like Cannon, which captures a distal chessman by leaping over an intervening chessman, and the + Vao, a Bishop-like Cannon, in that it moves and captures diagonally. + + Camel: Adds a custom army themed after 3,x leapers like the Camel (3,1) and Tribbabah (3,0). (The Knight is a 2,1 + leaper.) + """ + display_name = "Fairy Chess Pieces Configure" + valid_keys = frozenset([ + "FIDE", + "Rookies", + "Clobberers", + "Nutty", + "Cannon", + "Camel", + ]) + default = valid_keys + + +# TODO: Rename to ...Mixed/Mercs +class FairyChessArmy(Choice): + """ + Whether to mix pieces between the Different Armies. Does not affect pawns. Note that the Cannon pieces, which + replace the Bishop and Knight with a Vao and Cannon, constitute a very powerful yet flawed Different Army. + + Chaos: Chooses random enabled options. (You can disable armies by setting "Fairy Chess Pieces Configure".) + + Stable: Chooses within one army. (If you want at most 2 Bishops, 2 Knights, 2 Rooks, and 1 Queen, add Piece Type + Limits below: 2 Minor, 2 Major, and 1 Queen.) + """ + display_name = "Fairy Chess Army" + option_chaos = 0 + option_stable = 1 + # TODO: will select within one army but that army will change between games - clobberers issue with major pieces + # option_limited = 2 + default = 0 + + +class FairyChessPawns(Choice): + """ + Whether to use fairy chess pawns. + + Vanilla: Only use the standard pawn. + + Mixed: Adds all implemented fairy chess pawns to the pool. You may receive a mix of different types of pawns. + + Berolina: Only use the Berolina pawn (may appear to be a Ferz), which moves diagonally and captures forward. + """ + display_name = "Fairy Chess Pawns" + option_vanilla = 0 + option_mixed = 1 + option_berolina = 2 + default = 0 + + +class MinorPieceLimitByType(NamedRange): + """ + How many of any given type of minor piece you might play with. If set to 1, you will never start with more than 1 + Knight, nor 1 Bishop, but you may have both 1 Knight and 1 Bishop. If set to 0, this setting is disabled. + """ + display_name = "Minor Piece Limit by Type" + range_start = 1 + range_end = 15 + default = 0 + special_range_names = { + "disabled": 0, + } + + +class MajorPieceLimitByType(NamedRange): + """ + How many of any given type of major piece you might play with. If set to 1, you will never start with more than 1 + Rook. If set to 0, this setting is disabled. + """ + display_name = "Major Piece Limit by Type" + range_start = 1 + range_end = 11 + default = 0 + special_range_names = { + "disabled": 0, + } + + +class QueenPieceLimitByType(NamedRange): + """ + How many of any given type of Queen-equivalent piece you might play with. If set to 1, you will never start with + more than 1 Queen. You may have both 1 Queen and 1 Amazon. If set to 0, this setting is disabled. + """ + display_name = "Queen Piece Limit by Type" + range_start = 1 + range_end = 9 + default = 0 + special_range_names = { + "disabled": 0, + } + + +class PocketLimitByPocket(NamedRange): + """ + How many Progressive Pocket items might be allocated to any given pocket. If this is set to 1, any given Pocket will + never hold anything more substantial than a Pawn. If this is set to 3, any given Pocket will never hold a Queen. + + The default of 4 allows each of the 3 spaces to hold between 0-4 progressive items. + + Disabling this option will remove Pocket items from the item pool. + """ + display_name = "Pocket Limit by Pocket" + range_start = 1 + range_end = 4 + default = 4 + special_range_names = { + "disabled": 0, + } + + +class QueenPieceLimit(NamedRange): + """ + How many Queen-equivalent pieces you might play with. If set to 1, you will never have more than 1 piece upgraded to + a Queen. (This does nothing when greater than 'Queen Piece Limit by Type'.) You may still promote pawns during a + game. If set to 0, this setting is disabled. + """ + display_name = "Queen Piece Limit" + range_start = 1 + range_end = 9 + default = 0 + special_range_names = { + "disabled": 0, + } + + +class LockedItems(ItemDict): + """ + Guarantees that these progression and filler items will be unlockable. + + Implementation note: Currently forces this many items into the item pool before distribution begins. This behaviour + is not guaranteed - a future version may simply validate the pool contains these items. + """ + display_name = "Locked Items" + + +class Deathlink(DeathLink): + """ + Whenever you are checkmated or resign (close the game window), everyone who is also on Death Link dies. Whenever + you receive a Death Link event, your game window closes. (You cannot undo or review.) + """ + + +@dataclass +class CMOptions(PerGameCommonOptions): + goal: Goal + difficulty: Difficulty + enable_tactics: EnableTactics + piece_locations: PieceLocations + piece_types: PieceTypes + early_material: EarlyMaterial + max_engine_penalties: MaximumEnginePenalties + max_pocket: MaximumPocket + max_kings: MaximumKings + fairy_kings: FairyKings + fairy_chess_pieces: FairyChessPieces + fairy_chess_pieces_configure: FairyChessPiecesConfigure + fairy_chess_army: FairyChessArmy + fairy_chess_pawns: FairyChessPawns + minor_piece_limit_by_type: MinorPieceLimitByType + major_piece_limit_by_type: MajorPieceLimitByType + queen_piece_limit_by_type: QueenPieceLimitByType + queen_piece_limit: QueenPieceLimit + pocket_limit_by_pocket: PocketLimitByPocket + locked_items: LockedItems + death_link: Deathlink + + +piece_type_limit_options: Dict[str, Callable[[CMOptions], Option]] = { + "Progressive Minor Piece": lambda cmoptions: cmoptions.minor_piece_limit_by_type, + "Progressive Major Piece": lambda cmoptions: cmoptions.major_piece_limit_by_type, + "Progressive Major To Queen": lambda cmoptions: cmoptions.queen_piece_limit_by_type, +} + + +piece_limit_options: Dict[str, Callable[[CMOptions], Option]] = { + "Progressive Major To Queen": lambda cmoptions: cmoptions.queen_piece_limit, +} diff --git a/worlds/checksmate/Presets.py b/worlds/checksmate/Presets.py new file mode 100644 index 000000000000..baa5d856c3de --- /dev/null +++ b/worlds/checksmate/Presets.py @@ -0,0 +1,101 @@ +from typing import Any + +from .Options import * + +checksmate_option_presets: Dict[str, Dict[str, Any]] = { + # Standard Chess pieces, moving in standard Chess ways, allowing many combinations of material. + # Leaves unique features and mixed material on, but all pieces will be recognizable. + "No Dumb Pieces": { + "fairy_chess_pieces": FairyChessPieces.option_fide, + "fairy_chess_pawns": FairyChessPawns.option_vanilla, + }, + + # A vanilla army with no pockets, comprising 2 Bishops+Knights+Rooks, and 1 Queen (or Rook until upgraded) + "Strict Traditional": { + "early_material": EarlyMaterial.option_pawn, # not counted against locked_items (this may be changed) + + "difficulty": 0, # excludes so many items that it can never get more than 45 material + "max_engine_penalties": 5, + "max_pocket": 0, + "fairy_chess_pieces": FairyChessPieces.option_fide, + "fairy_chess_pawns": FairyChessPawns.option_vanilla, + + "minor_piece_limit_by_type": 2, + "major_piece_limit_by_type": 2, + "queen_piece_limit": 1, + "locked_items": { + "Progressive Minor Piece": 4, + "Progressive Major Piece": 3, + "Progressive Major To Queen": 1, + }, + "start_hints": {"Play as White"}, + }, + + # Chaos and pocket pieces + "Sleeved Ace": { + "early_material": EarlyMaterial.option_pawn, + + "difficulty": 2, + "max_engine_penalties": 5, + "max_pocket": 12, + "fairy_chess_pieces": FairyChessPieces.option_betza, + "fairy_chess_pawns": FairyChessPawns.option_vanilla, + "fairy_chess_army": FairyChessArmy.option_chaos, + + "minor_piece_limit_by_type": 2, + "major_piece_limit_by_type": 2, + "queen_piece_limit": 1, + "locked_items": { + "Progressive Pocket": 12, + }, + "start_hints": {"Play as White"}, + }, + + # Weird Fairy Chess with opportunity to study the opening + "Different Army": { + "early_material": EarlyMaterial.option_piece, + + "difficulty": 2, + "max_engine_penalties": 5, + "max_pocket": 12, + "fairy_chess_pieces": FairyChessPieces.option_betza, + "fairy_chess_pawns": FairyChessPawns.option_vanilla, + "fairy_chess_army": FairyChessArmy.option_stable, + + "minor_piece_limit_by_type": 2, + "major_piece_limit_by_type": 2, + "queen_piece_limit": 1, + "locked_items": { + "Progressive Minor Piece": 4, + "Progressive Major Piece": 3, + "Progressive Major To Queen": 1, + }, + "start_hints": {"Play as White"}, + }, + + # Many exotic royal pieces + "Power Couples": { + "progression_balancing": 69, + + "early_material": EarlyMaterial.option_major, + + "difficulty": 2, + "max_engine_penalties": 5, + "max_pocket": 12, + "fairy_chess_pieces": FairyChessPieces.option_betza, + "fairy_chess_pawns": FairyChessPawns.option_berolina, + "fairy_chess_army": FairyChessArmy.option_chaos, + + "minor_piece_limit_by_type": 1, + "major_piece_limit_by_type": 1, + "queen_piece_limit_by_type": 1, + "queen_piece_limit": 5, + "locked_items": { + "Progressive Consul": 2, + "Progressive King Promotion": 2, + "Progressive Major Piece": 2, # the 3rd is granted as Early Material! + "Progressive Major To Queen": 3 + }, + "start_hints": {"Play as White"}, + }, +} diff --git a/worlds/checksmate/Rules.py b/worlds/checksmate/Rules.py new file mode 100644 index 000000000000..0bfabbe01a74 --- /dev/null +++ b/worlds/checksmate/Rules.py @@ -0,0 +1,153 @@ +from math import ceil + +from typing import cast + +from BaseClasses import CollectionState +from worlds.AutoWorld import World +from . import progression_items + +from worlds.generic.Rules import set_rule, add_rule +from .Options import CMOptions +from .Locations import location_table, Tactic + + +def has_french_move(state: CollectionState, player: int) -> bool: + return state.has("Progressive Pawn", player, 7) # and self.has("Play En Passant", player) + + +def has_pawn(state: CollectionState, player: int) -> bool: + return state.has("Progressive Pawn", player) + + +def has_pin(state: CollectionState, player: int) -> bool: + return state.has_any(("Progressive Minor Piece", "Progressive Major Piece"), player) + + +def determine_difficulty(opts: CMOptions): + difficulty = 1.0 + if opts.fairy_chess_army.value == opts.fairy_chess_army.option_stable: + difficulty *= 1.05 + if opts.fairy_chess_pawns.value != opts.fairy_chess_pawns.option_vanilla: + difficulty *= 1.05 + if opts.fairy_chess_pawns.value == opts.fairy_chess_pawns.option_mixed: + difficulty *= 1.05 + fairy_pieces = len(opts.fairy_chess_pieces_configure.value) + if opts.fairy_chess_pieces.value == opts.fairy_chess_pieces.option_fide: + fairy_pieces = 1 + elif opts.fairy_chess_pieces.value == opts.fairy_chess_pieces.option_betza: + fairy_pieces = 4 + elif opts.fairy_chess_pieces.value == opts.fairy_chess_pieces.option_full: + fairy_pieces = 6 + difficulty *= 0.99 + (0.01 * fairy_pieces) + # difficulty *= 1 + (0.025 * (5 - self.options.max_engine_penalties)) + + if opts.difficulty.value == opts.difficulty.option_daily: + difficulty *= 1.1 # results in, for example, the 4000 checkmate requirement becoming 4400 + if opts.difficulty.value == opts.difficulty.option_bullet: + difficulty *= 1.2 # results in, for example, the 4000 checkmate requirement becoming 4800 + if opts.difficulty.value == opts.difficulty.option_relaxed: + difficulty *= 1.35 # results in, for example, the 4000 checkmate requirement becoming 5400 + return difficulty + + +def determine_material(opts: CMOptions, base_material: int): + difficulty = determine_difficulty(opts) + material = base_material * 100 * difficulty + material += progression_items["Play as White"].material * difficulty + return material + determine_relaxation(opts) + + +def determine_min_material(opts: CMOptions): + return determine_material(opts, 41) + + +def determine_max_material(opts: CMOptions): + return determine_material(opts, 46) + + +def determine_relaxation(opts: CMOptions) -> int: + if opts.difficulty.value == opts.difficulty.option_bullet: + return 120 + if opts.difficulty.value == opts.difficulty.option_relaxed: + return 240 + return 0 + + +def meets_material_expectations(state: CollectionState, + material: int, player: int, difficulty: float, absolute_relaxation: int) -> bool: + # TODO: handle other goals + target = (material * difficulty) + (absolute_relaxation if material > 90 else 0) + return state.prog_items[player]["Material"] >= target + + +def meets_chessmen_expectations(state: CollectionState, + count: int, player: int, pocket_limit_by_pocket: int) -> bool: + chessmen_count = state.count_group("Chessmen", player) + if pocket_limit_by_pocket == 0: + return chessmen_count >= count + pocket_count = ceil(state.count("Progressive Pocket", player) / pocket_limit_by_pocket) + return chessmen_count + pocket_count >= count + + +def set_rules(world: World): + opts = cast(CMOptions, world.options) + difficulty = determine_difficulty(opts) + absolute_relaxation = determine_relaxation(opts) + super_sized = opts.goal.value != opts.goal.option_single + always_super_sized = opts.goal.value == opts.goal.option_super + + world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player) + + for name, item in location_table.items(): + if not super_sized and item.material_expectations == -1: + continue + if item.is_tactic is not None: + if opts.enable_tactics.value == opts.enable_tactics.option_none: + continue + elif opts.enable_tactics.value == opts.enable_tactics.option_turns and \ + item.is_tactic == Tactic.Fork: + continue + # AI avoids making trades except where it wins material or secures victory, so require that much material + material_cost = item.material_expectations if not super_sized else ( + item.material_expectations_grand if always_super_sized else max( + item.material_expectations, item.material_expectations_grand + )) + if material_cost > 0: + set_rule(world.multiworld.get_location(name, world.player), + lambda state, v=material_cost: meets_material_expectations( + state, v, world.player, difficulty, absolute_relaxation)) + # player must have (a king plus) that many chessmen to capture any given number of chessmen + if item.chessmen_expectations == -1: + # this is used for items which change between grand and not... currently only 1 location + assert item.code == 4_902_039, f"Unknown location code for custom chessmen: {str(item.code)}" + add_rule(world.multiworld.get_location(name, world.player), + lambda state: meets_chessmen_expectations( + state, 18 if super_sized else 14, world.player, opts.pocket_limit_by_pocket.value)) + elif item.chessmen_expectations > 0: + add_rule(world.multiworld.get_location(name, world.player), + lambda state, v=item.chessmen_expectations: meets_chessmen_expectations( + state, v, world.player, opts.pocket_limit_by_pocket.value)) + if item.material_expectations == -1: + add_rule(world.multiworld.get_location(name, world.player), + lambda state: state.has("Super-Size Me", world.player)) + + # tactics + # add_rule(multiworld.get_location("Pin", player), lambda state: has_pin(state, player)) + if opts.enable_tactics.value == opts.enable_tactics.option_all: + add_rule(world.get_location("Fork, Sacrificial"), lambda state: has_pin(state, world.player)) + add_rule(world.get_location("Fork, True"), lambda state: has_pin(state, world.player)) + add_rule(world.get_location("Fork, Sacrificial Triple"), lambda state: has_pin(state, world.player)) + add_rule(world.get_location("Fork, True Triple"), lambda state: has_pin(state, world.player)) + add_rule(world.get_location("Fork, Sacrificial Royal"), lambda state: has_pin(state, world.player)) + add_rule(world.get_location("Fork, True Royal"), lambda state: has_pin(state, world.player)) + add_rule(world.get_location("Threaten Minor"), lambda state: has_pin(state, world.player)) + add_rule(world.get_location("Threaten Major"), lambda state: has_pin(state, world.player)) + add_rule(world.get_location("Threaten Queen"), lambda state: has_pin(state, world.player)) + add_rule(world.get_location("Threaten King"), lambda state: has_pin(state, world.player)) + # special moves + total_queens = world.items_used[world.player].get("Progressive Major To Queen", 0) + add_rule(world.get_location("O-O Castle"), + lambda state: state.has("Progressive Major Piece", world.player, 2 + total_queens)) + add_rule(world.get_location("O-O-O Castle"), + lambda state: state.has("Progressive Major Piece", world.player, 2 + total_queens)) + # add_rule(multiworld.get_location("French Move", player), lambda state: state.has_french_move(player)) diff --git a/worlds/checksmate/__init__.py b/worlds/checksmate/__init__.py new file mode 100644 index 000000000000..51f0b51ef04d --- /dev/null +++ b/worlds/checksmate/__init__.py @@ -0,0 +1,696 @@ +import logging +import math +from collections import Counter +from enum import Enum +from typing import List, Dict, ClassVar, Callable, Type, Union + +from BaseClasses import Tutorial, Region, MultiWorld, Item, CollectionState +from Options import PerGameCommonOptions +from worlds.AutoWorld import WebWorld, World +from .Options import CMOptions, piece_type_limit_options, piece_limit_options +from .Items import (CMItem, item_table, filler_items, progression_items, + useful_items, item_name_groups, CMItemData) +from .Locations import CMLocation, location_table, highest_chessmen_requirement_small, highest_chessmen_requirement, \ + Tactic +from .Presets import checksmate_option_presets +from .Rules import set_rules, determine_difficulty, determine_relaxation, determine_min_material, determine_max_material + + +class CMWeb(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the ChecksMate software on your computer. This guide covers single-player, " + "multiworld, and related software.", + "English", + "checksmate_en.md", + "checks-mate/en", + ["roty", "rft50"] + )] + + options_presets = checksmate_option_presets + + +class CMWorld(World): + """ + ChecksMate is a game where you play chess, but all of your pieces were scattered across the multiworld. + You win when you checkmate the opposing king! + """ + game: ClassVar[str] = "ChecksMate" + data_version = 0 + web = CMWeb() + required_client_version = (0, 2, 11) + options_dataclass: ClassVar[Type[PerGameCommonOptions]] = CMOptions + options: CMOptions + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = {name: data.code for name, data in location_table.items()} + locked_locations: List[str] + + item_name_groups = item_name_groups + items_used: Dict[int, Dict[str, int]] = {} + items_remaining: Dict[int, Dict[str, int]] = {} + armies: Dict[int, List[int]] = {} + + item_pool: List[CMItem] = [] + prefill_items: List[CMItem] = [] + + known_pieces = {"Progressive Minor Piece": 10, "Progressive Major Piece": 6, "Progressive Major To Queen": 6, } + + piece_types_by_army: Dict[int, Dict[str, int]] = { + # Vanilla + 0: {"Progressive Minor Piece": 2, "Progressive Major Piece": 1, "Progressive Major To Queen": 1}, + # Colorbound Clobberers (the War Elephant is rather powerful) + 1: {"Progressive Minor Piece": 1, "Progressive Major Piece": 2, "Progressive Major To Queen": 1}, + # Remarkable Rookies + 2: {"Progressive Minor Piece": 2, "Progressive Major Piece": 1, "Progressive Major To Queen": 1}, + # Nutty Knights (although the Short Rook and Half Duck swap potency) + 3: {"Progressive Minor Piece": 2, "Progressive Major Piece": 1, "Progressive Major To Queen": 1}, + # Eurasian pieces + 4: {"Progressive Minor Piece": 2, "Progressive Major Piece": 1, "Progressive Major To Queen": 1}, + # Camel pieces + 5: {"Progressive Minor Piece": 2, "Progressive Major Piece": 1, "Progressive Major To Queen": 1}, + } + + def __init__(self, multiworld: MultiWorld, player: int) -> None: + super(CMWorld, self).__init__(multiworld, player) + self.locked_locations = [] + + # TODO: this probably can go in some other method now?? + def generate_early(self) -> None: + piece_collection = self.options.fairy_chess_pieces.value + army_options = [] + if piece_collection == self.options.fairy_chess_pieces.option_fide: + army_options = [0] + elif piece_collection == self.options.fairy_chess_pieces.option_betza: + army_options = [0, 1, 2, 3] + elif piece_collection == self.options.fairy_chess_pieces.option_full: + army_options = [0, 1, 2, 3, 4, 5] + elif piece_collection == self.options.fairy_chess_pieces.option_configure: + which_pieces = self.options.fairy_chess_pieces_configure + # TODO: I am not ok with this + if (which_pieces.value is None or which_pieces.value == 'None' or + None in which_pieces.value or 'None' in which_pieces.value): + raise Exception( + "This ChecksMate YAML is invalid! Add text after fairy_chess_piece_collection_configure.") + # FIDE: Contains the standard chess pieces, consisting of the Bishop, Knight, Rook, and Queen. + if "FIDE" in which_pieces.value: + army_options += [0] + # CwDA: Adds the pieces from Ralph Betza's 12 Chess With Different Armies. + if "Clobberers" in which_pieces.value: + army_options += [1] + if "Rookies" in which_pieces.value: + army_options += [2] + if "Nutty" in which_pieces.value: + army_options += [3] + # Cannon: Adds the Rook-like Cannon, which captures a distal chessman by leaping over an intervening + # chessman, and the Vao, a Bishop-like Cannon, in that it moves and captures diagonally. + if "Cannon" in which_pieces.value: + army_options += [4] + # Camel: Adds a custom army themed after 3,x leapers like the Camel (3,1) and Tribbabah (3,0). + if "Camel" in which_pieces.value: + army_options += [5] + # An empty set disables fairy chess pieces completely. + if not army_options: + army_options = [0] + army_constraint = self.options.fairy_chess_army + if army_constraint != self.options.fairy_chess_army.option_chaos: + self.armies[self.player] = [self.random.choice(army_options)] + else: + self.armies[self.player] = army_options + + def fill_slot_data(self) -> dict: + cursed_knowledge = {name: self.random.getrandbits(31) for name in [ + "pocket_seed", "pawn_seed", "minor_seed", "major_seed", "queen_seed"]} + potential_pockets = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2] + self.random.shuffle(potential_pockets) + cursed_knowledge["pocket_order"] = potential_pockets + cursed_knowledge["total_queens"] = self.items_used[self.player].get("Progressive Major To Queen", 0) + if self.player in self.armies: + cursed_knowledge["army"] = self.armies[self.player] + # See Archipelago.APChessV.ApmwConfig#Instantiate to observe requested parameters + option_names = ["goal", "difficulty", "piece_locations", "piece_types", + "fairy_chess_army", "fairy_chess_pieces", "fairy_chess_pieces_configure", "fairy_chess_pawns", + "minor_piece_limit_by_type", "major_piece_limit_by_type", "queen_piece_limit_by_type", + "pocket_limit_by_pocket"] + return dict(cursed_knowledge, **self.options.as_dict(*option_names)) + + def create_item(self, name: str) -> CMItem: + data = item_table[name] + return CMItem(name, data.classification, data.code, self.player) + + def set_rules(self) -> None: + set_rules(self) + + def create_items(self) -> None: + super_sized = self.options.goal.value != self.options.goal.option_single + # for enemy_pawn in self.item_name_groups["Enemy Pawn"]: + # self.multiworld.push_precollected(self.create_item(enemy_pawn)) + # for enemy_piece in self.item_name_groups["Enemy Piece"]: + # self.multiworld.push_precollected(self.create_item(enemy_piece)) + + # TODO: limit total material + # items = [[self.create_item(item) for _ in range(item_data.quantity)] + # for item, item_data in progression_items] + excluded_items = self.get_excluded_items() + self.items_used[self.player] = {} + self.items_remaining[self.player] = {} + + # setup for starting_inventory generic collection and then for early_material custom option + for item_name in excluded_items: + if item_name not in self.items_used[self.player]: + self.items_used[self.player][item_name] = 0 + self.items_used[self.player][item_name] += excluded_items[item_name] + starter_items = self.assign_starter_items(excluded_items, self.locked_locations) + for item in starter_items: + self.consume_item(item.name, {}) + + # determine how many items we need to add to the custom item_pool + user_location_count = len(starter_items) + user_location_count += 1 # Victory item is counted as part of the pool + items = [] + if self.options.goal.value == self.options.goal.option_progressive: + items.append(self.create_item("Super-Size Me")) + items.append(self.create_item("Play as White")) + self.items_used[self.player]["Play as White"] = 1 + + # find the material value the user's army should provide once fully collected + material = sum([ + progression_items[item].material * self.items_used[self.player][item] + for item in self.items_used[self.player] if item in progression_items]) + min_material = determine_min_material(self.options) + max_material = determine_max_material(self.options) + if super_sized: + endgame_multiplier = (location_table["Checkmate Maxima"].material_expectations_grand / + location_table["Checkmate Minima"].material_expectations_grand) + min_material *= endgame_multiplier + max_material *= endgame_multiplier + + # remove items player does not want + # TODO: tie these "magic numbers" to the corresponding Item.quantity + self.items_used[self.player]["Progressive Consul"] = ( + self.items_used[self.player].get("Progressive Consul", 0) + + (3 - self.options.max_kings.value)) + self.items_used[self.player]["Progressive King Promotion"] = ( + self.items_used[self.player].get("Progressive King Promotion", 0) + + (2 - self.options.fairy_kings.value)) + self.items_used[self.player]["Progressive Engine ELO Lobotomy"] = ( + self.items_used[self.player].get("Progressive Engine ELO Lobotomy", 0) + + (5 - self.options.max_engine_penalties.value)) + self.items_used[self.player]["Progressive Pocket"] = ( + self.items_used[self.player].get("Progressive Pocket", 0) + + (12 - min(self.options.max_pocket.value, 3 * self.options.pocket_limit_by_pocket.value))) + # if self.options.goal.value != self.options.goal.option_progressive: + self.items_used[self.player]["Super-Size Me"] = 1 + + # add items player really wants + yaml_locked_items: Dict[str, int] = self.options.locked_items.value + locked_items = dict(yaml_locked_items) + # ensure locked items have enough parents + player_queens: int = (locked_items.get("Progressive Major To Queen", 0) + + self.items_used[self.player].get("Progressive Major To Queen", 0)) + locked_items["Progressive Major Piece"] = max( + player_queens, locked_items.get("Progressive Major Piece", 0)) + # ensure castling + if self.options.accessibility.value != self.options.accessibility.option_minimal: + required_majors: int = 2 - self.items_used[self.player].get("Progressive Major Piece", 0) + player_queens + locked_items["Progressive Major Piece"] = max( + required_majors, locked_items.get("Progressive Major Piece", 0)) + # TODO(chesslogic): I can instead remove items from locked_items during the corresponding loop, until we would + # reach min_material by adding the remaining contents of locked_items. We would also need to check remaining + # locations, e.g. because the locked_items might contain some filler items like Progressive Pocket Range. + remaining_material = sum([locked_items[item] * progression_items[item].material for item in locked_items]) + logging.debug(str(self.player) + " pre-fill granted total material of " + str(material + remaining_material) + + " toward " + str(max_material) + " via items " + str(self.items_used[self.player]) + + " having set " + str(starter_items) + " and generated " + str(Counter(items))) + + my_progression_items = list(progression_items.keys()) + self.items_remaining[self.player] = { + name: progression_items[name].quantity - self.items_used.get(name, 0) for name in my_progression_items} + + # prevent victory event from being added to general pool + my_progression_items.remove("Victory") + + # more pawn chance + my_progression_items.append("Progressive Pawn") + my_progression_items.append("Progressive Pawn") + # I am proud of this feature, so I want players to see more of it. Fight me. + my_progression_items.append("Progressive Pocket") + my_progression_items.append("Progressive Pocket") + # halve chance of queen promotion - with an equal distribution, user will end up with no majors and only queens + my_progression_items.extend([item for item in my_progression_items if item != "Progressive Major To Queen"]) + # add an extra minor piece... there are so many types ... + my_progression_items.append("Progressive Minor Piece") + my_progression_items.append("Progressive Pawn") + + # items are now in a distribution of 1 queen:1 major:3 minor:7 pawn:6 pocket (material 9 + 5 + 9 + 7 + 6 = 36) + # note that queens require that a major precede them, which increases the likelihood of the other types + + max_items = len(location_table) + if not super_sized: + max_items -= len([loc for loc in location_table if location_table[loc].material_expectations == -1]) + if self.options.enable_tactics.value == self.options.enable_tactics.option_none: + max_items -= len([loc for loc in location_table if location_table[loc].is_tactic is not None]) + elif self.options.enable_tactics.value == self.options.enable_tactics.option_turns: + max_items -= len([loc for loc in location_table if location_table[loc].is_tactic == Tactic.Fork]) + + while ((len(items) + user_location_count + sum(locked_items.values())) < max_items and + material < max_material and len(my_progression_items) > 0): + chosen_item = self.random.choice(my_progression_items) + # obey user's wishes + if (self.should_remove(chosen_item, material, min_material, max_material, + items, my_progression_items, locked_items)): + my_progression_items.remove(chosen_item) + continue + # add item + if not self.has_prereqs(chosen_item): + continue + if self.can_add_more(chosen_item): + try_item = self.create_item(chosen_item) + was_locked = self.consume_item(chosen_item, locked_items) + items.append(try_item) + material += progression_items[chosen_item].material + if not was_locked: + self.lock_new_items(chosen_item, items, locked_items) + elif material >= min_material: + my_progression_items.remove(chosen_item) + all_material = sum([locked_items[item] * progression_items[item].material for item in locked_items]) + material + logging.debug(str(self.player) + " granted total material of " + str(all_material) + + " toward " + str(max_material) + " via items " + str(self.items_used[self.player]) + + " having generated " + str(Counter(items))) + + # exclude inaccessible locations + # (all locations should be accessible if accessibility != minimal) + # (I might change this later I guess, Archipelago is supposed to tolerate inaccessible items being swallowed up) + if self.options.accessibility.value == self.options.accessibility.option_minimal: + # castle + if (len([item for item in items if item.name == "Progressive Major Piece"]) < 2 + + len([item for item in items if item.name == "Progressive Major To Queen"])): + for location in ["O-O-O Castle", "O-O Castle"]: + if location not in self.options.exclude_locations.value: + self.options.exclude_locations.value.add(location) + # material + for location in location_table: + if location not in self.options.exclude_locations.value: + if material < location_table[location].material_expectations: + self.options.exclude_locations.value.add(location) + # chessmen + chessmen = chessmen_count(items, self.options.pocket_limit_by_pocket.value) + for location in location_table: + if location not in self.options.exclude_locations.value: + if chessmen < location_table[location].chessmen_expectations: + self.options.exclude_locations.value.add(location) + + my_useful_items = list(useful_items.keys()) + while ((len(items) + user_location_count + sum(locked_items.values())) < max_items and + len(my_useful_items) > 0): + chosen_item = self.random.choice(my_useful_items) + if not self.has_prereqs(chosen_item): + continue + if self.can_add_more(chosen_item): + self.consume_item(chosen_item, locked_items) + try_item = self.create_item(chosen_item) + items.append(try_item) + else: + my_useful_items.remove(chosen_item) + + my_filler_items = list(filler_items.keys()) + # get whether any item mentions "pocket" in the current items pool + has_pocket = False + for item in list(locked_items.keys()): + if "Pocket" in item: + has_pocket = True + break + if not has_pocket: + for item in items: + if "Pocket" in item.name: + has_pocket = True + break + # remove "pocket" fillers from the list of filler items + if not has_pocket: + my_filler_items = [item for item in my_filler_items if "Pocket" not in item] + while (len(items) + user_location_count + sum(locked_items.values())) < max_items: + if len(my_filler_items) == 0: + my_filler_items = ["Progressive Pocket Gems"] + chosen_item = self.random.choice(my_filler_items) + if not has_pocket and not self.has_prereqs(chosen_item): + continue + if has_pocket or self.can_add_more(chosen_item): + self.consume_item(chosen_item, locked_items) + try_item = self.create_item(chosen_item) + items.append(try_item) + else: + my_filler_items.remove(chosen_item) + + # TODO: Check that there are enough chessmen. (Player may have locked too much material.) + for item in locked_items: + if item not in self.items_used[self.player]: + self.items_used[self.player][item] = 0 + self.items_used[self.player][item] += locked_items[item] + items.extend([self.create_item(item) for i in range(locked_items[item])]) + material += progression_items[item].material * locked_items[item] + + self.multiworld.itempool += items + + def consume_item(self, chosen_item: str, locked_items: Dict[str, int]) -> bool: + if chosen_item not in self.items_used[self.player]: + self.items_used[self.player][chosen_item] = 0 + self.items_used[self.player][chosen_item] += 1 + if chosen_item in self.items_remaining[self.player]: + self.items_remaining[self.player][chosen_item] -= 1 + if self.items_remaining[self.player][chosen_item] <= 0: + del (self.items_remaining[self.player][chosen_item]) + if chosen_item in locked_items and locked_items[chosen_item] > 0: + locked_items[chosen_item] -= 1 + if locked_items[chosen_item] <= 0: + del (locked_items[chosen_item]) + return True + return False + + # this method assumes we cannot run out of pawns... we don't support excluded_items{pawn} + # there is no maximum number of chessmen... just minimum chessmen and maximum material. + def should_remove(self, + chosen_item: str, + material: int, + min_material: float, + max_material: float, + items: List[CMItem], + my_progression_items: List[Union[str, CMItemData]], + locked_items: Dict[str, int]) -> bool: + if chosen_item == "Progressive Major To Queen" and "Progressive Major Piece" not in my_progression_items: + # TODO: there is a better way, probably next step is a "one strike" mechanism + return True + + if self.items_used[self.player].get(chosen_item, 0) >= item_table[chosen_item].quantity: + return True + if not self.under_piece_limit(chosen_item, self.PieceLimitCascade.POTENTIAL_CHILDREN): + return True + + chosen_material = self.lockable_material_value(chosen_item, items, locked_items) + remaining_material = sum([locked_items[item] * progression_items[item].material for item in locked_items]) + total_material = material + chosen_material + remaining_material + exceeds_max = total_material > max_material + + if self.options.accessibility.value == self.options.accessibility.option_minimal: + enough_yet = material + remaining_material >= min_material + return exceeds_max and enough_yet + + chessmen_requirement = highest_chessmen_requirement_small if \ + self.options.goal.value == self.options.goal.option_single else \ + highest_chessmen_requirement + necessary_chessmen = (chessmen_requirement - chessmen_count(items, self.options.pocket_limit_by_pocket.value)) + if chosen_item in item_name_groups["Chessmen"]: + necessary_chessmen -= 1 + elif chosen_item == "Progressive Pocket": + # We know pocket_limit > 0, because pockets are not disabled, because we checked items_used + pocket_limit = self.options.pocket_limit_by_pocket.value + next_pocket = locked_items.get("Progressive Pocket", 0) + \ + len([item for item in items if item.name == "Progressive Pocket"]) + \ + 1 + if next_pocket % pocket_limit == 1: + necessary_chessmen -= 1 + if necessary_chessmen <= 0: + return exceeds_max + minimum_possible_material = total_material + ( + item_table["Progressive Pawn"].material * necessary_chessmen) + return minimum_possible_material > max_material + + # if this piece was added, it might add more than its own material to the locked pool + def lockable_material_value(self, chosen_item: str, items: List[CMItem], locked_items: Dict[str, int]): + material = progression_items[chosen_item].material + if self.options.accessibility.value == self.options.accessibility.option_minimal: + return material + if chosen_item == "Progressive Major To Queen" and self.unupgraded_majors_in_pool(items, locked_items) <= 2: + material += progression_items["Progressive Major Piece"].material + return material + + # ensures the Castling location is reachable + def lock_new_items(self, chosen_item: str, items: List[CMItem], locked_items: Dict[str, int]) -> None: + if self.options.accessibility.value == self.options.accessibility.option_minimal: + return + if chosen_item == "Progressive Major To Queen": + if self.unupgraded_majors_in_pool(items, locked_items) < 2: + if "Progressive Major Piece" not in locked_items: + locked_items["Progressive Major Piece"] = 0 + locked_items["Progressive Major Piece"] += 1 + + def unupgraded_majors_in_pool(self, items: List[CMItem], locked_items: Dict[str, int]) -> int: + total_majors = len([item for item in items if item.name == "Progressive Major Piece"]) + len( + [item for item in locked_items if item == "Progressive Major Piece"]) + total_upgrades = len([item for item in items if item.name == "Progressive Major To Queen"]) + len( + [item for item in locked_items if item == "Progressive Major To Queen"]) + + return total_majors - total_upgrades + + def create_regions(self) -> None: + region = Region("Menu", self.player, self.multiworld) + for loc_name in location_table: + loc_data = location_table[loc_name] + if self.options.goal.value == self.options.goal.option_single: + if loc_data.material_expectations == -1: + continue + if self.options.enable_tactics.value == self.options.enable_tactics.option_none: + if loc_data.is_tactic: + continue + if self.options.enable_tactics.value == self.options.enable_tactics.option_turns: + if loc_data.is_tactic == Tactic.Fork: + continue + region.locations.append(CMLocation(self.player, loc_name, loc_data.code, region)) + self.multiworld.regions.append(region) + + def get_filler_item_name(self) -> str: + if self.player not in self.items_used: + return "Progressive Pawn Forwardness" + if self.items_used[self.player].get("Progressive Pocket", 0) > 0 and \ + self.items_used[self.player].get("Progressive Pocket Range", 0) < \ + item_table["Progressive Pocket Range"].quantity: + return "Progressive Pocket Range" + if self.items_used[self.player].get("Progressive Pawn Forwardness", 0) < \ + min(item_table["Progressive Pawn Forwardness"].quantity, + item_table["Progressive Pawn Forwardness"].parents[0][1] + * self.items_used[self.player].get("Progressive Pawn", 0)): + return "Progressive Pawn Forwardness" + return "Progressive Pocket Gems" + + def generate_basic(self) -> None: + if self.options.goal.value == self.options.goal.option_single: + victory_item = self.create_item("Victory") + self.multiworld.get_location("Checkmate Minima", self.player).place_locked_item(victory_item) + else: + victory_item = self.create_item("Victory") + self.multiworld.get_location("Checkmate Maxima", self.player).place_locked_item(victory_item) + + def fewest_parents(self, parents: List[List[Union[str, int]]]): + # TODO: this concept doesn't work if a parent can have multiple children and another can't, e.g. forwardness + return min([self.items_used[self.player].get(item[0], 0) for item in parents]) + + def has_prereqs(self, chosen_item: str) -> bool: + parents = get_parents(chosen_item) + if parents: + fewest_parents = self.fewest_parents(parents) * get_parents(chosen_item)[0][1] + if fewest_parents is None: + return True + enough_parents = fewest_parents > self.items_used[self.player].get(chosen_item, 0) + if not enough_parents: + return False + return self.under_piece_limit(chosen_item, self.PieceLimitCascade.ACTUAL_CHILDREN) + + class PieceLimitCascade(Enum): + NO_CHILDREN = 1 + ACTUAL_CHILDREN = 2 + POTENTIAL_CHILDREN = 3 + + def can_add_more(self, chosen_item: str) -> bool: + if not self.under_piece_limit(chosen_item, self.PieceLimitCascade.POTENTIAL_CHILDREN): + return False + return chosen_item not in self.items_used[self.player] or \ + item_table[chosen_item].quantity == -1 or \ + self.items_used[self.player][chosen_item] < item_table[chosen_item].quantity + + def under_piece_limit(self, chosen_item: str, with_children: PieceLimitCascade) -> bool: + if self.player not in self.items_used: + # this can be the case during push_precollected + return True + piece_limit = self.find_piece_limit(chosen_item, with_children) + pieces_used = self.items_used[self.player].get(chosen_item, 0) + if 0 < piece_limit <= pieces_used: + # Intentionally ignore "parents" property: player might receive parent items after all children + # The player ending up with bounded parents on the upper end is handled in has_prereqs + return False + # Limit pieces placed by total number + if chosen_item in piece_limit_options: + piece_total_limit = piece_limit_options[chosen_item](self.options).value + pieces_used = self.items_used[self.player].get(chosen_item, 0) + if 0 < piece_total_limit <= pieces_used: + return False + return True + + def find_piece_limit(self, chosen_item: str, with_children: PieceLimitCascade) -> int: + """Limit pieces placed by individual variety. This applies the Piece Type Limit setting.""" + if chosen_item not in piece_type_limit_options: + return 0 + + piece_limit: int = self.piece_limit_of(chosen_item) + army_piece_types = { + piece: sum([self.piece_types_by_army[army][piece] for army in self.armies[self.player]]) + for piece in set().union(*self.piece_types_by_army.values())} + limit_multiplier = get_limit_multiplier_for_item(army_piece_types) + piece_limit = piece_limit * limit_multiplier(chosen_item) + if piece_limit > 0 and with_children != self.PieceLimitCascade.NO_CHILDREN: + children = get_children(chosen_item) + if children: + if with_children == self.PieceLimitCascade.ACTUAL_CHILDREN: + piece_limit = piece_limit + sum([self.items_used[self.player].get(child, 0) for child in children]) + elif with_children == self.PieceLimitCascade.POTENTIAL_CHILDREN: + piece_limit = piece_limit + sum([self.find_piece_limit(child, with_children) for child in children]) + return piece_limit + + def piece_limit_of(self, chosen_item: str): + return piece_type_limit_options[chosen_item](self.options).value + + def get_excluded_items(self) -> Dict[str, int]: + excluded_items: Dict[str, int] = {} + + if self.options.goal.value == self.options.goal.option_super: + item = self.create_item("Super-Size Me") + self.multiworld.push_precollected(item) + for item in self.multiworld.precollected_items[self.player]: + if item.name not in excluded_items: + excluded_items[item.name] = 0 + excluded_items[item.name] += 1 + + # excluded_items_option = getattr(multiworld, 'excluded_items', {player: []}) + + # excluded_items.update(excluded_items_option[player].value) + + return excluded_items + + def assign_starter_items(self, excluded_items: Dict[str, int], + locked_locations: List[str]) -> List[Item]: + multiworld = self.multiworld + player = self.player + cmoptions: CMOptions = self.options + non_local_items = self.options.non_local_items.value + early_material_option = cmoptions.early_material.value + + user_items = [] + if self.options.goal.value == self.options.goal.option_ordered_progressive: + item = self.create_item("Super-Size Me") + multiworld.get_location("Checkmate Minima", player).place_locked_item(item) + locked_locations.append("Checkmate Minima") + user_items.append(item) + if early_material_option > 0: + early_units = [] + if early_material_option == 1 or early_material_option > 4: + early_units.append("Progressive Pawn") + if early_material_option == 2 or early_material_option > 3: + early_units.append("Progressive Minor Piece") + if early_material_option > 2: + early_units.append("Progressive Major Piece") + local_basic_unit = sorted(item for item in early_units if + item not in non_local_items and ( + item not in excluded_items or + excluded_items[item] < item_table[item].quantity)) + if not local_basic_unit: + raise Exception("At least one early chessman must be local") + + item = self.create_item(self.random.choice(local_basic_unit)) + multiworld.get_location("King to E2/E7 Early", player).place_locked_item(item) + locked_locations.append("King to E2/E7 Early") + user_items.append(item) + + return user_items + + def collect(self, state: CollectionState, item: Item) -> bool: + material = 0 + item_count = state.prog_items[self.player][item.name] + # check if there are existing unused upgrades to this piece which are immediately satisfied + children = get_children(item.name) + for child in children: + if item_table[child].material == 0: + continue + # TODO: when a child could have multiple parents, check that this is also the least parent + if item_count < state.prog_items[self.player][child]: + # we had an upgrade, so add that upgrade to the material count + material += item_table[child].material + logging.debug("Adding child " + child + " having count: " + str(state.prog_items[self.player][child])) + else: + # not immediately upgraded, but maybe later + logging.debug("Added item " + item.name + " had insufficient children " + child + " to upgrade it") + # check if this is an upgrade which is immediately satisfied by applying it to an existing piece + parents = get_parents(item.name) + if len(parents) == 0 or item_table[item.name].material == 0: + # this is a root element (like a piece), not an upgrade, so we can use it immediately + material += item_table[item.name].material + else: + # this is an upgrade, so we can only apply it if it can find an unsatisfied parent + fewest_parents = min([state.prog_items[self.player].get(parent[0], 0) for parent in parents]) + # TODO: when a parent could have multiple children, check that this is also the least child + if item_count < fewest_parents * parents[0][1]: + # found a piece we could upgrade, so apply the upgrade + material += item_table[item.name].material + logging.debug("Item " + item.name + " had sufficient parents " + str(fewest_parents) + " to be tried") + else: + # not upgrading anything, but maybe later + logging.debug("Added item " + item.name + " had insufficient parents " + str(fewest_parents)) + change = super().collect(state, item) + if change: + # we actually collected the item, so we must gain the material + state.prog_items[self.player]["Material"] += material + logging.debug("Adding " + item.name + " with " + str(state.prog_items[self.player].get("Material", 0)) + + " having " + str(state.prog_items)) + return change + + # TODO: extremely similar - refactor to pass lt/lte comparator and an arithmetic lambda +/- + def remove(self, state: CollectionState, item: Item) -> bool: + # if state.prog_items[self.player].get(item.name, 0) == 0: + # return False # TODO(chesslogic): I think this should be base behaviour, and doing this probably breaks it + material = 0 + item_count = state.prog_items[self.player][item.name] + children = get_children(item.name) + for child in children: + if item_table[child].material == 0: + continue + # TODO: when a child could have multiple parents, check that this is also the least parent + if item_count <= state.prog_items[self.player][child]: + material -= item_table[child].material + logging.debug("Removing child " + child + " having count: " + str(state.prog_items[self.player][child])) + else: + logging.debug("Removed item " + item.name + " had insufficient children " + child) + parents = get_parents(item.name) + if len(parents) == 0 or item_table[item.name].material == 0: + material -= item_table[item.name].material + else: + fewest_parents = min([state.prog_items[self.player].get(parent[0], 0) for parent in parents]) + if item_count <= fewest_parents: + material -= item_table[item.name].material + logging.debug("Item " + item.name + " had sufficient parents " + str(fewest_parents) + " to be removed") + else: + logging.debug("Removed item " + item.name + " had insufficient parents " + str(fewest_parents)) + change = super().remove(state, item) + if change: + state.prog_items[self.player]["Material"] += material + logging.debug("Removing " + item.name + " with " + str(state.prog_items[self.player].get("Material", 0)) + + " having " + str(state.prog_items)) + return change + + +def get_limit_multiplier_for_item(item_dictionary: Dict[str, int]) -> Callable[[str], int]: + return lambda item_name: item_dictionary[item_name] + + +def get_parents(chosen_item: str) -> List[List[Union[str, int]]]: + return item_table[chosen_item].parents + + +def get_children(chosen_item: str) -> List[str]: + return [item for item in item_table + if item_table[item].parents is not None and chosen_item in map( + lambda x: x[0], item_table[item].parents)] + + +def chessmen_count(items: List[CMItem], pocket_limit: int) -> int: + pocket_amount = (0 if pocket_limit <= 0 else + math.ceil(len([item for item in items if item.name == "Progressive Pocket"]) / pocket_limit)) + chessmen_amount = len([item for item in items if item.name in item_name_groups["Chessmen"]]) + logging.debug("Found {} chessmen and {} pocket men".format(chessmen_amount, pocket_amount)) + return chessmen_amount + pocket_amount diff --git a/worlds/checksmate/docs/checksmate-example.yaml b/worlds/checksmate/docs/checksmate-example.yaml new file mode 100644 index 000000000000..1515f6b71332 --- /dev/null +++ b/worlds/checksmate/docs/checksmate-example.yaml @@ -0,0 +1,129 @@ +description: 'Standard Chess pieces, moving in standard Chess ways, allowing many combinations of material' +name: NoDumbPieces +game: ChecksMate +ChecksMate: + early_material: off # this allows the generator to work out its own logical progression + fairy_chess_pieces: fide # this excludes all fairy chess pieces, although you may see many rooks, or no bishops, etc + fairy_chess_pawns: vanilla # this excludes Berolina pawns, so every pawn will be a traditional pawn + +--- + +description: 'A vanilla army with no pockets, comprising 2 Bishops+Knights+Rooks, and 1 Queen (or Rook until upgraded)' +name: Traditional +game: ChecksMate +ChecksMate: + goal: single + difficulty: daily + enable_tactics: all + early_material: pawn # not counted against locked_items (this may be changed) + max_pocket: 0 + fairy_chess_pieces: fide + fairy_chess_pawns: vanilla + minor_piece_limit_by_type: 2 + major_piece_limit_by_type: 2 + queen_piece_limit: 1 + locked_items: + Progressive Minor Piece: 4 + Progressive Major Piece: 3 + Progressive Major To Queen: 1 + +--- + +description: 'Horde Chess, but with random additional pieces' +name: OopsAllPawns +game: ChecksMate +ChecksMate: + difficulty: relaxed # raises material requirement for goal far beyond just 32 pawns, therefore, player will get a lot + early_material: pawn + fairy_chess_pieces: betza + fairy_chess_pawns: vanilla + pocket_limit_by_pocket: 1 + locked_items: + Progressive Pawn: 31 # the 32nd pawn is early material + Progressive Pocket: 3 + +--- + +description: 'Chaos and pocket pieces' +name: SleevedAce +game: ChecksMate +ChecksMate: + difficulty: daily + piece_types: chaos + early_material: major + fairy_chess_pieces: betza + fairy_chess_pawns: + vanilla: 1 + berolina: 1 + locked_items: + Progressive Pocket: 12 + +--- + +description: 'Weird Fairy Chess with opportunity to study the opening' +name: DifferentArmy +game: ChecksMate +ChecksMate: + difficulty: daily + piece_locations: stable + piece_types: stable + early_material: piece + fairy_chess_pieces: configure + fairy_chess_pieces_configure: + - Rookies + - Clobberers + - Nutty + - Camel + fairy_chess_army: stable + fairy_chess_pawns: + vanilla: 20 + mixed: 1 + berolina: 8 + +--- + +description: 'Many exotic royal pieces' +name: PowerCouples +game: ChecksMate +ChecksMate: + difficulty: daily + piece_types: chaos + early_material: major + max_kings: 3 + fairy_kings: 2 + fairy_chess_pieces: full + fairy_chess_army: chaos + fairy_chess_pawns: + vanilla: 1 + berolina: 1 + minor_piece_limit_by_type: 1 + major_piece_limit_by_type: 1 + queen_piece_limit_by_type: 1 + locked_items: + Progressive Consul: 2 + Progressive King Promotion: 2 + Progressive Major Piece: 2 # the 3rd is granted as Early Material! + Progressive Major To Queen: 3 + + +--- + +description: 'Skip the first half to learn the second half' +name: Endgame +game: ChecksMate +ChecksMate: + goal: super + difficulty: daily + piece_types: chaos + early_material: major + max_kings: 3 + fairy_kings: 1 + fairy_chess_pieces: betza + fairy_chess_army: chaos + fairy_chess_pawns: vanilla + start_inventory: + Progressive Consul: 1 + Progressive Pawn: 6 + Progressive Minor Piece: 2 + Progressive Major Piece: 2 + Progressive Major To Queen: 1 diff --git a/worlds/checksmate/docs/checksmate_en.md b/worlds/checksmate/docs/checksmate_en.md new file mode 100644 index 000000000000..21fe6cb00282 --- /dev/null +++ b/worlds/checksmate/docs/checksmate_en.md @@ -0,0 +1,55 @@ +# ChecksMate Chess Randomizer Setup Guide + +![A chess piece with several blurry opposing pieces in the background](https://i.imgur.com/fqng206.png) + +## Required Software + +- Any ChecksMate client. Currently, a modified ChessV client is supported and can be accessed via + its [GitHub releases page](https://github.com/chesslogic/chessv/releases/latest) (latest version) +- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases/latest) + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +Some releases of the ChecksMate client include an example YAML file demonstrating some supported options. + +### Where do I get a YAML file? + +You can customize your options by visiting the [ChecksMate Player Options Page](/games/ChecksMate/player-options) + +Some examples of certain outcomes are available in [this valid players file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/checksmate/docs/checksmate-example.yaml), which can be used +to generate a multiplayer multiworld (but should instead be used for your own inspiration). + +#### Material + +A normal (FIDE) army has 8 points of pawns plus 31 points of pieces (12 from 4 minor pieces, 10 from 2 rooks, and 9 from +1 queen). Material isn't everything: An army of 27 pawns plus 4 Knights is considered to be extremely powerful. +Conversely, having no pawns whatsoever opens your position dramatically, allowing your pieces to make very aggressive +moves and to maintain a very high tempo. + +### Generating a ChecksMate game + +**ChecksMate is a short game! You might restart many times, but you should expect no more than an hour of gameplay!** + +You need to start a ChecksMate client yourself, which are available from the [releases page](https://github.com/chesslogic/chessv/releases/latest). +Generally, these need to be extracted to a folder before they are run, due to a dependency on asset files and dynamic libraries. + +### Connect to the MultiServer + +First start ChecksMate. + +Once ChecksMate is started, in the client at the top type in the spot labeled `Server` type the `IP Address` and `Port` +separated with a `:` symbol. Then input your slot name in the next box. The third box can be used for any password, +and is often left empty. + +These connection settings will be saved in a simple text file for the next time you start the client. (You may safely +delete this convenience file.) + +### Play the game + +When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a +multiworld game! diff --git a/worlds/checksmate/docs/en_ChecksMate.md b/worlds/checksmate/docs/en_ChecksMate.md new file mode 100644 index 000000000000..e18d456673d7 --- /dev/null +++ b/worlds/checksmate/docs/en_ChecksMate.md @@ -0,0 +1,38 @@ +# ChecksMate Chess + +## Where is the options page? + +The [player options page for this game](../player-options) contains all the options you need to configure and export a +config file. + +## What is considered a location check in ChecksMate? + +Perform various feats of style, grace, and conquest. + + - Capture individual enemy pieces and pawns (e.g. capture pawn E, the pawn that begins on the E file) + - Capture multiple enemy pieces and pawns in 1 match (e.g. capture any 2 pawns), including sequences of pairs (e.g. both 2 pieces and 2 pawns) + - Attack (e.g. threaten) any opposing pawn, minor piece, major piece, or queen + - Attack multiple opposing pieces with a single piece (Sacrificial if it is itself attacked, True otherwise): two pieces, three pieces, and the King and Queen + - Move your King each of: forward one space, to the A file, to the center 4 squares, to the opposing home rank, and to capture a piece + - Perform the French move + +## When the player receives an item, what happens? + +The player will receive either: + + - The white pieces (permitting the player to make the first move) + - A piece of material, being a pawn, piece, or upgrade for a piece + - Engine Elo reduction, eagerly bringing the 2000+ Elo engine down to a beatable level + - Before calculation penalties are applied, the current supported engines have an approximate Elo (in an AI-only tournament) of at least 2030. See: https://www.computerchess.org.uk/ccrl/404/ + - Pawn forwardness - placing a random pawn on the 3rd rank rather than the 2nd + - A pocket piece, which can be played from one of your three pockets onto the board! + - Powerful pieces cannot be played onto the board at the start of the game. One must wait turns equal to their material value before playing such a piece + - In addition, the player may receive "Pocket Gems", which grant "turns passed" toward pocket pieces + - Finally, "Pocket Range" allows these pocket pieces to deploy onto ranks beyond the home rank, from 1st up to the 7th rank + - A Consul, adding an extra King piece. You lose when all of your Kings are captured + - A King Upgrade, where your primary King (not Consuls) becomes a Mounted King and gains Knight movement + - When you gain both King Upgrades, your primary King becomes a Hyper King and gains Nightrider (Knight slider) and Elephant (2x diagonal leaper) movement + +## What is the victory condition? + +Put the opposing King in checkmate. diff --git a/worlds/checksmate/test/TestMaterialState.py b/worlds/checksmate/test/TestMaterialState.py new file mode 100644 index 000000000000..5617676e9e04 --- /dev/null +++ b/worlds/checksmate/test/TestMaterialState.py @@ -0,0 +1,77 @@ +from copy import copy + +from . import CMTestBase +from .. import determine_difficulty + + +class MaterialStateTestBase(CMTestBase): + + def world_setup(self, *args, **kwargs) -> None: + self.options = copy(self.options) + self.options["goal"] = "single" + self.options["difficulty"] = "grandmaster" + super().world_setup(*args, **kwargs) + + # this class ultimately isn't trying to test this relatively simple function + self.difficulty = determine_difficulty(self.world.options) + + def test_basic_fill(self) -> None: + # this is mostly to demonstrate that collect fundamentally acquires the items and to show that setUp sets up + self.assertEqual(0, self.multiworld.state.prog_items[self.player]["Progressive Pawn"]) + self.assertEqual(0, self.multiworld.state.prog_items[self.player]["Material"]) + self.collect_all_but("Progressive Pocket Gems", self.multiworld.state) + self.assertEqual( + len([item for item in self.multiworld.itempool if item.name == "Progressive Pawn"]), + self.multiworld.state.prog_items[self.player]["Progressive Pawn"]) + + +class TestSimpleMaterial(MaterialStateTestBase): + """ + Checks that goal can be reached based on the math performed by collect() + + If this fails, it's not necessarily the fault of collect(), it might be that the generator isn't adding enough items + """ + def test_no_options(self) -> None: + self.collect_all_but("Progressive Pocket Gems", self.multiworld.state) + past_material = self.multiworld.state.prog_items[self.player]["Material"] + self.assertLessEqual(4150 * self.difficulty, past_material) + self.assertGreaterEqual(4650 * self.difficulty, past_material) + + +class TestCyclicMaterial(MaterialStateTestBase): + """Removes all material, then adds it back again. This tests remove() via sledgehammer method""" + def test_no_options(self) -> None: + self.collect_all_but("Progressive Pocket Gems", self.multiworld.state) + past_material = self.multiworld.state.prog_items[self.player]["Material"] + self.assertEqual(past_material, self.multiworld.state.prog_items[self.player]["Material"]) + self.assertLessEqual(4150 * self.difficulty, past_material) + self.assertGreaterEqual(4650 * self.difficulty, past_material) + + for item in list(self.multiworld.state.prog_items[self.player].keys()): + self.remove_by_name(item) + # self.assertEqual(0, self.multiworld.state.prog_items[self.player]) + self.assertEqual(0, self.multiworld.state.prog_items[self.player]["Progressive Pawn"]) + self.assertEqual(0, self.multiworld.state.prog_items[self.player]["Material"]) + self.collect_all_but("Progressive Pocket Gems", self.multiworld.state) + + self.assertEqual(past_material, self.multiworld.state.prog_items[self.player]["Material"]) + + """Same as before, but backward, to test "children" logic""" + def test_backward(self) -> None: + self.collect_all_but("Progressive Pocket Gems", self.multiworld.state) + past_material = self.multiworld.state.prog_items[self.player]["Material"] + self.assertEqual(past_material, self.multiworld.state.prog_items[self.player]["Material"]) + self.assertLessEqual(4150 * self.difficulty, past_material) + self.assertGreaterEqual(4650 * self.difficulty, past_material) + + items = list(self.multiworld.state.prog_items[self.player].keys()) + items.reverse() + for item in items: + self.remove_by_name(item) + # self.assertEqual(0, self.multiworld.state.prog_items[self.player]) + self.assertEqual(0, self.multiworld.state.prog_items[self.player]["Progressive Pawn"]) + self.assertEqual(0, self.multiworld.state.prog_items[self.player]["Material"]) + self.collect_all_but("Progressive Pocket Gems", self.multiworld.state) + + self.assertEqual(past_material, self.multiworld.state.prog_items[self.player]["Material"]) + diff --git a/worlds/checksmate/test/TestPieceLimits.py b/worlds/checksmate/test/TestPieceLimits.py new file mode 100644 index 000000000000..19b6ade089ed --- /dev/null +++ b/worlds/checksmate/test/TestPieceLimits.py @@ -0,0 +1,190 @@ +from copy import copy + +from . import CMTestBase +from .. import CMWorld, Options + + +# I don't like that this generates many entire seeds just to check some global logic. +# TODO(chesslogic): Convert as much of this as possible to use test.bases, not WorldTestBase. + +# TODO(chesslogic): find_piece_limit should accept an army option value. Store piece distribution on some other helper + + +class PieceLimitTestBase(CMTestBase): + NO_CHILDREN = CMWorld.PieceLimitCascade.NO_CHILDREN + ACTUAL_CHILDREN = CMWorld.PieceLimitCascade.ACTUAL_CHILDREN + POTENTIAL_CHILDREN = CMWorld.PieceLimitCascade.POTENTIAL_CHILDREN + + def world_setup(self, *args, **kwargs) -> None: + self.options = copy(self.options) + super().world_setup(*args, **kwargs) + + def assert_matches(self, expected_minors: int, expected_majors: int, expected_queens: int) -> None: + self.assertEqual(0, self.world.find_piece_limit("Progressive Pawn", self.NO_CHILDREN + )) + self.assertEqual(expected_minors, self.world.find_piece_limit("Progressive Minor Piece", self.NO_CHILDREN + )) + self.assertEqual(expected_majors, self.world.find_piece_limit("Progressive Major Piece", self.NO_CHILDREN + )) + self.assertEqual(expected_queens, self.world.find_piece_limit("Progressive Major To Queen", self.NO_CHILDREN + )) + + def assert_actuals(self, expected_majors, expected_queens) -> None: + actual_queens = self.world.items_used[self.player].get("Progressive Major To Queen", 0) + self.assertEqual(expected_majors + actual_queens, + self.world.find_piece_limit("Progressive Major Piece", self.ACTUAL_CHILDREN)) + self.assertEqual(expected_majors + expected_queens, + self.world.find_piece_limit("Progressive Major Piece", self.POTENTIAL_CHILDREN)) + + +class TestChaosPieceLimits(PieceLimitTestBase): + def world_setup(self, *args, **kwargs) -> None: + self.options = copy(self.options) + self.options["fairy_chess_army"] = Options.FairyChessArmy.option_chaos + super().world_setup(*args, **kwargs) + + def test_no_options(self) -> None: + # self.options["fairy_chess_army"] = "chaos" + expected_minors = 0 + expected_majors = 0 + expected_queens = 0 + self.assert_matches(expected_minors, expected_majors, expected_queens) + + +class TestChaosPieceLimitsOfVanilla(PieceLimitTestBase): + def world_setup(self, *args, **kwargs) -> None: + self.options = copy(self.options) + self.options["fairy_chess_pieces"] = Options.FairyChessPieces.option_full + self.options["fairy_chess_army"] = Options.FairyChessArmy.option_chaos + self.options["minor_piece_limit_by_type"] = 2 + self.options["major_piece_limit_by_type"] = 2 + self.options["queen_piece_limit_by_type"] = 1 + super().world_setup(*args, **kwargs) + + def test_limit(self) -> None: + expected_minors = 22 + expected_majors = 14 + expected_queens = 6 + self.assert_matches(expected_minors, expected_majors, expected_queens) + self.assert_actuals(expected_majors, expected_queens) + + +class TestChaosPieceLimitsOfOne(PieceLimitTestBase): + def world_setup(self, *args, **kwargs) -> None: + self.options = copy(self.options) + self.options["fairy_chess_pieces"] = Options.FairyChessPieces.option_full + self.options["fairy_chess_army"] = Options.FairyChessArmy.option_chaos + self.options["minor_piece_limit_by_type"] = 1 + self.options["major_piece_limit_by_type"] = 1 + self.options["queen_piece_limit_by_type"] = 1 + super().world_setup(*args, **kwargs) + + def test_limit(self) -> None: + expected_minors = 11 + expected_majors = 7 + expected_queens = 6 + self.assert_matches(expected_minors, expected_majors, expected_queens) + self.assert_actuals(expected_majors, expected_queens) + + +class TestChaosPieceLimitsOfTwo(PieceLimitTestBase): + def world_setup(self, *args, **kwargs) -> None: + self.options = copy(self.options) + self.options["fairy_chess_pieces"] = Options.FairyChessPieces.option_full + self.options["fairy_chess_army"] = Options.FairyChessArmy.option_chaos + self.options["minor_piece_limit_by_type"] = 2 + self.options["major_piece_limit_by_type"] = 2 + self.options["queen_piece_limit_by_type"] = 2 + super().world_setup(*args, **kwargs) + + def test_limit(self) -> None: + expected_minors = 22 + expected_majors = 14 + expected_queens = 12 + self.assert_matches(expected_minors, expected_majors, expected_queens) + self.assert_actuals(expected_majors, expected_queens) + + +class TestChaosPieceLimitsByVariety(PieceLimitTestBase): + def world_setup(self, *args, **kwargs) -> None: + self.options = copy(self.options) + self.options["fairy_chess_pieces"] = Options.FairyChessPieces.option_full + self.options["fairy_chess_army"] = Options.FairyChessArmy.option_chaos + self.options["minor_piece_limit_by_type"] = 5 + self.options["major_piece_limit_by_type"] = 1 + self.options["queen_piece_limit_by_type"] = 3 + super().world_setup(*args, **kwargs) + + def test_limit(self) -> None: + expected_minors = 55 + expected_majors = 7 + expected_queens = 18 + self.assert_matches(expected_minors, expected_majors, expected_queens) + self.assert_actuals(expected_majors, expected_queens) + + +class TestStablePieceLimits(PieceLimitTestBase): + def world_setup(self, *args, **kwargs) -> None: + self.options = copy(self.options) + self.options["fairy_chess_pieces"] = Options.FairyChessPieces.option_full + super().world_setup(*args, **kwargs) + + def test_no_options(self) -> None: + expected_minors = 0 + expected_majors = 0 + expected_queens = 0 + self.assert_matches(expected_minors, expected_majors, expected_queens) + + +class TestStablePieceLimitsOfVanilla(PieceLimitTestBase): + def world_setup(self, *args, **kwargs) -> None: + self.options = copy(self.options) + self.options["fairy_chess_pieces"] = Options.FairyChessPieces.option_fide + self.options["fairy_chess_army"] = Options.FairyChessArmy.option_stable + self.options["minor_piece_limit_by_type"] = 2 + self.options["major_piece_limit_by_type"] = 2 + self.options["queen_piece_limit_by_type"] = 1 + super().world_setup(*args, **kwargs) + + def test_limit(self) -> None: + expected_minors = 4 + expected_majors = 2 + expected_queens = 1 + self.assert_matches(expected_minors, expected_majors, expected_queens) + self.assert_actuals(expected_majors, expected_queens) + + +class TestStablePieceLimitsOfThree(PieceLimitTestBase): + def world_setup(self, *args, **kwargs) -> None: + self.options = copy(self.options) + self.options["fairy_chess_pieces"] = Options.FairyChessPieces.option_fide + self.options["fairy_chess_army"] = Options.FairyChessArmy.option_stable + self.options["minor_piece_limit_by_type"] = 3 + self.options["major_piece_limit_by_type"] = 3 + self.options["queen_piece_limit_by_type"] = 3 + super().world_setup(*args, **kwargs) + + def test_limit(self) -> None: + expected_minors = 6 + expected_majors = 3 + expected_queens = 3 + self.assert_matches(expected_minors, expected_majors, expected_queens) + self.assert_actuals(expected_majors, expected_queens) + + +class TestStablePieceLimitsByVariety(PieceLimitTestBase): + def world_setup(self, *args, **kwargs) -> None: + self.options = copy(self.options) + self.options["fairy_chess_pieces"] = Options.FairyChessPieces.option_fide + self.options["fairy_chess_army"] = Options.FairyChessArmy.option_stable + self.options["minor_piece_limit_by_type"] = 4 + self.options["major_piece_limit_by_type"] = 2 + self.options["queen_piece_limit_by_type"] = 3 + super().world_setup(*args, **kwargs) + + def test_limit(self) -> None: + expected_minors = 8 + expected_majors = 2 + expected_queens = 3 + self.assert_matches(expected_minors, expected_majors, expected_queens) + self.assert_actuals(expected_majors, expected_queens) diff --git a/worlds/checksmate/test/__init__.py b/worlds/checksmate/test/__init__.py new file mode 100644 index 000000000000..c5e45dd70967 --- /dev/null +++ b/worlds/checksmate/test/__init__.py @@ -0,0 +1,19 @@ +from typing import ClassVar +from copy import copy + +from test.bases import WorldTestBase +from .. import CMWorld + + +class CMTestBase(WorldTestBase): + game = "ChecksMate" + world: CMWorld + player: ClassVar[int] = 1 + + def world_setup(self, *args, **kwargs) -> None: + # TODO(chesslogic): Subclasses could implement a "modify_options" method + # to modify the options before the world is constructed + self.options = copy(self.options) + super().world_setup(*args, **kwargs) + if self.constructed: + self.world = self.multiworld.worlds[self.player] # noqa