Fifty Solitaires – Piling it Up

A pile of playing cards of suit hearts
If you like this post, please share.

So here is the third instalment of my Solitaire card game. In the previous post, I created the basic Card component and set up Storybook to let me browse and test my components while developing them. Today, I will create another component that displays a collection of cards. In a Solitaire game, cards are arranged on the table in piles. The cards in the piles can be face-up or face-down. In addition, piles can be closed or open. In closed piles, each card is placed exactly on top of the previous one. In open piles, each card is placed slightly offset from the one beneath. For face-up cards, this allows the player to see the suit and value of each card in the pile. For face-down cards, it lets the player see easily how many cards are in the pile.

Face-down cards

The Card component that I created in the previous post did not allow for face-down cards. So, let’s first add the face-down feature to the existing component. The first step is to create a face-down card symbol in the SVG file that contains all the other cards. In src/assets/playing_cards.svg, I added the following symbol before the closing </svg> tag.

  <symbol id="face-down" viewBox="30 2310 360 540">
  <g
     transform="translate(30,1797.6378)"
     id="face-down">
    <rect
       rx="29.944447"
       ry="29.944447"
       y="512.86218"
       x="0.5"
       height="539"
       width="359"
       id="rect6472-45"
       style="fill:#ffffff;stroke:#000000;stroke-width:0.99999976" />
    <rect
       rx="19.944447"
       ry="19.944447"
       y="532.86218"
       x="20.5"
       height="499"
       width="319"
       id="rect6472-45"
       style="fill:none; stroke:#000088; stroke-width:5" />
    <rect
       rx="9.944447"
       ry="9.944447"
       y="552.86218"
       x="40.5"
       height="459"
       width="279"
       id="rect6472-45"
       style="fill:none; stroke:#000088; stroke-width:5" />
  </g>
  </symbol>

I tried to set the parameters of the ViewBox and the transform attributes in line with all the other symbols in the file. The face-down symbol simply consists of a white background with two rounded rectangles inside. It is probably not the most beautiful reverse side of a playing card but, given that I coded the SVG by hand, it will have to do for now.

Next, I amended the Card component in src/components/Card.tsx to allow a faceUp property to be passed in. If faceUp is true the card will be displayed as usual, and if it is false the face-down symbol will be shown. I also changed the CardProperties type to allow additional properties to be passed in.

export interface CardData {
  suit: CardSuit;
  value: CardValue;
  faceUp: boolean;
}

export interface CardProperties extends React.SVGProps<SVGSVGElement>, CardData {}

export function Card({suit, value, faceUp, ...props}: CardProperties) {
  const cardId = faceUp
    ? `${playingCards}#${value.toLowerCase()}-${suit.toLowerCase()}`
    : `${playingCards}#face-down`;
  const classNames = `${props.className} card-component`;
  return <svg {...props} className={classNames} >
    <use xlinkHref={cardId}></use>
  </svg>
}

To be able to test the new feature in Storybook, I added a faceUp: true property to the existing stories in src/stories/Card.stories.tsx. This automatically adds a switch in the Storybook stories that toggle the face-down/face-up status.

Creating the Pile

Next, I created a new component in src/components/Pile.tsx. This file contains a few bits, so I will go through it piece by piece. At the top of the file, I do some imports and type definitions.

import { Card, CardData } from "./Card";
import './Pile.css'

export interface CardDisplayProperties extends CardData {
    open: boolean;
}

export type PileData = Array<CardDisplayProperties>;

export enum Direction {
    east = 'east',
    west = 'west',
    north = 'north',
    south = 'south'
}

export interface PileProperties {
    pile: PileData;
    direction: Direction;
}

First, I imported the Card component as well as a CSS stylesheet that I yet have to create. The CardDisplayProperties interface contains all the data needed to show a card on the pile. In addition to the CardData, it contains an open flag to control if the position of the card is offset from the card below it. The PileData type is then simply an array of CardDisplayProperties. I also wanted to allow the direction of the offset to be controlled. I remember that some Solitaire variants have piles that fan out to the left or right. I just want to be prepared for this. So, I created a Direction enum that contains the four directions of the compass. Finally, the PileProperties interface is made up of the pile data and a direction.

To position the cards, the idea is to use a <div> and use absolute positioning and then place the individual cards. Each card will have a different offset from the top or left, depending on how many open and closed cards have already been placed below it. Before I continue with the code in Pile.jsx, let me first show you the CSS style in src/components/Pile.css

.card-pile {
    position: relative;
    overflow:  visible;
    width: 100%;
    height: 100%;
}

.card-pile .card {
    position: absolute;
    top: 0;
    left: 0;
}

The .card-pile class will be used for the Pile component and the .card is the CSS class of the Card component within a pile. The overflow: visible is needed so that cards can freely be placed outside the original bounding box of the pile which should just be the size of the bottom card in the pile. You can see that the top and left properties of the cards default to zero but they can be overwritten by inline styles.

Next, in src/components/Pile.tsx I defined a helper object called margins.

const margins: {[key: string]: [string, number, number]} = {
    east: [ 'left', 15, 2],
    west: [ 'left', -15, -2],
    north: [ 'top', -15, -2],
    south: [ 'top', 15, 2],
}

