1/*
2 * Copyright (C) 2003 Josh Metzler <joshdeb@metzlers.org>
3 * Copyright (C) 2000-2009 Stephan Kulow <coolo@kde.org>
4 * Copyright (C) 2010 Parker Coates <coates@kde.org>
5 *
6 * License of original code:
7 * -------------------------------------------------------------------------
8 * Permission to use, copy, modify, and distribute this software and its
9 * documentation for any purpose and without fee is hereby granted,
10 * provided that the above copyright notice appear in all copies and that
11 * both that copyright notice and this permission notice appear in
12 * supporting documentation.
13 *
14 * This file is provided AS IS with no warranties of any kind. The author
15 * shall have no liability with respect to the infringement of copyrights,
16 * trade secrets or any patents by this file or any part thereof. In no
17 * event will the author be liable for any lost revenue or profits or
18 * other special, indirect and consequential damages.
19 * -------------------------------------------------------------------------
20 *
21 * License of modifications/additions made after 2009-01-01:
22 * -------------------------------------------------------------------------
23 * This program is free software; you can redistribute it and/or
24 * modify it under the terms of the GNU General Public License as
25 * published by the Free Software Foundation; either version 2 of
26 * the License, or (at your option) any later version.
27 *
28 * This program is distributed in the hope that it will be useful,
29 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 * GNU General Public License for more details.
32 *
33 * You should have received a copy of the GNU General Public License
34 * along with this program. If not, see <http://www.gnu.org/licenses/>.
35 * -------------------------------------------------------------------------
36 */
37
38#include "spider.h"
39
40#include "dealerinfo.h"
41#include "pileutils.h"
42#include "settings.h"
43#include "speeds.h"
44#include "patsolve/spidersolver.h"
45
46#include <KLocale>
47#include <KRandom>
48#include <KSelectAction>
49
50
51class InvisiblePile : public PatPile
52{
53public:
54 InvisiblePile( DealerScene * scene, int index, const QString & objectName = QString() )
55 : PatPile( scene, index, objectName )
56 {
57 };
58
59protected:
60 virtual void paintGraphic( QPainter * painter, qreal highlightedness )
61 {
62 Q_UNUSED( painter );
63 Q_UNUSED( highlightedness );
64 };
65};
66
67
68Spider::Spider( const DealerInfo * di )
69 : DealerScene( di )
70{
71}
72
73
74void Spider::initialize()
75{
76 m_leg = 0;
77 m_redeal = 0;
78
79 const qreal dist_x = 1.12;
80 const qreal smallNeg = -1e-6;
81
82 m_suits = Settings::spiderSuitCount();
83
84 createDeck();
85
86 // Dealing the cards out into 5 piles so the user can see how many
87 // sets of 10 cards are left to be dealt out
88 for( int column = 0; column < 5; ++column )
89 {
90 redeals[column] = new InvisiblePile( this, column + 1, QString( "redeals%1" ).arg( column ) );
91 redeals[column]->setPileRole(PatPile::Stock);
92 redeals[column]->setLayoutPos( dist_x * (9 - (4.0 - column) / 3), smallNeg );
93 redeals[column]->setZValue(12 * ( 5-column ));
94 redeals[column]->setSpread(0, 0);
95 redeals[column]->setKeyboardSelectHint( KCardPile::NeverFocus );
96 redeals[column]->setKeyboardDropHint( KCardPile::NeverFocus );
97 connect( redeals[column], SIGNAL(clicked(KCard*)), SLOT(drawDealRowOrRedeal()) );
98 }
99
100 // The 10 playing piles
101 for( int column = 0; column < 10; ++column )
102 {
103 stack[column] = new PatPile( this, column + 6, QString( "stack%1" ).arg( column ) );
104 stack[column]->setPileRole(PatPile::Tableau);
105 stack[column]->setLayoutPos(dist_x * column, 0);
106 stack[column]->setZValue(20);
107 stack[column]->setAutoTurnTop(true);
108 stack[column]->setBottomPadding( 1.5 );
109 stack[column]->setHeightPolicy( KCardPile::GrowDown );
110 stack[column]->setKeyboardSelectHint( KCardPile::AutoFocusDeepestRemovable );
111 stack[column]->setKeyboardDropHint( KCardPile::AutoFocusTop );
112 }
113
114 // The 8 'legs' so named by me because spiders have 8 legs - why
115 // else the name Spider?
116 for( int column = 0; column < 8; ++column )
117 {
118 legs[column] = new InvisiblePile( this, column + 16, QString( "legs%1" ).arg( column ) );
119 legs[column]->setPileRole(PatPile::Foundation);
120 legs[column]->setLayoutPos(dist_x / 3 * column, smallNeg);
121 legs[column]->setZValue(column+1);
122 legs[column]->setSpread(0, 0);
123 legs[column]->setZValue(14 * column);
124 legs[column]->setVisible( false );
125 legs[column]->setKeyboardSelectHint( KCardPile::NeverFocus );
126 legs[column]->setKeyboardDropHint( KCardPile::NeverFocus );
127 }
128
129 // Moving an A-K run to a leg is not really an autoDrop - the
130 // user should have no choice.
131 setAutoDropEnabled(false);
132 setActions(DealerScene::Hint | DealerScene::Demo | DealerScene::Deal);
133 setSolver( new SpiderSolver( this ) );
134
135 options = new KSelectAction(i18n("Spider &Options"), this );
136 options->addAction( i18n("1 Suit (Easy)") );
137 options->addAction( i18n("2 Suits (Medium)") );
138 options->addAction( i18n("4 Suits (Hard)") );
139 if ( m_suits == 1 )
140 options->setCurrentItem( 0 );
141 else if ( m_suits == 2 )
142 options->setCurrentItem( 1 );
143 else
144 options->setCurrentItem( 2 );
145 connect( options, SIGNAL(triggered(int)), SLOT(gameTypeChanged()) );
146}
147
148
149QList<QAction*> Spider::configActions() const
150{
151 return QList<QAction*>() << options;
152}
153
154
155void Spider::gameTypeChanged()
156{
157 stopDemo();
158
159 if ( allowedToStartNewGame() )
160 {
161 if ( options->currentItem() == 0 )
162 setSuits( 1 );
163 else if ( options->currentItem() == 1 )
164 setSuits( 2 );
165 else
166 setSuits( 4 );
167 startNew( gameNumber() );
168 }
169 else
170 {
171 // If we're not allowed, reset the option to
172 // the current number of suits.
173 if ( m_suits == 1 )
174 options->setCurrentItem( 0 );
175 else if ( m_suits == 2 )
176 options->setCurrentItem( 1 );
177 else
178 options->setCurrentItem( 2 );
179 }
180}
181
182
183void Spider::setSuits(int suits)
184{
185 if ( suits != m_suits )
186 {
187 m_suits = suits;
188
189 stopDemo();
190 clearHighlightedItems();
191 setKeyboardModeActive( false );
192
193 int cardWidth = deck()->cardWidth();
194 createDeck();
195 deck()->setCardWidth( cardWidth );
196
197 Settings::setSpiderSuitCount( m_suits );
198
199 if ( m_suits == 1 )
200 options->setCurrentItem( 0 );
201 else if ( m_suits == 2 )
202 options->setCurrentItem( 1 );
203 else
204 options->setCurrentItem( 2 );
205 }
206}
207
208
209void Spider::createDeck()
210{
211 // These look a bit weird, but are needed to keep the game numbering
212 // from breaking. The original logic always created groupings of 4
213 // suits, while the new logic is more flexible. We maintain the card
214 // ordering by always passing a list of 4 suits even if we really only
215 // have one or two.
216 QList<KCardDeck::Suit> suits;
217 if ( m_suits == 1 )
218 suits << KCardDeck::Spades << KCardDeck::Spades << KCardDeck::Spades << KCardDeck::Spades;
219 else if ( m_suits == 2 )
220 suits << KCardDeck::Hearts << KCardDeck::Spades << KCardDeck::Hearts << KCardDeck::Spades;
221 else
222 suits << KCardDeck::Clubs << KCardDeck::Diamonds << KCardDeck::Hearts << KCardDeck::Spades;
223
224 setDeckContents( 2, suits );
225}
226
227
228bool Spider::checkAdd(const PatPile * pile, const QList<KCard*> & oldCards, const QList<KCard*> & newCards) const
229{
230 // assuming the cardlist is a valid unit, since I allowed
231 // it to be removed - can drop any card on empty pile or
232 // on any suit card of one higher rank
233 return pile->pileRole() == PatPile::Tableau
234 && ( oldCards.isEmpty()
235 || oldCards.last()->rank() == newCards.first()->rank() + 1 );
236}
237
238
239bool Spider::checkRemove(const PatPile * pile, const QList<KCard*> & cards) const
240{
241 return pile->pileRole() == PatPile::Tableau
242 && isSameSuitDescending(cards);
243}
244
245
246QString Spider::getGameState() const
247{
248 return QString::number(m_leg*10 + m_redeal);
249}
250
251
252void Spider::setGameState( const QString & state )
253{
254 int n = state.toInt();
255 int numLegs = n / 10;
256 int numRedeals = n % 10;
257
258 if ( numRedeals != m_redeal || numLegs != m_leg )
259 {
260 m_redeal = numRedeals;
261 for ( int i = 0; i < 5; ++i )
262 redeals[i]->setVisible( i >= m_redeal );
263
264 m_leg = numLegs;
265 for ( int i = 0; i < 8; ++i )
266 legs[i]->setVisible( i < m_leg );
267
268 recalculatePileLayouts();
269 foreach ( KCardPile * p, piles() )
270 updatePileLayout( p, 0 );
271
272 emit newCardsPossible(m_redeal <= 4);
273 }
274}
275
276
277QString Spider::getGameOptions() const
278{
279 return QString::number(m_suits);
280}
281
282
283void Spider::setGameOptions( const QString & options )
284{
285 setSuits(options.toInt());
286}
287
288
289void Spider::restart( const QList<KCard*> & cards )
290{
291 m_pilesWithRuns.clear();
292
293 // make the redeal piles visible
294 for (int i = 0; i < 5; ++i )
295 redeals[i]->setVisible( true );
296
297 // make the leg piles invisible
298 for (int i = 0; i < 8; ++i )
299 legs[i]->setVisible( false );
300
301 recalculatePileLayouts();
302
303 m_leg = 0;
304 m_redeal = 0;
305
306 QList<KCard*> cardList = cards;
307
308 int column = 0;
309 // deal face down cards (5 to first 4 piles, 4 to last 6)
310 for ( int i = 0; i < 44; ++i )
311 {
312 addCardForDeal( stack[column], cardList.takeLast(), false, randomPos() );
313 column = (column + 1) % 10;
314 }
315 // deal face up cards, one to each pile
316 for ( int i = 0; i < 10; ++i )
317 {
318 addCardForDeal( stack[column], cardList.takeLast(), true, randomPos() );
319 column = (column + 1) % 10;
320 }
321 // deal the remaining cards into 5 'redeal' piles
322 for ( int column = 0; column < 5; ++column )
323 for ( int i = 0; i < 10; ++i )
324 addCardForDeal( redeals[column], cardList.takeLast(), false, randomPos() );
325
326 startDealAnimation();
327
328 emit newCardsPossible(true);
329}
330
331
332void Spider::cardsMoved( const QList<KCard*> & cards, KCardPile * oldPile, KCardPile * newPile )
333{
334 PatPile * p = dynamic_cast<PatPile*>( newPile );
335
336 // The solver treats the removal of complete runs from the table as a
337 // separate move, so we don't do it automatically when the demo is active.
338 if ( !isDemoActive()
339 && p
340 && p->pileRole() == PatPile::Tableau
341 && pileHasFullRun( p ) )
342 {
343 m_pilesWithRuns << p;
344 }
345
346 DealerScene::cardsMoved( cards, oldPile, newPile );
347}
348
349
350bool Spider::pileHasFullRun( KCardPile * pile )
351{
352 QList<KCard*> potentialRun = pile->topCards( 13 );
353 return pile->count() >= 13
354 && potentialRun.first()->isFaceUp()
355 && isSameSuitDescending( potentialRun );
356}
357
358
359void Spider::moveFullRunToLeg( KCardPile * pile )
360{
361 QList<KCard*> run = pile->topCards( 13 );
362
363 PatPile * leg = legs[m_leg];
364 ++m_leg;
365 leg->setVisible( true );
366
367 recalculatePileLayouts();
368 for ( int i = 0; i < 10; ++i )
369 if ( stack[i] != pile )
370 updatePileLayout( stack[i], DURATION_RELAYOUT );
371
372 for ( int i = 0; i < run.size(); ++i )
373 {
374 KCard * c = run.at( i );
375 leg->add( c );
376 int duration = DURATION_AUTODROP * (0.7 + i / 10.0);
377 c->animate( leg->pos(), leg->zValue() + i, 0, true, true, duration );
378 }
379
380 updatePileLayout( pile, DURATION_RELAYOUT );
381}
382
383
384QPointF Spider::randomPos()
385{
386 QRectF rect = sceneRect();
387 qreal x = rect.left() + qreal(KRandom::random()) / RAND_MAX * (rect.width() - deck()->cardWidth());
388 qreal y = rect.top() + qreal(KRandom::random()) / RAND_MAX * (rect.height() - deck()->cardHeight());
389 return QPointF( x, y );
390}
391
392
393bool Spider::newCards()
394{
395 // The solver doesn't distinguish between dealing a new row of cards and
396 // removing complete runs from the tableau. So it we're in demo mode and
397 // newCards() is called, we should check to see if there are any complete
398 // runs to move before dealing a new row.
399 if ( isDemoActive() )
400 {
401 for ( int i = 0; i < 10; ++i )
402 {
403 if ( pileHasFullRun( stack[i] ) )
404 {
405 moveFullRunToLeg( stack[i] );
406 return true;
407 }
408 }
409 }
410
411 if ( m_redeal > 4 )
412 return false;
413
414 redeals[m_redeal]->setVisible(false);
415 recalculatePileLayouts();
416
417 for ( int column = 0; column < 10; ++column )
418 {
419 KCard * c = redeals[m_redeal]->topCard();
420 if ( !c )
421 break;
422
423 flipCardToPileAtSpeed( c, stack[column], DEAL_SPEED );
424 c->setZValue( c->zValue() + 10 - column );
425 }
426
427 ++m_redeal;
428
429 if (m_redeal > 4)
430 emit newCardsPossible(false);
431
432 return true;
433}
434
435
436void Spider::animationDone()
437{
438 if ( !m_pilesWithRuns.isEmpty() )
439 moveFullRunToLeg( m_pilesWithRuns.takeFirst() );
440 else
441 DealerScene::animationDone();
442}
443
444
445
446void Spider::mapOldId(int id)
447{
448 switch (id) {
449 case DealerInfo::SpiderOneSuitId :
450 setSuits(1);
451 break;
452 case DealerInfo::SpiderTwoSuitId :
453 setSuits(2);
454 break;
455 case DealerInfo::SpiderFourSuitId :
456 setSuits(4);
457 break;
458 }
459}
460
461
462int Spider::oldId() const
463{
464 switch (m_suits) {
465 case 1 :
466 return DealerInfo::SpiderOneSuitId;
467 case 2 :
468 return DealerInfo::SpiderTwoSuitId;
469 case 4 :
470 default :
471 return DealerInfo::SpiderFourSuitId;
472 }
473}
474
475
476static class SpideDealerInfo : public DealerInfo
477{
478public:
479 SpideDealerInfo()
480 : DealerInfo(I18N_NOOP("Spider"), SpiderGeneralId)
481 {
482 addSubtype( SpiderOneSuitId, I18N_NOOP( "Spider (1 Suit)" ) );
483 addSubtype( SpiderTwoSuitId, I18N_NOOP( "Spider (2 Suit)" ) );
484 addSubtype( SpiderFourSuitId, I18N_NOOP( "Spider (4 Suit)" ) );
485 }
486
487 virtual DealerScene *createGame() const
488 {
489 return new Spider( this );
490 }
491} spideDealerInfo;
492
493
494#include "spider.moc"
495