From 9773eee2d6e35d3376a8bb629ff3ef5ca8af7fbf Mon Sep 17 00:00:00 2001
From: Joe R <joeraz5@verizon.net>
Date: Mon, 3 Mar 2025 21:28:35 -0500
Subject: [PATCH] Add Wizard's Storeroom game

---
 html-src/rules/bigstoreroom.html     | 13 ++++
 html-src/rules/wizardsstoreroom.html | 23 +++++++
 pysollib/gamedb.py                   |  3 +-
 pysollib/games/special/hexadeck.py   | 95 +++++++++++++++++++++++++++-
 4 files changed, 130 insertions(+), 4 deletions(-)
 create mode 100644 html-src/rules/bigstoreroom.html
 create mode 100644 html-src/rules/wizardsstoreroom.html

diff --git a/html-src/rules/bigstoreroom.html b/html-src/rules/bigstoreroom.html
new file mode 100644
index 00000000..b30a091f
--- /dev/null
+++ b/html-src/rules/bigstoreroom.html
@@ -0,0 +1,13 @@
+<h1>Big Storeroom</h1>
+<p>
+Hex A Deck type. 2 decks. 1 redeal.
+
+<h3>Object</h3>
+<p>
+Move all cards to the foundations.
+
+<h3>Rules</h3>
+<p>
+Like <a href="wizardsstoreroom.html">Wizard's Storeroom</a>
+but with two decks and nine piles.
+
diff --git a/html-src/rules/wizardsstoreroom.html b/html-src/rules/wizardsstoreroom.html
new file mode 100644
index 00000000..3c6f32a7
--- /dev/null
+++ b/html-src/rules/wizardsstoreroom.html
@@ -0,0 +1,23 @@
+<h1>Wizard's Storeroom</h1>
+<p>
+Hex A Deck type. 1 deck. 1 redeal.
+
+<h3>Object</h3>
+<p>
+Move all cards to the foundations.
+
+<h3>Rules</h3>
+<p>
+The tableau consists of one reserve stack and five row stacks.
+Fourteen cards are dealt to the reserve stack and one card each to the
+row stacks.  When a row stack is emptied it must be filled from the reserve
+stack first.  When the reserve stack is empty any card can be played on an
+empty row stack.  Wizards are wild - any card can be played on them, and they
+can be played on any card.  The Wizards have ranks like the suit cards
+corresponding to Ace through Four.  If a Wizard is played in it's proper rank
+position the row can still be moved, otherwise, the full sequence can't be moved.
+The row stacks build down in rank by alternate colors.
+<p>
+The foundations build up in rank by suit.  The Wizards will not move off of the
+tableau until all the other cards have been moved to the foundations.
+The game is won when all cards are moved to the foundations.
diff --git a/pysollib/gamedb.py b/pysollib/gamedb.py
index 0af79e36..893dbbb8 100644
--- a/pysollib/gamedb.py
+++ b/pysollib/gamedb.py
@@ -595,7 +595,8 @@ class GI:
          tuple(range(22353, 22361))),
         ('fc-3.1', tuple(range(961, 971))),
         ('dev', tuple(range(971, 978)) + tuple(range(5419, 5421)) +
-         tuple(range(18005, 18007)) + (44, 526,)),
+         tuple(range(16683, 16685)) + tuple(range(18005, 18007)) +
+         (44, 526,)),
     )
 
     # deprecated - the correct way is to or a GI.GT_XXX flag
diff --git a/pysollib/games/special/hexadeck.py b/pysollib/games/special/hexadeck.py
index 8e573dca..cac77f0c 100644
--- a/pysollib/games/special/hexadeck.py
+++ b/pysollib/games/special/hexadeck.py
@@ -62,7 +62,8 @@ class HexATrump_Foundation(HexADeck_FoundationStack):
     def acceptsCards(self, from_stack, cards):
         if not self.basicAcceptsCards(from_stack, cards):
             return 0
-        for s in self.game.s.foundations[:3]:
+        for s in self.game.s.foundations[:((self.game.gameinfo.decks
+                                            * 4) - 1)]:
             if len(s.cards) != 16:
                 return 0
         return 1
@@ -93,11 +94,12 @@ class Merlins_Foundation(AbstractFoundationStack):
 
 class HexADeck_OpenStack(OpenStack):
 
