Commits

Juha Komulainen committed f993df4

Initial revision.

  • Participants

Comments (0)

Files changed (84)

+syntax: glob
+*~
+target
+project/boot
+lib_managed
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

File project/build.properties

+#Project properties
+#Thu Apr 21 14:49:27 EEST 2011
+project.organization=komu
+project.name=solitarius
+sbt.version=0.7.5
+project.version=1.0
+build.scala.versions=2.8.1
+project.initialize=false

File project/build/SolitariusProject.scala

+import sbt._
+
+class SolitariusProject(info: ProjectInfo) extends DefaultProject(info) {
+  val specs = "org.scala-tools.testing" % "specs_2.8.1" % "1.6.7" % "test"
+}

File src/main/resources/images/jiff/b1fh.png

Added
New image

File src/main/resources/images/jiff/b1fv.png

Added
New image

File src/main/resources/images/jiff/b1pb.png

Added
New image

File src/main/resources/images/jiff/b1pl.png

Added
New image

File src/main/resources/images/jiff/b1pr.png

Added
New image

File src/main/resources/images/jiff/b1pt.png

Added
New image

File src/main/resources/images/jiff/b2fh.png

Added
New image

File src/main/resources/images/jiff/b2fv.png

Added
New image

File src/main/resources/images/jiff/b2pb.png

Added
New image

File src/main/resources/images/jiff/b2pl.png

Added
New image

File src/main/resources/images/jiff/b2pr.png

Added
New image

File src/main/resources/images/jiff/b2pt.png

Added
New image

File src/main/resources/images/jiff/c1.png

Added
New image

File src/main/resources/images/jiff/c10.png

Added
New image

File src/main/resources/images/jiff/c2.png

Added
New image

File src/main/resources/images/jiff/c3.png

Added
New image

File src/main/resources/images/jiff/c4.png

Added
New image

File src/main/resources/images/jiff/c5.png

Added
New image

File src/main/resources/images/jiff/c6.png

Added
New image

File src/main/resources/images/jiff/c7.png

Added
New image

File src/main/resources/images/jiff/c8.png

Added
New image

File src/main/resources/images/jiff/c9.png

Added
New image

File src/main/resources/images/jiff/cj.png

Added
New image

File src/main/resources/images/jiff/ck.png

Added
New image

File src/main/resources/images/jiff/cq.png

Added
New image

File src/main/resources/images/jiff/d1.png

Added
New image

File src/main/resources/images/jiff/d10.png

Added
New image

File src/main/resources/images/jiff/d2.png

Added
New image

File src/main/resources/images/jiff/d3.png

Added
New image

File src/main/resources/images/jiff/d4.png

Added
New image

File src/main/resources/images/jiff/d5.png

Added
New image

File src/main/resources/images/jiff/d6.png

Added
New image

File src/main/resources/images/jiff/d7.png

Added
New image

File src/main/resources/images/jiff/d8.png

Added
New image

File src/main/resources/images/jiff/d9.png

Added
New image

File src/main/resources/images/jiff/dj.png

Added
New image

File src/main/resources/images/jiff/dk.png

Added
New image

File src/main/resources/images/jiff/dq.png

Added
New image

File src/main/resources/images/jiff/ec.png

Added
New image

File src/main/resources/images/jiff/h1.png

Added
New image

File src/main/resources/images/jiff/h10.png

Added
New image

File src/main/resources/images/jiff/h2.png

Added
New image

File src/main/resources/images/jiff/h3.png

Added
New image

File src/main/resources/images/jiff/h4.png

Added
New image

File src/main/resources/images/jiff/h5.png

Added
New image

File src/main/resources/images/jiff/h6.png

Added
New image

File src/main/resources/images/jiff/h7.png

Added
New image

File src/main/resources/images/jiff/h8.png

Added
New image

File src/main/resources/images/jiff/h9.png

Added
New image

File src/main/resources/images/jiff/hj.png

Added
New image

File src/main/resources/images/jiff/hk.png

Added
New image

File src/main/resources/images/jiff/hq.png

Added
New image

File src/main/resources/images/jiff/jb.png

Added
New image

File src/main/resources/images/jiff/jr.png

Added
New image

File src/main/resources/images/jiff/s1.png

Added
New image

File src/main/resources/images/jiff/s10.png

Added
New image

File src/main/resources/images/jiff/s2.png

Added
New image

File src/main/resources/images/jiff/s3.png

Added
New image

File src/main/resources/images/jiff/s4.png

Added
New image

File src/main/resources/images/jiff/s5.png

Added
New image

File src/main/resources/images/jiff/s6.png

Added
New image

File src/main/resources/images/jiff/s7.png

Added
New image

File src/main/resources/images/jiff/s8.png

Added
New image

File src/main/resources/images/jiff/s9.png

Added
New image

File src/main/resources/images/jiff/sj.png

