From c2bc75caf7db6203afe673c1e09511b9a70b3d8c Mon Sep 17 00:00:00 2001
From: Joe R <joeraz5@verizon.net>
Date: Mon, 15 Nov 2021 21:41:39 -0500
Subject: [PATCH] Added Rainbow Fan game.

---
 html-src/rules/rainbowfan.html | 33 ++++++++++++++++++++++++
 pysollib/gamedb.py             | 12 ++++++---
 pysollib/games/fan.py          | 46 +++++++++++++++++++++++++++-------
 3 files changed, 79 insertions(+), 12 deletions(-)
 create mode 100644 html-src/rules/rainbowfan.html

diff --git a/html-src/rules/rainbowfan.html b/html-src/rules/rainbowfan.html
new file mode 100644
index 00000000..393b257b
--- /dev/null
+++ b/html-src/rules/rainbowfan.html
@@ -0,0 +1,33 @@
+<h1>Rainbow Fan</h1>
+<p>
+Fan game type. 2 decks. 3 redeals.
+
+<h3>Object</h3>
+<p>
+Move all cards to the foundations.
+
+<h3>Rules</h3>
+<p>
+There are eight foundations.  At the start of the game, a king of
+each suit is placed on the bottom of four of them, and an ace on the
+other four.  The foundations that start with a king are built down
+by suit, while the ones that start with an ace are built up.
+<p>
+The tableau consists of twenty piles of three cards, and may be
+built up or down by suit.  When building tableau piles, sequences
+can turn the corner, so aces may be played on kings and vice versa.
+However, this cannot be done in the foundation.
+<p>
+If there are still cards in the talon, three cards are dealt from
+the talon to fill in empty tableau piles.  Once the talon is empty,
+empty tableau piles can no longer be filled.
+<p>
+Three redeals are allowed.  During each redeal, the bottom card of
+each tableau pile is moved to the top of that pile.  The remaining
+cards are unmoved.
+
+<h3>Notes</h3>
+<p>
+This game is traditionally known as Rainbow - it is often renamed
+Rainbow Fan to avoid confusion with a Canfield variation of the same
+name.
diff --git a/pysollib/gamedb.py b/pysollib/gamedb.py
index d9d2e582..b85b6433 100644
--- a/pysollib/gamedb.py
+++ b/pysollib/gamedb.py
@@ -337,6 +337,12 @@ class GI:
         ("Microsoft Solitaire Collection", (2, 8, 11, 38, 22231,)),
 
         # XM Solitaire
+        # NOTE: This collection has a lot of games with the same name as
+        # established games but completely different rules, or more obscure
+        # variations with more generic names.  As such rules/names may
+        # conflict with other attempts to add games in the future, games
+        # from XM Solitaire should be researched before being added to PySol.
+        #
         # still missing:
         #       Ace of Hearts, Affinity, Agnes Three, Antares, Archway,
         #       Avenue, Baker's Fan, Baker's Spider, Bedeviled, Binding,
@@ -353,7 +359,7 @@ class GI:
         #       La Cabane, La Double Entente, Little Gazette, Magic FreeCell,
         #       Mini Gaps, Montreal, Napoleon at Iena, Napoleon at Waterloo,
         #       Napoleon's Guards, Nationale, Oasis, Opera, Ordered Suits,
-        #       Osmotic FreeCell, Pair FreeCell, Pairs 2, Petal, Rainbow Fan,
+        #       Osmotic FreeCell, Pair FreeCell, Pairs 2, Petal,
         #       Reserved Thirteens, Sea Spider, Sept Piles 0, Short Solitaire,
         #       Simple Alternations, Simple Spark, Step By Step, Strategy 7,
         #       Stripped FreeCell, Tarantula, Triple Dispute, Trusty Twenty,
@@ -368,7 +374,7 @@ class GI:
             363, 364, 372, 376, 383, 384, 385, 386, 390, 391, 393, 398,
             405, 415, 416, 425, 451, 453, 461, 464, 466, 467, 476, 480,
             484, 511, 512, 516, 561, 610, 625, 629, 631, 638, 641, 647,
-            650, 655, 678, 734, 751, 784, 825, 829, 901,
+            650, 655, 678, 734, 751, 784, 825, 829, 834, 901,
         )),
 
         # xpat2 1.06 (we have 14 out of 16 games)
@@ -479,7 +485,7 @@ class GI:
         ('fc-2.12',   tuple(range(774, 811)) + (16681,) +
          tuple(range(22217, 22219))),
         ('fc-2.14', tuple(range(811, 827))),
-        ('fc-2.16', tuple(range(827, 834)))
+        ('fc-2.16', tuple(range(827, 835)))
     )
 
     # deprecated - the correct way is to or a GI.GT_XXX flag
