diff --git a/html-src/rules/roamingproils.html b/html-src/rules/roamingproils.html
new file mode 100644
index 00000000..b469b71d
--- /dev/null
+++ b/html-src/rules/roamingproils.html
@@ -0,0 +1,28 @@
+<h1>Roaming Proils</h1>
+<p>
+Fan game type. 1 deck. No redeal.
+
+<h3>Object</h3>
+<p>
+Move all cards to the foundations.
+
+<h3>Rules</h3>
+<p>
+Cards are dealt into 17 piles of three cards each, with only the
+top card of each pile face-up.  The final card is dealt to a
+single reserve pile.
+<p>
+Tableau piles are built by same rank, but only three consecutive
+cards of the same rank are allowed in the same tableau pile.  This is
+called a proil, and the fourth card of the same rank cannot be moved to
+a proil.  Empty piles cannot be filled.  The reserve card can be moved
+to an appropriate tableau pile at any time, but once the reserve is
+empty, only a king can be moved to it.
+<p>
+The foundations are built up by suit.  The game is won if
+all cards are moved to the foundations.
+
+<h3>Notes</h3>
+<p>
+The name "Proil" is a distortion of "Pair Royal", which describes
+a three-of-a-kind in Cribbage.
diff --git a/pysollib/gamedb.py b/pysollib/gamedb.py
index 9675f101..2456df8f 100644
--- a/pysollib/gamedb.py
+++ b/pysollib/gamedb.py
@@ -548,7 +548,7 @@ class GI:
          tuple(range(22217, 22219))),
         ('fc-2.14', tuple(range(811, 827))),
         ('fc-2.15', tuple(range(827, 855)) + tuple(range(22400, 22407))),
-        ('dev', tuple(range(855, 879)))
+        ('dev', tuple(range(855, 880)))
     )
 
     # 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 e0e14514..a52e556d 100644
--- a/pysollib/games/fan.py
+++ b/pysollib/games/fan.py
@@ -611,10 +611,10 @@ class BoxFan(Fan):
 
 # ************************************************************************
 # * Troika
+# * Quads
 # ************************************************************************
 
 class Troika(Fan):
-
     RowStack_Class = StackWrapper(RK_RowStack, dir=0,
                                   base_rank=NO_RANK, max_cards=3)
 
@@ -667,6 +667,42 @@ class QuadsPlus(Quads):
         self.s.talon.dealRow(rows=self.s.rows[:-1])
 
 
+# ************************************************************************
+# * Roaming Proils
+# ************************************************************************
+
+class RoamingProils_RowStack(RK_RowStack):
+
+    def acceptsCards(self, from_stack, cards):
+        if not RK_RowStack.acceptsCards(self, from_stack, cards):
+            return False
+        rank_sequence = 1
+        for card in reversed(self.cards):
+            if card.rank == cards[0].rank and card.face_up:
+                rank_sequence += 1
+            else:
+                break
+
+        if rank_sequence > 3:
+            return False
+        return True
+
+
+class RoamingProils(Fan):
+    RowStack_Class = StackWrapper(RoamingProils_RowStack, dir=0,
+                                  base_rank=NO_RANK)
+    ReserveStack_Class = StackWrapper(ReserveStack, base_rank=KING)
+
+    def createGame(self):
+        Fan.createGame(self, rows=(5, 5, 5, 2), playcards=5, reserves=1)
+
+    def startGame(self):
+        for i in range(2):
+            self.s.talon.dealRow(rows=self.s.rows[:17], flip=0, frames=0)
+        self._startAndDealRow()
+        self.s.talon.dealRow(rows=self.s.reserves)
+
+
 # ************************************************************************
 # * Fascination Fan
 # ************************************************************************
@@ -1103,3 +1139,5 @@ registerGame(GameInfo(834, RainbowFan, "Rainbow Fan",
                       GI.GT_FAN_TYPE, 2, 3, GI.SL_MOSTLY_SKILL))
 registerGame(GameInfo(871, CeilingFan, "Ceiling Fan",
                       GI.GT_FAN_TYPE | GI.GT_OPEN, 1, 0, GI.SL_MOSTLY_SKILL))
+registerGame(GameInfo(879, RoamingProils, "Roaming Proils",
+                      GI.GT_FAN_TYPE, 1, 0, GI.SL_BALANCED))