The object is meant to serve as a look-up from the Direction enum to an array of parameters. The first entry in the array determines the CSS property that needs to be modified, the second entry is the percentage offset for open cards, and the third entry is the percentage offset for closed cards.

Now, I am ready to create the Pile component.

export function Pile({pile, direction}: PileProperties) {
    const marginSpec = margins[direction]; 
    return <div className={`card-pile card-pile-${direction}`}>
        {pile.map(function cardMapper(this: {offset: number}, card, index) {
            const cardStyle = {
                [marginSpec[0]]: `${this.offset}%`
            }
            this.offset += card.open ? marginSpec[1] : marginSpec[2];
            return <Card 
                className={`card ${card.open ? 'open' : ''}`}
                suit={card.suit} 
                value={card.value} 
                faceUp={card.faceUp}
                key={index}
                style={cardStyle} />
        }, {offset: 0})}
    </div>
}

The first line chooses the margin specifications from the margins dictionary based on the pile direction. Then inside the outer <div> the pile array is mapped to an array of Card components. I am using a less well-known feature of the Array.map function by passing {offset: 0} as a second argument after the mapper function. This argument will be attached to this inside the mapper callback. To make this work, I have to make sure that two conditions are met. First, I have to use the function keyword for the callback. This ensures that the callback has a this reference. Second, for Typescript to know the type of this, the callback function takes this as a first argument. This is compiled away in the transformation from Typescript to JavaScript and is only there to make the Typescript type system aware that this.offset exists. The offset property itself is incremented depending on the card.open flag and the margin specification.

The Story for the Pile

Now that we have completed the Pile component, we want to make it show up in Storybook as well. What follows is a slightly lengthy file that contains three stories and is stored in src/stories/Pile.stories.tsx.

import { Pile, PileProperties } from '../components/Pile';
import { CardSuit, CardValue } from '../components/Card';

export default {
  component: Pile,
  title: 'Components/Pile',
};

interface StoryPileProperties extends PileProperties {
  style: { [key: string]: string}
}

function Template(args: StoryPileProperties) {
  return <div style={({
      display: 'flex',
      justifyContent: "center",
      width: "100%",
    })}>
      <div style={args.style}><Pile {...args} /></div>
    </div>
};

export const Open = Template.bind({});

(Open as any).args = {
  pile: [
    { suit: CardSuit.clubs, value: CardValue.ace, faceUp: true, open: true },
    { suit: CardSuit.clubs, value: CardValue.three, faceUp: true, open: true },
    { suit: CardSuit.diamonds, value: CardValue.eight, faceUp: true, open: true },
], 
  direction: 'south',
  style: {
    width: 200,
    height: 290,
    backgroundColor: '#444444',
    padding: 10
  }
};

export const Closed = Template.bind({});

(Closed as any).args = {
  pile: [
    { suit: CardSuit.clubs, value: CardValue.ace, faceUp: true, open: false },
    { suit: CardSuit.clubs, value: CardValue.three, faceUp: true, open: false },
    { suit: CardSuit.diamonds, value: CardValue.eight, faceUp: true, open: false },
], 
  direction: 'south',
  style: {
    width: 200,
    height: 290,
    backgroundColor: '#444444',
    padding: 10
  }
};

export const Mixed = Template.bind({});

(Mixed as any).args = {
  pile: [
    { suit: CardSuit.clubs, value: CardValue.ace, faceUp: true, open: false },
    { suit: CardSuit.clubs, value: CardValue.three, faceUp: true, open: false },
    { suit: CardSuit.diamonds, value: CardValue.eight, faceUp: true, open: false },
    { suit: CardSuit.hearts, value: CardValue.jack, faceUp: true, open: true },
    { suit: CardSuit.spades, value: CardValue.four, faceUp: true, open: true },
    { suit: CardSuit.hearts, value: CardValue.seven, faceUp: true, open: true },
], 
  direction: 'south',
  style: {
    width: 200,
    height: 290,
    backgroundColor: '#444444',
    padding: 10
  }
};

export const FaceUpAndDown = Template.bind({});

(FaceUpAndDown as any).args = {
  pile: [
    { suit: CardSuit.clubs, value: CardValue.ace, faceUp: false, open: true },
    { suit: CardSuit.clubs, value: CardValue.three, faceUp: false, open: true },
    { suit: CardSuit.spades, value: CardValue.four, faceUp: true, open: true },
    { suit: CardSuit.hearts, value: CardValue.seven, faceUp: true, open: true },
], 
  direction: 'south',
  style: {
    width: 200,
    height: 290,
    backgroundColor: '#444444',
    padding: 10,
  }
};

There is nothing too fancy about this file. It defines a reusable Template to show the Pile component in some context. Then, each story is defined by the open and closed, and the face-up and face-down cards. I have created three stories showing different use cases. Now I can run

npm run storybook

The image below shows an example of a pile feathered out to the east direction.

A pile of cards inside the Storybook application with controls to change its appearance

One of the stories for the Pile component in Storybook

Next Steps

Now that I have a Card component and a Pile component, I will need another component that arranges different piles on the table. This will most likely be the topic of my next post on the JavaScript Solitaire game. After that, I need to look into implementing a drag-and-drop mechanism so that the player can pick up cards from one pile and place them on another pile. When that’s done, I can start creating the game model that implements the rules of the Solitaire games.


If you like this post, please share.

Leave a Reply