Added
New image

File src/main/resources/images/jiff/sk.png

Added
New image

File src/main/resources/images/jiff/sq.png

Added
New image

File src/main/scala/solitarius/Main.scala

+/*
+ *  Copyright 2008 Juha Komulainen
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package solitarius
+
+import java.awt.event._
+import javax.swing._
+
+import general.Tableau
+import rules.{ SpiderTableau, SpiderLevel, KlondikeTableau, FreeCellTableau }
+import ui.TableauView
+import ui._
+
+object Main {
+  
+  val frame = new JFrame("Solitarius")
+  
+  def main(args: Array[String]) {
+    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName)
+    frame.setJMenuBar(menuBar)
+    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
+    frame.setSize(600, 400)
+    frame.setLocationRelativeTo(null)
+    frame.setVisible(true)
+  }
+  
+  def newGame(tableau: Tableau) {
+    frame.getContentPane.removeAll()
+    frame.getContentPane.add(new TableauView(tableau))
+    frame.pack()
+  }
+  
+  def menuBar =
+    MenuBarBuilder.buildMenuBar { menuBar =>
+      menuBar.submenu("Game") { game =>
+        game.submenu("New Game") { sub =>
+          sub.action("FreeCell") { newGame(new FreeCellTableau) }
+          sub.separator
+          sub.action("Klondike") { newGame(new KlondikeTableau) }
+          sub.separator
+          sub.action("Spider - Easy")   { newGame(new SpiderTableau(SpiderLevel.Easy)) }
+          sub.action("Spider - Medium") { newGame(new SpiderTableau(SpiderLevel.Medium)) }
+          sub.action("Spider - Hard")   { newGame(new SpiderTableau(SpiderLevel.Hard)) }
+        }
+        game.separator
+        game.action("Quit") { System.exit(0) }
+      }
+      menuBar.submenu("Help") { help =>
+        help.action("About Solitarius") { showAbout() }
+      }
+    }
+      
+  def showAbout() {
+    val message =
+        "Solitarius 0.3\n" +
+        "\n" +
+        "Copyright 2008 Juha Komulainen\n"
+
+    JOptionPane.showMessageDialog(
+        frame, message, 
+        "About Solitarius", JOptionPane.INFORMATION_MESSAGE)
+  }
+}

File src/main/scala/solitarius/general/Card.scala

+/*
+ *  Copyright 2008 Juha Komulainen
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package solitarius.general
+
+import Utils.shuffled
+
+sealed abstract class CardColor {
+  def opposing: CardColor
+}
+
+object CardColor {
+  object Black extends CardColor {
+    def opposing = Red
+  }
+  object Red extends CardColor {
+    def opposing = Black
+  }
+}
+
+sealed abstract class Suit(val name: String, val color: CardColor) {
+  override def toString = name
+}
+
+object Suit {
+  case object Heart   extends Suit("Heart",   CardColor.Red)
+  case object Spade   extends Suit("Spade",   CardColor.Black)
+  case object Diamond extends Suit("Diamond", CardColor.Red)
+  case object Club    extends Suit("Club",    CardColor.Black)
+  
+  val cardsInSuit = 13
+  val suits = List(Heart, Spade, Diamond, Club)
+}
+
+sealed abstract class Rank(val shortName: String, val longName: String, val value: Int) {
+  override def toString = longName
+}
+
+object Rank {
+  case object Ace   extends Rank("A", "Ace",  1)
+  case object Deuce extends Rank("2",   "2",  2)
+  case object Three extends Rank("3",   "3",  3)
+  case object Four  extends Rank("4",   "4",  4)
+  case object Five  extends Rank("5",   "5",  5)
+  case object Six   extends Rank("6",   "6",  6)
+  case object Seven extends Rank("7",   "7",  7)
+  case object Eight extends Rank("8",   "8",  8)
+  case object Nine  extends Rank("9",   "9",  9)
+  case object Ten   extends Rank("10", "10",  10)
+  case object Jack  extends Rank("J",  "Jack",  11)
+  case object Queen extends Rank("Q",  "Queen", 12)
+  case object King  extends Rank("K",  "King",  13)
+  
+  val ranks = List(Ace, Deuce, Three, Four, Five, Six, Seven, Eight, Nine,
+                  Ten, Jack, Queen, King)
+}
+
+case class Card(rank: Rank, suit: Suit) {
+  def value = rank.value
+  def color = suit.color
+  override def toString = rank.longName + " of " + suit.name
+}
+
+object Deck {
+  val cards    = for (suit <- Suit.suits; rank <- Rank.ranks) yield Card(rank, suit)
+  val hearts   = ofSuit(Suit.Heart)
+  val spades   = ofSuit(Suit.Spade)
+  val diamonds = ofSuit(Suit.Diamond)
+  val clubs    = ofSuit(Suit.Spade)
+  
+  def ofSuit(suit: Suit): List[Card] = Rank.ranks.map(Card(_,suit))
+
+  def shuffledCards = shuffled(cards).toList
+  def shuffledDecks(deckCount: Int) = shuffled(decks(deckCount)).toList
+  
+  def decks(count: Int): List[Card] = if (count == 0) Nil else cards ::: decks(count - 1)
+}

File src/main/scala/solitarius/general/Solitaire.scala

+/*
+ *  Copyright 2008-2011 Juha Komulainen
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package solitarius.general
+
+abstract class Pile {
+  val showAsCascade: Boolean
+  def top = cards.headOption
+  def size = cards.size
+  def visibleCount: Int
+  def hiddenCount  = size - visibleCount
+  def isEmpty = size == 0
+  def cards: List[Card]
+  def visibleCards = cards.take(visibleCount)
+  def hiddenCards  = cards.drop(visibleCount)
+  def sequence(count: Int): Option[Sequence] = None
+  def drop(sequence: Sequence): Boolean = false
+}
+
+object Pile {
+  def dealToPiles(piles: Seq[BasicPile], cards: List[Card]) =
+    for ((card, index) <- cards.zipWithIndex)
+      piles(index % piles.size).push(card)
+}
+
+/**
+ * Pile is a possibly empty stack of cards on Tableau. If the pile is not
+ * empty, then one or more cards on top of the pile are face up and therefore
+ * visible.
+ */
+abstract class BasicPile extends Pile {
+  private var _cards: List[Card] = Nil
+  private var _visible = 0
+  
+  override val showAsCascade = true
+  override def isEmpty       = cards.isEmpty
+  override def size          = cards.size
+  override def visibleCount  = _visible
+  override def cards         = _cards
+  
+  override def sequence(count: Int): Option[Sequence] = 
+    if (count <= longestDraggableSequence)
+      Some(new BasicSequence(cards.take(count)))
+    else 
+      None
+      
+  private class BasicSequence(cards: List[Card]) extends Sequence(cards) {
+    override def removeFromOriginalPile() {
+      for (_ <- 1 to cards.size)
+        pop()
+    }
+  }
+  
+  private def pop(): Option[Card] = cards match {
+    case (c :: cs) =>
+      _visible = if (cs.isEmpty) 0 else (_visible - 1) max 1
+      _cards = cs
+      afterModification()
+      Some(c)
+    case Nil =>
+      None
+  }
+  
+  def clear() {
+    _cards = Nil
+    _visible = 0
+  }
+      
+  override def drop(sequence: Sequence) = {
+    if (canDrop(sequence)) {
+      sequence.removeFromOriginalPile()
+    
+      _cards = sequence.toList ::: _cards
+      _visible += sequence.size
+    
+      afterModification()
+      true
+    } else {
+      false
+    }
+  }
+  
+  protected def canDrop(sequence: Sequence): Boolean
+  protected def afterModification() { }
+  
+  def longestDraggableSequence: Int
+  
+  def showOnlyTop() { 
+    _visible = if (cards.isEmpty) 0 else 1
+  }
+  
+  def push(card: Card) {
+    _cards ::= card
+    _visible += 1
+  }
+  
+  protected def longestSequence(predicate: (Card,Card) => Boolean): Int = 
+    visibleCards match {
+      case (first :: rest) =>
+        var previous = first
+        var count = 1
+        for (card <- rest) {
+          if (predicate(previous, card)) {
+            count += 1
+            previous = card
+          } else {
+            return count
+          }
+        }
+        return count
+      case Nil =>
+        return 0
+    }
+}
+
+/**
+ * Base class for cascades that are built down by cards of alternating colors. 
+ */
+abstract class AlternateColorCascade extends BasicPile {
+  override def canDrop(seq: Sequence) = top match {
+    case Some(c) => 
+      seq.bottomSuit.color == c.color.opposing && seq.bottomRank.value == c.value - 1
+    case None => 
+      canDropOnEmpty(seq.bottomCard)
+  }
+  
+  def canDropOnEmpty(card: Card): Boolean
+}
+
+/**
+ * General base class for foundations.
+ */
+abstract class Foundation extends BasicPile {
+  override val showAsCascade = false
+  override def longestDraggableSequence = 0
+}
+
+/**
+ * A foundation that is built up by suited cards from Ace to King.
+ */
+class BySuitFoundation extends Foundation {
+  override def canDrop(seq: Sequence) = canDrop(seq.bottomCard) 
+
+  def canDrop(card: Card) = top match {
+    case Some(top) => card.suit == top.suit && card.value == top.value + 1
+    case None      => card.rank == Rank.Ace
+  }
+}
+
+/**
+ * Waste piles are piles that are not shown as cascade and where we can't
+ * drop anything manually, but we can drag cards away from them.
+ */
+class Waste extends BasicPile {
+  override val showAsCascade = false
+  override def longestDraggableSequence = if (isEmpty) 0 else 1
+  override def canDrop(sequence: Sequence) = false
+}
+
+/**
+ * Cells are piles that can contain only zero or one cards.
+ */
+class Cell extends Pile {
+  var card: Option[Card] = None
+  
+  override def cards = card.toList
+  override def visibleCount = size
+  override val showAsCascade = false
+
+  override def sequence(count: Int) = 
+    if (count == 1)
+      card.map(new CellSequence(_)) 
+    else 
+      None
+
+  override def drop(sequence: Sequence) =
+    if (isEmpty && sequence.size == 1) {
+      sequence.removeFromOriginalPile()
+    
+      card = Some(sequence.bottomCard)
+      true
+    } else {
+      false
+    }
+  
+  private class CellSequence(c: Card) extends Sequence(List(c)) {
+    override def removeFromOriginalPile() {
+      card = None
+    }
+  }
+}
+
+/**
+ * Sequence is a non-empty list of cards that can be moved together.
+ * The exact rules of what forms a valid sequence depends on the game.
+ */
+abstract class Sequence(cards: List[Card]) extends Seq[Card] {
+  assert(!cards.isEmpty, "empty sequence")
+
+  override def iterator = cards.iterator
+  override def length = cards.length
+  override def apply(i: Int) = cards.apply(i)
+  override def toList = cards
+  def bottomCard = cards.last
+  def bottomSuit = bottomCard.suit
+  def bottomRank = bottomCard.rank
+  def isSuited   = cards.forall(_.suit == cards.head.suit)
+  def removeFromOriginalPile(): Unit
+}
+
+/** 
+ * Stock contains cards that are not dealt in the beginning.
+ */
+class Stock(private var _cards: List[Card]) extends Pile {
+  override val showAsCascade = false
+  override def cards = _cards
+  override def visibleCount = 0
+  override def isEmpty = cards.isEmpty
+  override def size    = cards.size
+  
+  def takeOne() = take(1).headOption
+  
+  def take(count: Int) = {
+    val (taken, rest) = cards.splitAt(count)
+    _cards = rest
+    taken
+  }
+  
+  def pushAll(newCards: List[Card]) {
+    _cards = newCards.reverse ::: _cards
+  }
+}