-    def __init__(self, x, y, game, yoffset, **cap):
+    def __init__(self, x, y, game, yoffset=None, **cap):
         kwdefault(cap, max_move=UNLIMITED_MOVES, max_accept=UNLIMITED_ACCEPTS,
                   dir=-1)
         OpenStack.__init__(self, x, y, game, **cap)
-        self.CARD_YOFFSET = yoffset
+        if yoffset is not None:
+            self.CARD_YOFFSET = yoffset
 
     def isRankSequence(self, cards, dir=None):
         if not dir:
@@ -1600,6 +1602,89 @@ class MagicMontana(Montana):
         return True
 
 
+# ************************************************************************
+# * Wizard's Storeroom
+# ************************************************************************
+
+class WizardsStoreroom(AbstractHexADeckGame):
+    MAX_ROUNDS = 2
+
+    #
+    # Game layout
+    #
+
+    def createGame(self):
+        l, s = Layout(self), self.s
+
+        # Set window size
+        decks = self.gameinfo.decks
+        self.setSize(2*l.XM + (2 + 5*decks)*l.XS, 3*l.YM + 5*l.YS)
+        yoffset = min(l.YOFFSET, max(10, l.YOFFSET // 2))
+
+        # Create talon
+        x = l.XM
+        y = l.YM
+        s.talon = WasteTalonStack(
+            x, y, self, num_deal=1, max_rounds=self.MAX_ROUNDS)
+        l.createText(s.talon, "s")
+        x = x + l.XS
+        s.waste = WasteStack(x, y, self)
+        l.createText(s.waste, "s")
+
+        # Create foundations
+        x = x + l.XM + l.XS
+        for j in range(4):
+            for i in range(decks):
+                s.foundations.append(
+                    SS_FoundationStack(x, y, self, j, max_cards=16))
+                x = x + l.XS
+        for i in range(decks):
+            s.foundations.append(
+                HexATrump_Foundation(x, y, self, 4, max_cards=4))
+            x = x + l.XS
+
+        # Create reserve
+        x = l.XM
+        y = l.YM + l.YS + l.TEXT_HEIGHT
+        s.reserves.append(OpenStack(x, y, self))
+        s.reserves[0].CARD_YOFFSET = (l.YOFFSET, yoffset)[decks == 2]
+
+        # Create rows
+        x = x + l.XM + l.XS
+        for i in range(4*decks+1):
+            s.rows.append(HexAKlon_RowStack(x, y, self))
+            x = x + l.XS
+        self.setRegion(s.rows, (-999, y - l.YS, 999999, 999999))
+
+        # Define stack groups
+        l.defaultStackGroups()
+
+    #
+    # Game over rides
+    #
+
+    def startGame(self):
+        decks = self.gameinfo.decks
+        self.startDealSample()
+        for i in range(14 * decks):
+            self.s.talon.dealRow(rows=self.s.reserves, flip=0, frames=4)
+        self.s.reserves[0].flipMove()
+        self.s.talon.dealRow(rows=self.s.rows)
+        self.s.talon.dealCards()          # deal first card to WasteStack
+
+    def fillStack(self, stack):
+        r = self.s.reserves[0]
+        if not stack.cards and stack in self.s.rows:
+            if r.cards and stack.acceptsCards(r, r.cards[-1:]):
+                r.moveMove(1, stack)
+        if r.canFlipCard():
+            r.flipMove()
+
+    def shallHighlightMatch(self, stack1, card1, stack2, card2):
+        return ((card1.rank + 1 == card2.rank or
+                card1.rank - 1 == card2.rank) and
+                card1.color != card2.color)
+
 # ************************************************************************
 # *
 # ************************************************************************
@@ -1646,4 +1731,8 @@ r(16680, Snakestone, 'Snakestone', GI.GT_HEXADECK | GI.GT_OPEN, 2, 0,
 r(16681, HexYukon, 'Hex Yukon', GI.GT_HEXADECK, 1, 0, GI.SL_BALANCED)
 r(16682, MagicMontana, 'Magic Montana', GI.GT_HEXADECK | GI.GT_OPEN, 1, 2,
   GI.SL_MOSTLY_SKILL)
+r(16683, WizardsStoreroom, "Wizard's Storeroom", GI.GT_HEXADECK, 1, 1,
+  GI.SL_MOSTLY_SKILL)
+r(16684, WizardsStoreroom, "Big Storeroom", GI.GT_HEXADECK, 2, 1,
+  GI.SL_MOSTLY_SKILL)
 del r