1#!/usr/bin/env python 2# -*- mode: python; coding: utf-8; -*- 3# ---------------------------------------------------------------------------## 4# 5# Copyright (C) 1998-2003 Markus Franz Xaver Johannes Oberhumer 6# Copyright (C) 2003 Mt. Hood Playing Card Co. 7# Copyright (C) 2005-2009 Skomoroh 8# 9# This program is free software: you can redistribute it and/or modify 10# it under the terms of the GNU General Public License as published by 11# the Free Software Foundation, either version 3 of the License, or 12# (at your option) any later version. 13# 14# This program is distributed in the hope that it will be useful, 15# but WITHOUT ANY WARRANTY; without even the implied warranty of 16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17# GNU General Public License for more details. 18# 19# You should have received a copy of the GNU General Public License 20# along with this program. If not, see <http://www.gnu.org/licenses/>. 21# 22# ---------------------------------------------------------------------------## 23 24from pysollib.game import Game 25from pysollib.gamedb import GI, GameInfo, registerGame 26from pysollib.hint import DefaultHint 27from pysollib.layout import Layout 28from pysollib.stack import \ 29 BasicRowStack, \ 30 InitialDealTalonStack, \ 31 InvisibleStack, \ 32 Stack, \ 33 StackWrapper, \ 34 TalonStack, \ 35 WasteStack, \ 36 WasteTalonStack 37from pysollib.util import ACE, NO_SUIT 38 39# ************************************************************************ 40# * 41# ************************************************************************ 42 43 44class Montana_Hint(DefaultHint): 45 def computeHints(self): 46 game = self.game 47 RSTEP, RBASE = game.RSTEP, game.RBASE 48 freerows = [s for s in game.s.rows if not s.cards] 49 # for each stack 50 for r in game.s.rows: 51 if not r.cards: 52 continue 53 assert len(r.cards) == 1 and r.cards[-1].face_up 54 c, pile, rpile = r.cards[0], r.cards, [] 55 if r.id % RSTEP > 0: 56 left = game.s.rows[r.id - 1] 57 else: 58 left = None 59 if c.rank == RBASE: 60 # do not move the leftmost card of a row if the 61 # rank is correct 62 continue 63 for t in freerows: 64 if self.shallMovePile(r, t, pile, rpile): 65 # FIXME: this scoring is completely simple 66 if left and left.cards: 67 # prefer low-rank left neighbours 68 score = 40000 + (self.K - left.cards[-1].rank) 69 else: 70 score = 50000 71 self.addHint(score, 1, r, t) 72 73 74# ************************************************************************ 75# * Montana 76# ************************************************************************ 77 78class Montana_Talon(TalonStack): 79 def canDealCards(self): 80 return self.round != self.max_rounds and not self.game.isGameWon() 81 82 def _inSequence(self, card, suit, rank): 83 return card.suit == suit and card.rank == rank 84 85 def dealCards(self, sound=False): 86 # move cards to the Talon, shuffle and redeal 87 game = self.game 88 decks = game.gameinfo.decks 89 RSTEP, RBASE = game.RSTEP, game.RBASE 90 num_cards = 0 91 assert len(self.cards) == 0 92 rows = game.s.rows 93 # move out-of-sequence cards from the Tableau to the Talon 94 stacks = [] 95 gaps = [None] * 4 * decks 96 for g in range(4*decks): 97 i = g * RSTEP 98 r = rows[i] 99 if r.cards and r.cards[-1].rank == RBASE: 100 in_sequence, suit = 1, r.cards[-1].suit 101 else: 102 in_sequence, suit = 0, NO_SUIT 103 for j in range(RSTEP): 104 r = rows[i + j] 105 if in_sequence: 106 if (not r.cards or 107 not self._inSequence(r.cards[-1], suit, RBASE+j)): 108 in_sequence = 0 109 if not in_sequence: 110 stacks.append(r) 111 if gaps[g] is None: 112 gaps[g] = r 113 if r.cards: 114 game.moveMove(1, r, self, frames=0) 115 num_cards = num_cards + 1 116 assert len(self.cards) == num_cards 117 assert len(stacks) == num_cards + len(gaps) 118 if num_cards == 0: # game already finished 119 return 0 120 if sound: 121 game.startDealSample() 122 # shuffle 123 game.shuffleStackMove(self) 124 # redeal 125 game.nextRoundMove(self) 126 spaces = self.getRedealSpaces(stacks, gaps) 127 for r in stacks: 128 if r not in spaces: 129 self.game.moveMove(1, self, r, frames=4) 130 # done 131 assert len(self.cards) == 0 132 if sound: 133 game.stopSamples() 134 return num_cards 135 136 def getRedealSpaces(self, stacks, gaps): 137 # the spaces are directly after the sorted sequence in each row 138 return gaps 139 140 141class Montana_RowStack(BasicRowStack): 142 def acceptsCards(self, from_stack, cards): 143 if not BasicRowStack.acceptsCards(self, from_stack, cards): 144 return False 145 if self.id % self.game.RSTEP == 0: 146 return cards[0].rank == self.game.RBASE 147 left = self.game.s.rows[self.id - 1] 148 return left.cards and left.cards[-1].suit == cards[0].suit \ 149 and left.cards[-1].rank + 1 == cards[0].rank 150 151 def clickHandler(self, event): 152 if not self.cards: 153 return self.quickPlayHandler(event) 154 return BasicRowStack.clickHandler(self, event) 155 156 getBottomImage = Stack._getBlankBottomImage 157 158 159class Montana(Game): 160 Talon_Class = StackWrapper(Montana_Talon, max_rounds=3) 161 RowStack_Class = Montana_RowStack 162 Hint_Class = Montana_Hint 163 164 RLEN, RSTEP, RBASE = 52, 13, 1 165 166 def createGame(self, round_text=True): 167 decks = self.gameinfo.decks 168 169 # create layout 170 l, s = Layout(self, card_x_space=4), self.s 171 172 # set window 173 w, h = l.XM + self.RSTEP*l.XS, l.YM + (4*decks)*l.YS 174 if round_text: 175 h += l.YS 176 self.setSize(w, h) 177 178 # create stacks 179 for k in range(decks): 180 for i in range(4): 181 x, y = l.XM, l.YM + (i+k*4)*l.YS 182 for j in range(self.RSTEP): 183 s.rows.append(self.RowStack_Class(x, y, self, 184 max_accept=1, max_cards=1)) 185 x += l.XS 186 if round_text: 187 x, y = l.XM + (self.RSTEP-1)*l.XS//2, self.height-l.YS 188 s.talon = self.Talon_Class(x, y, self) 189 l.createRoundText(s.talon, 'se') 190 else: 191 # Talon is invisible 192 x, y = self.getInvisibleCoords() 193 s.talon = self.Talon_Class(x, y, self) 194 if self.RBASE: 195 # create an invisible stack to hold the four Aces 196 s.internals.append(InvisibleStack(self)) 197 198 # define stack-groups 199 l.defaultStackGroups() 200 201 # 202 # game overrides 203 # 204 205 def startGame(self): 206 frames = 0 207 toprows = len(self.s.talon.cards) * .75 208 for i in range(len(self.s.talon.cards)): 209 c = self.s.talon.cards[-1] 210 if c.rank == ACE: 211 self.s.talon.dealRow(rows=self.s.internals, frames=0) 212 else: 213 if frames == 0 and i >= toprows: 214 self.startDealSample() 215 frames = 4 216 self.s.talon.dealRow(rows=(self.s.rows[i],), frames=frames) 217 218 def isGameWon(self): 219 rows = self.s.rows 220 for i in range(0, self.RLEN, self.RSTEP): 221 if not rows[i].cards: 222 return False 223 suit = rows[i].cards[-1].suit 224 for j in range(self.RSTEP - 1): 225 r = rows[i + j] 226 if not r.cards or r.cards[-1].rank != self.RBASE + j \ 227 or r.cards[-1].suit != suit: 228 return False 229 return True 230 231 def getHighlightPilesStacks(self): 232 return () 233 234 def getAutoStacks(self, event=None): 235 return (self.sg.dropstacks, (), self.sg.dropstacks) 236 237 shallHighlightMatch = Game._shallHighlightMatch_SS 238 239 def getQuickPlayScore(self, ncards, from_stack, to_stack): 240 if from_stack.cards: 241 if from_stack.id % self.RSTEP == 0 and \ 242 from_stack.cards[-1].rank == self.RBASE: 243 # do not move the leftmost card of a row if the rank is correct 244 return -1 245 return 1 246 247 248# ************************************************************************ 249# * Spaces 250# ************************************************************************ 251 252class Spaces_Talon(Montana_Talon): 253 def getRedealSpaces(self, stacks, gaps): 254 # use four random spaces, ignore gaps 255 # note: the random.seed is already saved in shuffleStackMove 256 spaces = [] 257 while len(spaces) != 4: 258 r = self.game.random.choice(stacks) 259 if r not in spaces: 260 spaces.append(r) 261 return spaces 262 263 264class Spaces(Montana): 265 Talon_Class = StackWrapper(Spaces_Talon, max_rounds=3) 266 267 268# ************************************************************************ 269# * Blue Moon 270# ************************************************************************ 271 272class BlueMoon(Montana): 273 RLEN, RSTEP, RBASE = 56, 14, 0 274 275 def startGame(self): 276 frames = 0 277 for i in range(self.RLEN): 278 if i == self.RLEN-self.RSTEP: # last row 279 self.startDealSample() 280 frames = -1 281 if i % self.RSTEP == 0: # left column 282 continue 283 self.s.talon.dealRow(rows=(self.s.rows[i],), frames=frames) 284 ace_rows = [r for r in self.s.rows 285 if r.cards and r.cards[-1].rank == ACE] 286 j = 0 287 for r in ace_rows: 288 self.moveMove(1, r, self.s.rows[j]) 289 j += self.RSTEP 290 291 292# ************************************************************************ 293# * Red Moon 294# ************************************************************************ 295 296class RedMoon(BlueMoon): 297 def _shuffleHook(self, cards): 298 # move Aces to top of the Talon (i.e. first cards to be dealt) 299 return self._shuffleHookMoveToTop( 300 cards, lambda c: (c.rank == 0, c.suit)) 301 302 def startGame(self): 303 decks = self.gameinfo.decks 304 frames = 0 305 r = self.s.rows 306 self.s.talon.dealRow(rows=(r[::14]), frames=frames) 307 for i in range(4*decks): 308 if i == 4*decks-1: 309 self.startDealSample() 310 frames = 4 311 n = i * 14 + 2 312 self.s.talon.dealRow(rows=r[n:n+12], frames=frames) 313 314 315# ************************************************************************ 316# * Galary 317# ************************************************************************ 318 319 320class Galary_Hint(Montana_Hint): 321 def shallMovePile(self, from_stack, to_stack, pile, rpile): 322 if from_stack is to_stack or \ 323 not to_stack.acceptsCards(from_stack, pile): 324 return False 325 # now check for loops 326 rr = self.ClonedStack(from_stack, stackcards=rpile) 327 if rr.acceptsCards(to_stack, pile): 328 # the pile we are going to move could be moved back - 329 # this is dangerous as we can create endless loops... 330 return False 331 return True 332 333 334class Galary_RowStack(Montana_RowStack): 335 def acceptsCards(self, from_stack, cards): 336 if not BasicRowStack.acceptsCards(self, from_stack, cards): 337 return False 338 if self.id % self.game.RSTEP == 0: 339 return cards[0].rank == self.game.RBASE 340 r = self.game.s.rows 341 left = r[self.id - 1] 342 if left.cards and left.cards[-1].suit == cards[0].suit \ 343 and left.cards[-1].rank + 1 == cards[0].rank: 344 return True 345 if self.id < len(r)-1: 346 right = r[self.id + 1] 347 if right.cards and right.cards[-1].suit == cards[0].suit \ 348 and right.cards[-1].rank - 1 == cards[0].rank: 349 return True 350 return False 351 352 353class Galary(RedMoon): 354 RowStack_Class = Galary_RowStack 355 Hint_Class = Galary_Hint 356 357 358# ************************************************************************ 359# * Moonlight 360# ************************************************************************ 361 362class Moonlight(Montana): 363 RowStack_Class = Galary_RowStack 364 Hint_Class = Galary_Hint 365 366 367# ************************************************************************ 368# * Jungle 369# ************************************************************************ 370 371class Jungle_RowStack(Montana_RowStack): 372 def acceptsCards(self, from_stack, cards): 373 if not BasicRowStack.acceptsCards(self, from_stack, cards): 374 return False 375 if self.id % self.game.RSTEP == 0: 376 return cards[0].rank == self.game.RBASE 377 left = self.game.s.rows[self.id - 1] 378 return left.cards and left.cards[-1].rank + 1 == cards[0].rank 379 380 381class Jungle(BlueMoon): 382 Talon_Class = StackWrapper(Montana_Talon, max_rounds=2) 383 RowStack_Class = Jungle_RowStack 384 Hint_Class = Galary_Hint 385 386 387# ************************************************************************ 388# * Spaces and Aces 389# ************************************************************************ 390 391class SpacesAndAces_RowStack(Montana_RowStack): 392 def acceptsCards(self, from_stack, cards): 393 if not BasicRowStack.acceptsCards(self, from_stack, cards): 394 return False 395 if self.id % self.game.RSTEP == 0: 396 return cards[0].rank == self.game.RBASE 397 left = self.game.s.rows[self.id - 1] 398 return left.cards and left.cards[-1].suit == cards[0].suit \ 399 and left.cards[-1].rank < cards[0].rank 400 401 402class SpacesAndAces(BlueMoon): 403 Hint_Class = Galary_Hint 404 Talon_Class = InitialDealTalonStack 405 RowStack_Class = SpacesAndAces_RowStack 406 407 def createGame(self): 408 Montana.createGame(self, round_text=False) 409 410 def startGame(self): 411 frames = 0 412 for i in range(self.RLEN): 413 if i == self.RLEN-self.RSTEP: # last row 414 self.startDealSample() 415 frames = -1 416 if i % self.RSTEP == 0: # left column 417 continue 418 self.s.talon.dealRow(rows=(self.s.rows[i],), frames=frames) 419 420# ************************************************************************ 421# * Paganini 422# ************************************************************************ 423 424 425class Paganini_Talon(Montana_Talon): 426 def _inSequence(self, card, suit, rank): 427 card_rank = card.rank 428 if card_rank >= 5: 429 card_rank -= 4 430 return card.suit == suit and card_rank == rank 431 432 433class Paganini_RowStack(Montana_RowStack): 434 def acceptsCards(self, from_stack, cards): 435 if not BasicRowStack.acceptsCards(self, from_stack, cards): 436 return False 437 if self.id % self.game.RSTEP == 0: 438 return cards[0].rank == self.game.RBASE 439 left = self.game.s.rows[self.id - 1] 440 if not left.cards: 441 return False 442 if left.cards[-1].suit != cards[0].suit: 443 return False 444 if left.cards[-1].rank == ACE: 445 return cards[0].rank == 5 446 return left.cards[-1].rank+1 == cards[0].rank 447 448 449class Paganini(BlueMoon): 450 RLEN, RSTEP, RBASE = 40, 10, 0 451 452 Talon_Class = StackWrapper(Paganini_Talon, max_rounds=2) 453 RowStack_Class = Paganini_RowStack 454 455 def isGameWon(self): 456 rows = self.s.rows 457 for i in range(0, self.RLEN, self.RSTEP): 458 if not rows[i].cards: 459 return False 460 suit = rows[i].cards[-1].suit 461 for j in range(self.RSTEP - 1): 462 r = rows[i + j] 463 if not r.cards: 464 return False 465 card = r.cards[-1] 466 card_rank = card.rank 467 if card_rank >= 5: 468 card_rank -= 4 469 if card_rank != self.RBASE + j or card.suit != suit: 470 return False 471 return True 472 473 474# ************************************************************************ 475# * Spoilt 476# ************************************************************************ 477 478class Spoilt_RowStack(BasicRowStack): 479 def acceptsCards(self, from_stack, cards): 480 # if not BasicRowStack.acceptsCards(self, from_stack, cards): 481 # return False 482 483 card = cards[0] 484 RSTEP = self.game.RSTEP 485 RBASE = self.game.RBASE 486 row, col = divmod(self.id, RSTEP) 487 # check rank 488 if card.rank == ACE: 489 if col != RSTEP-1: 490 return False 491 else: 492 if card.rank - RBASE != col: 493 return False 494 # check suit 495 suit = None 496 for i in range(row*RSTEP, (row+1)*RSTEP): 497 r = self.game.s.rows[i] 498 if r.cards and r.cards[0].face_up: 499 suit = r.cards[0].suit 500 break 501 if suit is not None: 502 return card.suit == suit 503 for r in self.game.s.rows: # check other rows 504 if r.cards and r.cards[0].face_up and r.cards[0].suit == card.suit: 505 return False 506 return True 507 508 def canFlipCard(self): 509 return False 510 511 512class Spoilt_Waste(WasteStack): 513 514 def moveMove(self, ncards, to_stack, frames=-1, shadow=-1): 515 assert ncards == 1 and to_stack in self.game.s.rows 516 if to_stack.cards: 517 self._swapPairMove(ncards, to_stack, frames=-1, shadow=0) 518 else: 519 WasteStack.moveMove(self, ncards, to_stack, frames, shadow) 520 521 def _swapPairMove(self, n, other_stack, frames=-1, shadow=-1): 522 game = self.game 523 old_state = game.enterState(game.S_FILL) 524 swap = game.s.internals[0] 525 game.flipMove(other_stack) 526 game.moveMove(n, self, swap, frames=0) 527 game.moveMove(n, other_stack, self, frames=frames, shadow=shadow) 528 game.moveMove(n, swap, other_stack, frames=0) 529 game.leaveState(old_state) 530 531 532class Spoilt(Game): 533 RSTEP, RBASE = 8, 6 534 535 def createGame(self): 536 # create layout 537 l, s = Layout(self), self.s 538 539 # set window 540 self.setSize(l.XM + self.RSTEP*l.XS, l.YM + 5.5*l.YS) 541 542 # create stacks 543 for i in range(4): 544 x, y, = l.XM, l.YM + i*l.YS 545 for j in range(self.RSTEP): 546 s.rows.append(Spoilt_RowStack(x, y, self, 547 max_accept=1, max_cards=2, min_cards=1)) 548 x += l.XS 549 x, y = self.width//2 - l.XS, self.height-l.YS 550 s.talon = WasteTalonStack(x, y, self, max_rounds=1) 551 l.createText(s.talon, 'n') 552 x += l.XS 553 s.waste = Spoilt_Waste(x, y, self, max_cards=1) 554 555 # create an invisible stack 556 s.internals.append(InvisibleStack(self)) 557 558 # define stack-groups 559 l.defaultStackGroups() 560 561 def startGame(self): 562 self.startDealSample() 563 for i in range(4): 564 rows = self.s.rows[self.RSTEP*i+1:self.RSTEP*(i+1)] 565 self.s.talon.dealRow(rows=rows, frames=4, flip=False) 566 self.s.talon.dealCards() 567 568 def isGameWon(self): 569 for r in self.s.rows: 570 if not r.cards: 571 return False 572 if not r.cards[0].face_up: 573 return False 574 return True 575 576 def getHighlightPilesStacks(self): 577 return () 578 579 def getAutoStacks(self, event=None): 580 return (), (), () 581 582 583# ************************************************************************ 584# * Double Montana 585# ************************************************************************ 586 587class DoubleMontana(Montana): 588 Talon_Class = InitialDealTalonStack 589 Hint_Class = Galary_Hint 590 RLEN, RSTEP, RBASE = 112, 14, 0 591 592 def createGame(self): 593 Montana.createGame(self, round_text=False) 594 595 def startGame(self): 596 frames = 0 597 for i in range(self.RLEN): 598 if i == self.RLEN-self.RSTEP: # last row 599 self.startDealSample() 600 frames = -1 601 if i % self.RSTEP == 0: # left column 602 continue 603 self.s.talon.dealRow(rows=(self.s.rows[i],), frames=frames) 604 605 606class DoubleBlueMoon(DoubleMontana, BlueMoon): 607 Talon_Class = StackWrapper(Montana_Talon, max_rounds=3) 608 RLEN, RSTEP, RBASE = 112, 14, 0 609 610 def createGame(self): 611 BlueMoon.createGame(self, round_text=True) 612 startGame = BlueMoon.startGame 613 614 615class DoubleRedMoon(DoubleMontana, RedMoon): 616 Talon_Class = StackWrapper(Montana_Talon, max_rounds=3) 617 RLEN, RROWS = 112, 8 618 _shuffleHook = RedMoon._shuffleHook 619 620 def createGame(self): 621 RedMoon.createGame(self, round_text=True) 622 startGame = RedMoon.startGame 623 624 625# ************************************************************************ 626# * House of Commons 627# * Pretzel 628# ************************************************************************ 629 630 631class HouseOfCommons(Montana): 632 Talon_Class = StackWrapper(Montana_Talon, max_rounds=2) 633 RLEN, RSTEP, RBASE = 40, 10, 1 634 635 def createGame(self): 636 Montana.createGame(self, round_text=True) 637 638 639class Pretzel(Montana): 640 Talon_Class = InitialDealTalonStack 641 RLEN, RSTEP, RBASE = 20, 5, 1 642 643 def createGame(self): 644 Montana.createGame(self, round_text=False) 645 646 647# register the game 648registerGame(GameInfo(53, Montana, "Montana", 649 GI.GT_MONTANA | GI.GT_OPEN, 1, 2, GI.SL_MOSTLY_SKILL, 650 si={"ncards": 48}, altnames="Gaps")) 651registerGame(GameInfo(116, Spaces, "Spaces", 652 GI.GT_MONTANA | GI.GT_OPEN, 1, 2, GI.SL_MOSTLY_SKILL, 653 si={"ncards": 48}, altnames="Addiction")) 654registerGame(GameInfo(63, BlueMoon, "Blue Moon", 655 GI.GT_MONTANA | GI.GT_OPEN, 1, 2, GI.SL_MOSTLY_SKILL, 656 altnames=("Rangoon",))) 657registerGame(GameInfo(117, RedMoon, "Red Moon", 658 GI.GT_MONTANA | GI.GT_OPEN, 1, 2, GI.SL_MOSTLY_SKILL)) 659registerGame(GameInfo(275, Galary, "Galary", 660 GI.GT_MONTANA | GI.GT_OPEN | GI.GT_ORIGINAL, 1, 2, 661 GI.SL_MOSTLY_SKILL)) 662registerGame(GameInfo(276, Moonlight, "Moonlight", 663 GI.GT_MONTANA | GI.GT_OPEN, 1, 2, GI.SL_MOSTLY_SKILL, 664 si={"ncards": 48}, altnames="Free Parking")) 665registerGame(GameInfo(380, Jungle, "Jungle", 666 GI.GT_MONTANA | GI.GT_OPEN, 1, 1, GI.SL_MOSTLY_SKILL)) 667registerGame(GameInfo(381, SpacesAndAces, "Spaces and Aces", 668 GI.GT_MONTANA | GI.GT_OPEN, 1, 0, GI.SL_MOSTLY_SKILL)) 669registerGame(GameInfo(706, Paganini, "Paganini", 670 GI.GT_MONTANA | GI.GT_OPEN, 1, 1, GI.SL_MOSTLY_SKILL, 671 ranks=(0, 5, 6, 7, 8, 9, 10, 11, 12), 672 altnames=('Long Trip',))) 673registerGame(GameInfo(736, Spoilt, "Spoilt", 674 GI.GT_MONTANA, 1, 0, GI.SL_MOSTLY_LUCK, 675 ranks=(0, 6, 7, 8, 9, 10, 11, 12))) 676registerGame(GameInfo(759, DoubleMontana, "Double Montana", 677 GI.GT_MONTANA | GI.GT_OPEN, 2, 0, GI.SL_MOSTLY_SKILL)) 678registerGame(GameInfo(770, DoubleBlueMoon, "Double Blue Moon", 679 GI.GT_MONTANA | GI.GT_OPEN, 2, 2, GI.SL_MOSTLY_SKILL)) 680registerGame(GameInfo(771, DoubleRedMoon, "Double Red Moon", 681 GI.GT_MONTANA | GI.GT_OPEN, 2, 2, GI.SL_MOSTLY_SKILL)) 682registerGame(GameInfo(794, HouseOfCommons, "House of Commons", 683 GI.GT_MONTANA | GI.GT_OPEN, 1, 1, GI.SL_MOSTLY_SKILL, 684 ranks=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), si={"ncards": 36})) 685registerGame(GameInfo(795, Pretzel, "Pretzel", 686 GI.GT_MONTANA | GI.GT_OPEN, 1, 0, GI.SL_MOSTLY_SKILL, 687 ranks=(0, 1, 2, 3, 4), si={"ncards": 16})) 688