File src/main/scala/solitarius/general/Tableau.scala

+/*
+ *  Copyright 2008-2011 Juha Komulainen
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package solitarius.general
+
+abstract class Tableau(val rowCount: Int, val columnCount: Int) {
+
+  lazy val table = {
+    val table = Array.ofDim[Pile](rowCount, columnCount)
+    for ((p, i) <- layout.piles.zipWithIndex) {
+      table(i / columnCount)(i % columnCount) = p
+    }
+    table
+  }
+  
+  def layout: Layout
+  def empty: Pile = null
+  def end = Layout(Nil)
+    
+  def rowHeights = table.map { row =>
+    if (row.exists(p => p != null && p.showAsCascade)) 4 else 1
+  }
+
+  lazy val allPiles = (for {
+    (row, y)  <- table.zipWithIndex
+    (pile, x) <- row.zipWithIndex
+    if (pile != null)
+  } yield (x, y, pile)).toList
+  
+  def pileClicked(pile: Pile, count: Int) { 
+  }
+  
+  case class Layout(piles: List[Pile]) {
+    def :-: (p: Pile): Layout       = Layout(p :: piles)
+    def :-: (ps: Seq[Pile]): Layout = Layout(ps.toList ::: piles)
+  }
+}

File src/main/scala/solitarius/general/Utils.scala

+/*
+ *  Copyright 2008-2011 Juha Komulainen
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package solitarius.general
+
+import java.util.Random
+
+object Utils {
+  
+  private val random = new Random
+
+  /** Poor man's profiling */
+  def time[T](name: String) (thunk: => T) {
+    val start = currentTime
+    try {
+      thunk
+    } finally {
+      val total = currentTime - start
+      println(name + ": " + total + "ms")
+    }
+  }
+  
+  def sum(xs: Seq[Int]) = xs.foldLeft(0)(_+_)
+  
+  def currentTime = System.currentTimeMillis
+  
+  /** Returns a shuffled array containing elements of given collection */
+  def shuffled[T](items: Collection[T]) (implicit manifest: scala.reflect.ClassManifest[T]): Array[T] = {
+    val array = items.toArray
+    shuffle(array)
+    array
+  }
+  
+  def replicate[T](count: Int, items: List[T]): List[T] =
+    if (count == 0) Nil else items ::: replicate(count-1, items)
+
+  /** Shuffles given array in-place */
+  def shuffle[T](array: Array[T]) =
+    for (i <- Iterator.range(array.size, 1, -1))
+      swap(array, i - 1, random.nextInt(i))
+
+  private def swap[T](array: Array[T], i: Int, j: Int) = {
+    val tmp = array(i)
+    array(i) = array(j)
+    array(j) = tmp
+  }
+}