diff --git a/pysollib/games/fan.py b/pysollib/games/fan.py
index afc0aa7c..4416a8be 100644
--- a/pysollib/games/fan.py
+++ b/pysollib/games/fan.py
@@ -699,6 +699,7 @@ class FascinationFan(Fan):
 
 # ************************************************************************
 # * Crescent
+# * Rainbow Fan
 # ************************************************************************
 
 class Crescent_Talon(RedealTalonStack):
@@ -715,12 +716,12 @@ class Crescent_Talon(RedealTalonStack):
             ncards += len(r.cards)
             # move cards to internal stacks
             while len(r.cards) != 1:
-                self.game.moveMove(1, r, intern1, frames=4)
-            self.game.moveMove(1, r, intern2, frames=4)
+                self.game.moveMove(1, r, intern1, frames=2)
+            self.game.moveMove(1, r, intern2, frames=2)
             # move back
             while intern1.cards:
-                self.game.moveMove(1, intern1, r, frames=4)
-            self.game.moveMove(1, intern2, r, frames=4)
+                self.game.moveMove(1, intern1, r, frames=2)
+            self.game.moveMove(1, intern2, r, frames=2)
         self.game.nextRoundMove(self)
         if sound:
             self.game.stopSamples()
@@ -731,14 +732,23 @@ class Crescent_Talon(RedealTalonStack):
 class Crescent(Game):
     Hint_Class = CautiousDefaultHint
 
+    ROWS = 4
+    COLS = 4
+    INIT_CARDS = 6
+
+    SHOW_TALON_COUNT = False
+
     def createGame(self):
         l, s = Layout(self), self.s
         playcards = 10
-        w0 = l.XS+(playcards-1)*l.XOFFSET
-        w, h = l.XM+max(4*w0, 9*l.XS), l.YM + 5 * l.YS + l.TEXT_HEIGHT
+        w0 = l.XS + (playcards - 1) * l.XOFFSET
+        w, h = l.XM + max(self.COLS * w0, 9 * l.XS), \
+            l.YM + (self.ROWS + 1) * l.YS + l.TEXT_HEIGHT
         self.setSize(w, h)
         x, y = l.XM, l.YM
         s.talon = Crescent_Talon(x, y, self, max_rounds=4)
+        if self.SHOW_TALON_COUNT:
+            l.createText(s.talon, 'ne')
         l.createRoundText(s.talon, 's')
         x, y = w-8*l.XS, l.YM
         for i in range(4):
@@ -749,9 +759,9 @@ class Crescent(Game):
                                                     base_rank=KING, dir=-1))
             x += l.XS
         y = l.YM + l.YS + l.TEXT_HEIGHT
-        for i in range(4):
+        for i in range(self.ROWS):
             x = l.XM
-            for j in range(4):
+            for j in range(self.COLS):
                 stack = UD_SS_RowStack(x, y, self, base_rank=NO_RANK, mod=13)
                 s.rows.append(stack)
                 stack.CARD_XOFFSET, stack.CARD_YOFFSET = l.XOFFSET, 0
@@ -770,11 +780,27 @@ class Crescent(Game):
 
     def startGame(self):
         self.s.talon.dealRow(rows=self.s.foundations, frames=0)
-        self._startDealNumRowsAndDealSingleRow(5)
+        self._startDealNumRowsAndDealSingleRow(self.INIT_CARDS - 1)
 
     shallHighlightMatch = Game._shallHighlightMatch_SSW
 
 
+class RainbowFan(Crescent):
+    ROWS = 4
+    COLS = 5
+    INIT_CARDS = 3
+    SHOW_TALON_COUNT = True
+
+    def fillStack(self, stack):
+        if stack in self.s.rows and len(stack.cards) == 0 \
+                and len(self.s.talon.cards) > 0:
+            old_state = self.enterState(self.S_FILL)
+            for i in range(3):
+                self.s.talon.flipMove(1)
+                self.s.talon.moveMove(1, stack)
+            self.leaveState(old_state)
+
+
 # ************************************************************************
 # * School
 # ************************************************************************
@@ -1069,3 +1095,5 @@ registerGame(GameInfo(767, QuadsPlus, "Quads +",
                       GI.SL_MOSTLY_SKILL))
 registerGame(GameInfo(819, BearRiver, "Bear River",
                       GI.GT_FAN_TYPE | GI.GT_OPEN, 1, 0, GI.SL_MOSTLY_SKILL))
+registerGame(GameInfo(834, RainbowFan, "Rainbow Fan",
+                      GI.GT_FAN_TYPE, 2, 3, GI.SL_MOSTLY_SKILL))