File src/main/scala/solitarius/rules/FreeCell.scala

+/*
+ *  Copyright 2008-2011 Juha Komulainen
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package solitarius.rules
+
+import solitarius.general._
+import Pile.dealToPiles
+
+class FreeCellPile extends AlternateColorCascade {
+  override def longestDraggableSequence = if (isEmpty) 0 else 1
+  override def canDropOnEmpty(card: Card) = true
+}
+
+class FreeCellTableau extends Tableau(2, 8) {
+  val cells       = Array.fill(4)(new Cell)
+  val foundations = Array.fill(4)(new BySuitFoundation)
+  val piles       = Array.fill(8)(new FreeCellPile)
+
+  dealToPiles(piles, Deck.shuffledCards)
+
+  def layout = cells :-: foundations :-: piles :-: end
+}

File src/main/scala/solitarius/rules/Klondike.scala

+/*
+ *  Copyright 2008-2011 Juha Komulainen
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package solitarius.rules
+
+import solitarius.general._
+
+class KlondikePile extends AlternateColorCascade {
+  override def longestDraggableSequence = longestSequence { (previous, card) =>
+    card.color == previous.color.opposing && card.value == previous.value + 1 
+  }
+  
+  override def canDropOnEmpty(card: Card) = card.rank == Rank.King
+}
+
+class KlondikeTableau extends Tableau(2, 7) {
+  val waste       = new Waste
+  val stock       = new Stock(Deck.shuffledCards)
+  val foundations = Array.fill(4)(new BySuitFoundation)
+  val piles       = Array.fill(7)(new KlondikePile)
+
+  for (s <- 0 until piles.size)
+    for (n <- s until piles.size)
+      piles(n).push(stock.takeOne().get)
+  
+  piles.foreach(_.showOnlyTop())
+
+  def layout =
+    stock :-: waste :-: empty :-: foundations :-:
+    piles :-: end
+  
+  def deal() {
+    stock.takeOne match {
+      case Some(card) =>
+        waste.push(card)
+      case None =>
+        stock.pushAll(waste.cards)
+        waste.clear()
+    }
+  }
+    
+  override def pileClicked(pile: Pile, count: Int) {
+    if (pile == stock) {
+      deal()
+    } else if (count == 2) {
+      pile.sequence(1).foreach { seq =>
+        foundations.find(_.canDrop(seq)).foreach { foundation =>
+          foundation.drop(seq)
+        }
+      }
+    }
+  }
+}

File src/main/scala/solitarius/rules/Spider.scala

+/*
+ *  Copyright 2008-2011 Juha Komulainen
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package solitarius.rules
+
+import solitarius.general._
+import Pile.dealToPiles
+import Utils.{ replicate, shuffled }
+
+sealed abstract class SpiderLevel(cards: List[Card]) {
+  def newCards = shuffled(cards).toList
+}
+
+object SpiderLevel {
+  case object Easy   extends SpiderLevel(replicate(8, Deck.hearts))
+  case object Medium extends SpiderLevel(replicate(4, Deck.hearts ::: Deck.spades))
+  case object Hard   extends SpiderLevel(replicate(2, Deck.cards))
+}
+
+class SpiderPile extends BasicPile {
+  
+  override def canDrop(sequence: Sequence) = top match {
+    case Some(c) => sequence.bottomRank.value == c.value - 1
+    case None    => true
+  }
+  
+  override def afterModification() {
+    sequence(Suit.cardsInSuit).filter(_.isSuited).foreach { seq =>
+      seq.removeFromOriginalPile()
+    }
+  }
+  
+  override def longestDraggableSequence = longestSequence { (previous, card) =>
+    card.suit == previous.suit && card.value == previous.value + 1 
+  }
+}
+
+class SpiderTableau(level: SpiderLevel) extends Tableau(2, 10) {
+  val cards = level.newCards
+  val piles = Array.fill(10)(new SpiderPile)
+  val reserve = new Stock(cards.drop(54).toList)
+  
+  dealToPiles(piles, cards.take(54))
+  piles.foreach(_.showOnlyTop)
+  
+  def layout = piles :-: reserve :-: end
+
+  def hasEmptyPiles = piles.exists(_.isEmpty)
+  
+  def deal() {
+    if (!hasEmptyPiles)
+      for ((card, index) <- reserve.take(piles.size).zipWithIndex)
+        piles(index).push(card)
+  }
+  
+  override def pileClicked(pile: Pile, count: Int) {
+    if (pile == reserve)
+      deal()
+  }
+}

File src/main/scala/solitarius/ui/CardImages.scala

+/*
+ *  Copyright 2008-2011 Juha Komulainen
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package solitarius.ui
+
+import java.awt.{ Image, Toolkit }
+import java.io.FileNotFoundException
+
+import solitarius.general.{ Card, Deck, Rank }
+
+object CardImages {
+
+  val cardWidth = 71
+  val cardHeight = 96
+
+  val backside = loadImage("/images/jiff/b1fv.png")
+  
+  val cardImages: Map[Card,Image] =
+    Map((for (card <- Deck.cards) yield (card, loadImage(card))):_*)
+    
+  def loadImage(card: Card): Image = loadImage(imageFile(card).toString)
+  
+  def loadImage(file: String): Image = {
+    val resource = getClass.getResource(file)
+    if (resource != null)
+      Toolkit.getDefaultToolkit.createImage(resource)
+    else
+      throw new FileNotFoundException(file)
+  }
+  
+  def imageFile(card: Card): String = {
+    val rank = if (card.rank == Rank.Ace) "1" else card.rank.shortName.toLowerCase
+    String.format("/images/jiff/%s%s.png", 
+                  card.suit.name.substring(0, 1).toLowerCase, rank)
+  }
+}

File src/main/scala/solitarius/ui/MenuBuilder.scala

+/*
+ *  Copyright 2008 Juha Komulainen
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package solitarius.ui
+
+import javax.swing.{ AbstractAction, JMenu, JMenuBar }
+import java.awt.event.ActionEvent
+
+object MenuBarBuilder {
+  def buildMenuBar(callback: MenuBarBuilder => Unit): JMenuBar = { 
+    val builder = new MenuBarBuilder
+    callback(builder)
+    builder.menuBar
+  }
+}
+
+class MenuBarBuilder {
+  val menuBar = new JMenuBar
+  
+  def submenu(name: String) (callback: MenuBuilder => Unit): this.type = {
+    val menu = new JMenu(name)
+    callback(new MenuBuilder(menu))
+    menuBar.add(menu)
+    this
+  }
+}
+
+class MenuBuilder(menu: JMenu) {
+  def action(name: String) (action: => Unit) {
+    menu.add(new AbstractAction(name) {
+      override def actionPerformed(e: ActionEvent) = action
+    })
+  }
+  
+  def submenu(name: String) (callback: MenuBuilder => Unit): this.type = {
+    val subMenu = new JMenu(name)
+    callback(new MenuBuilder(subMenu))
+    menu.add(subMenu)
+    this
+  }
+    
+  def separator() = menu.addSeparator()
+}

File src/main/scala/solitarius/ui/TableauView.scala

+/*
+ *  Copyright 2008-2011 Juha Komulainen
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package solitarius.ui
+
+import java.awt.{ Color, Dimension, Graphics }
+import java.awt.event._
+import javax.swing.JComponent
+
+import solitarius.general.{ Card, Pile, Sequence, Tableau, Utils }
+import Utils.sum
+
+import CardImages.{ cardImages, backside, cardWidth, cardHeight }
+
+class TableauView(model: Tableau) extends JComponent {
+
+  val marginTop = 5
+  val marginRight = 5
+  val marginBottom = 5
+  val marginLeft = 5
+  val preferredPadding = 5
+  val cascadeDy = 20
+  var currentDrag: Option[Drag] = None
+  
+  addMouseListener(myMouseListener)
+  addMouseMotionListener(myMouseMotionListener)
+  setPreferredSize(new Dimension(marginLeft + marginRight + model.columnCount * (preferredPadding + cardWidth),
+                                 marginTop + marginBottom + sum(preferredRowHeights)))
+  setMinimumSize(getPreferredSize)
+  
+  def preferredRowHeights = model.rowHeights.map(cards => preferredPadding + cards * cardHeight)
+
+  final override def paint(g: Graphics) {
+    paintTableau(g)
+    
+    currentDrag.foreach { drag =>
+      drawCards(g, drag.x, drag.y, drag.sequence)
+    }
+  }
+  
+  private def paintTableau(g: Graphics) {
+    for ((x, y, pile) <- model.allPiles) {
+      val (xx, yy) = gridCoordinates(x, y)
+      drawPile(g, pile, xx, yy)
+    }
+  }
+
+  private def horizontalPadding = {
+    val width = getWidth
+    val contentWidth = width - marginLeft - marginRight
+    val cardWidths = model.columnCount * cardWidth
+    (contentWidth - cardWidths) / (model.columnCount-1)
+  }
+
+  private def verticalPadding = {
+    val height = getHeight
+    val contentHeight = height - marginTop - marginTop
+    val cardHeights = sum(model.rowHeights) * cardHeight
+    (contentHeight - cardHeights) / (model.rowCount-1)
+  }
+  
+  private def gridCoordinates(x: Int, y: Int) = {
+    val prev = cardHeight * sum(model.rowHeights.take(y))
+    (marginLeft + (cardWidth + horizontalPadding) * x, 
+     marginTop + prev + (verticalPadding * y))
+  }
+  
+  private def drawPile(g: Graphics, pile: Pile, x: Int, y: Int) {
+    val dragged = if (currentDrag.map(_.pile) == Some(pile))
+      currentDrag.get.sequence.size else 0
+    
+    val reallyVisible = pile.visibleCount - dragged
+    val isEmpty = (pile.size - dragged) == 0
+    
+    if (isEmpty) {
+      g.setColor(Color.GRAY)
+      g.fillRect(x, y, cardWidth, cardHeight)
+    } else if (pile.showAsCascade) {
+      drawCascaded(g, pile, x, y)
+    } else {
+      drawNonCascaded(g, pile, x, y)
+    }
+  }
+  
+  private def drawCascaded(g: Graphics, pile: Pile, x: Int, startY: Int) {
+    var y = startY
+    for (i <- 1 to pile.hiddenCount) {
+      g.drawImage(backside, x, y, this)
+      y += cascadeDy
+    }
+  
+    if (currentDrag.map(_.pile) == Some(pile)) {
+      drawCards(g, x, y, pile.visibleCards.drop(currentDrag.get.sequence.size))
+      } else {
+      drawCards(g, x, y, pile.visibleCards)
+    }
+  }
+  
+  private def drawNonCascaded(g: Graphics, pile: Pile, x: Int, y: Int) {
+    val dragged = if (currentDrag.map(_.pile) == Some(pile))
+      currentDrag.get.sequence.size else 0
+    val reallyVisible = pile.visibleCount - dragged
+    
+    if (reallyVisible > 0) {
+      drawCard(g, pile.cards.drop(dragged).head, x, y)
+    } else {
+      g.drawImage(backside, x, y, this)
+    }
+  }
+  
+  private def drawCards(g: Graphics, x: Int, y: Int, cards: Seq[Card]) {
+    var yy = y
+    for (card <- cards.reverse) {
+      drawCard(g, card, x, yy)
+      yy += cascadeDy
+    }
+  }
+  
+  private def drawCard(g: Graphics, card: Card, x: Int, y: Int) {
+    g.drawImage(cardImages(card), x, y, this)
+  }
+  
+  class Drag(val pile: Pile, val sequence: Sequence, val dx: Int, val dy: Int, var mouseX: Int, var mouseY: Int) {
+    def x = mouseX - dx
+    def y = mouseY - dy
+  }
+    
+  def positionToCardLocation(x: Int, y: Int) = 
+    boundsForCards.find(_._1.inBounds(x, y)).map { p =>
+      val (bounds, pile, cardIndex) = p
+      val (xx, yy) = bounds.relative(x, y)
+      val take = if (pile.showAsCascade) pile.size - cardIndex else 1
+      CardLocation(pile, take, xx, yy)
+    }
+  
+  def pile(x: Int, y: Int): Option[Pile] =
+    boundsForPiles.find(_._1.inBounds(x,y)).map(_._2)
+  
+  def boundsForCards: Seq[(Bounds, Pile, Int)] = for {
+    (bounds, pile) <- boundsForPiles
+    result <- boundsForCardsOfPile(bounds, pile)
+  } yield result
+  
+  def boundsForCardsOfPile(bounds: Bounds, pile: Pile): Seq[(Bounds,Pile,Int)] =
+    if (pile.showAsCascade) {
+      for {
+        cardIndex <- Iterator.range(pile.size-1, -1, -1).toList
+        val top = bounds.y + (cardIndex * cascadeDy)
+      } yield (Bounds(bounds.x, top, cardWidth, cardHeight), pile, cardIndex)
+    } else {
+      List((bounds, pile, 0))
+    }
+  
+  def boundsForPiles: Seq[(Bounds,Pile)] =
+    for {
+      (x, y, pile) <- model.allPiles
+      val (xx, yy) = gridCoordinates(x, y)
+    } yield (Bounds(xx, yy, cardWidth, pileHeight(pile)), pile)
+    
+  private def pileHeight(pile: Pile) = 
+    if (pile.showAsCascade) cardHeight + (pile.size * cascadeDy) else cardHeight
+  
+  case class CardLocation(pile: Pile, cardCount: Int, dx: Int, dy: Int)
+  
+  private object myMouseListener extends MouseAdapter {
+    override def mouseClicked(e: MouseEvent) {
+      positionToCardLocation(e.getX, e.getY).filter(_.cardCount == 1).foreach { location =>
+        model.pileClicked(location.pile, e.getClickCount)
+        repaint()
+      }
+    }
+    
+    override def mousePressed(e: MouseEvent) {
+      currentDrag = positionToCardLocation(e.getX, e.getY).flatMap { location =>
+        location.pile.sequence(location.cardCount).map { seq =>
+          new Drag(location.pile, seq, location.dx, location.dy, e.getX, e.getY)
+        }
+      }
+      repaint()
+    }
+    
+    override def mouseReleased(e: MouseEvent) {
+      currentDrag.foreach { drag =>
+        pile(e.getX, e.getY).foreach { target =>
+          target.drop(drag.sequence)
+        }
+      
+        currentDrag = None
+        repaint()
+      }
+    }
+  }
+  
+  private object myMouseMotionListener extends MouseMotionAdapter {
+    override def mouseDragged(e: MouseEvent) {
+      currentDrag.foreach { drag =>
+        drag.mouseX = e.getX
+        drag.mouseY = e.getY
+        repaint()
+      }
+    }
+  }
+}
+
+case class Bounds(x: Int, y: Int, w: Int, h: Int) {
+  def inBounds(xx: Int, yy: Int) = 
+    (xx >= x) && (xx < x + w) && (yy >= y) && (yy < y + h)
+    
+  def relative(xx: Int, yy: Int) =
+    (xx - x, yy - y)
+}

File src/test/scala/solitarius/allSpecsRunner.scala

+/*
+ *  Copyright 2008 Juha Komulainen
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package solitarius
+
+import org.specs.runner.SpecsFileRunner
+
+object allSpecsRunner extends SpecsFileRunner("src/test/scala/**/*.scala", ".*")

File src/test/scala/solitarius/rules/SpiderSpecification.scala

+/*
+ *  Copyright 2008 Juha Komulainen
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package solitarius.rules
+
+import org.specs._
+import solitarius.general._
+import Utils.shuffled
+import Rank._
+
+object SpiderSpecification extends Specification {
+  "Pile" should {
+    "show only the topmost card" in {
+      val pile = new SpiderPile
+      val (card :: cards) = randomCards(6)
+      cards.reverse.foreach(pile.push)
+      pile.showOnlyTop()
+
+      pile.isEmpty      must beFalse
+      pile.size         mustEqual 5
+      pile.top          mustEqual Some(cards.head)
+      pile.visibleCards mustEqual List(cards.head)
+      pile.visibleCount mustEqual 1
+    }
+    
+    "be allowed to be empty" in {
+      val pile = new SpiderPile
+            
+      pile.isEmpty      must beTrue
+      pile.size         mustEqual 0
+      pile.top          mustEqual None
+      pile.visibleCards mustEqual Nil
+      pile.visibleCount mustEqual 0
+    }
+    
+    "allow dropping cards on top" in {
+      val pile = new SpiderPile
+      val (card :: cards) = hearts(Five, Six, Eight, Jack, Ace, King)
+      cards.reverse.foreach(pile.push)
+      pile.showOnlyTop()
+
+      val seq = new DummySequence(card)
+
+      pile.canDrop(seq) must beTrue
+      pile.drop(seq)
+
+      pile.isEmpty      must beFalse
+      pile.size         mustEqual 6
+      pile.top          mustEqual Some(card)
+      pile.visibleCards mustEqual List(card, cards.head)
+      pile.visibleCount mustEqual 2
+    }
+    
+    "flip the new top card to be visible when top card is popped" in {
+      val pile = new SpiderPile
+      val cards = randomCards(6)
+      cards.reverse.foreach(pile.push)
+      pile.showOnlyTop()
+      
+      val seq = pile.sequence(1)
+      seq                 must beSome[Sequence]
+      seq.get.toList      mustEqual List(cards(0))
+      seq.get.removeFromOriginalPile()
+      pile.size           mustEqual 5
+      pile.top            mustEqual Some(cards(1))
+      pile.visibleCards   mustEqual List(cards(1))
+      pile.visibleCount   mustEqual 1
+    }
+    
+    "support popping the last card" in {
+      val pile = new SpiderPile
+      val cards = randomCards(1)
+      pile.push(cards.head)
+      
+      pile.visibleCards   mustEqual cards
+      pile.visibleCount   mustEqual 1
+      val seq = pile.sequence(1)
+      seq                 must beSome[Sequence]
+      seq.get.toList      mustEqual List(cards.head)
+      seq.get.removeFromOriginalPile
+      pile.visibleCards   must beEmpty
+      pile.visibleCount   mustEqual 0
+    }
+    
+    "support returning sequences of cards" in {
+      val pile = new SpiderPile
+      val cards = hearts(Five, Six, Seven, Nine, Jack, Ace, King)
+      cards.drop(3).reverse.foreach(pile.push)
+      pile.showOnlyTop()
+      cards.take(3).reverse.foreach(pile.push)
+      
+      pile.longestDraggableSequence mustEqual 3
+    }
+  }
+  
+  def hearts(ranks: Rank*) = ranks.map(new Card(_, Suit.Heart)).toList
+  
+  def randomCards(count: Int) = Deck.shuffledCards.take(count)
+
+  class DummySequence(cards: Card*) extends Sequence(cards.toList) {
+    override def removeFromOriginalPile() { }
+  }
+}