| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| part of solitaire; |
| |
| class SolitaireCommand extends GameCommand { |
| // Usually this constructor is used when reading from a log/syncbase. |
| SolitaireCommand(String phase, String data) |
| : super(phase, data, simultaneity: SimulLevel.TURN_BASED); |
| |
| SolitaireCommand.fromCommand(String cmd) |
| : super(cmd.split("|")[0], cmd.split("|")[1], |
| simultaneity: SimulLevel.TURN_BASED); |
| |
| // The following constructors are used for the player generating the SolitaireCommand. |
| SolitaireCommand.deal(List<Card> allCards) |
| : super("Deal", computeDeal(allCards), |
| simultaneity: SimulLevel.TURN_BASED); |
| |
| SolitaireCommand.move(Card target, int targetPile) |
| : super("Move", computeMove(target, targetPile), |
| simultaneity: SimulLevel.TURN_BASED); |
| |
| SolitaireCommand.draw() |
| : super("Draw", computeDraw(), simultaneity: SimulLevel.TURN_BASED); |
| |
| SolitaireCommand.flip(int targetPile) |
| : super("Flip", computeFlip(targetPile), |
| simultaneity: SimulLevel.TURN_BASED); |
| |
| static String computeDeal(List<Card> allCards) { |
| StringBuffer buff = new StringBuffer(); |
| allCards.forEach((card) => buff.write("${card.toString()}:")); |
| buff.write("END"); |
| return buff.toString(); |
| } |
| |
| // Note: Depending on the target's position w.r.t. the targetPile, this may |
| // actually move a group of cards instead. |
| static String computeMove(Card target, int targetPile) { |
| return "${target.toString()}:${targetPile}:END"; |
| } |
| |
| // Note: If there are no cards to draw, this will reset the draw pile. |
| static String computeDraw() { |
| return "END"; |
| } |
| |
| static String computeFlip(int targetPile) { |
| return "${targetPile}:END"; |
| } |
| |
| @override |
| bool canExecute(Game g) { |
| // TODO(alexfandrianto): This is very similar to execute, but without the |
| // mutations. It's possible to use a shared function to simplify/combine the |
| // logic. |
| SolitaireGame game = g as SolitaireGame; |
| |
| print("SolitaireCommand is checking: ${command}"); |
| List<String> parts = data.split(":"); |
| switch (phase) { |
| case "Deal": |
| return game.phase == SolitairePhase.Deal && parts.length - 1 == 52; |
| case "Move": |
| if (game.phase != SolitairePhase.Play) { |
| return false; |
| } |
| |
| // Move the card to the pile. |
| Card c = new Card.fromString(parts[0]); |
| int targetId = int.parse(parts[1]); |
| int sourceId = game.findCard(c); |
| if (sourceId == -1) { |
| return false; |
| } |
| if (targetId < 0 || targetId >= game.cardCollections.length) { |
| return false; |
| } |
| List<Card> source = game.cardCollections[sourceId]; |
| List<Card> dest = game.cardCollections[targetId]; |
| |
| // If the card isn't valid, then we have an error. |
| String reason = game.canPlay(c, dest); |
| if (reason != null) { |
| return false; |
| } |
| bool canTransfer = this.transferCheck(source, dest, c); |
| return canTransfer; |
| case "Draw": |
| if (game.phase != SolitairePhase.Play) { |
| return false; |
| } |
| |
| List<Card> drawPile = game.cardCollections[SolitaireGame.OFFSET_DRAW]; |
| List<Card> discardPile = |
| game.cardCollections[SolitaireGame.OFFSET_DISCARD]; |
| |
| return drawPile.length > 0 || discardPile.length > 0; |
| case "Flip": |
| if (game.phase != SolitairePhase.Play) { |
| return false; |
| } |
| |
| int flipId = int.parse(parts[0]); |
| if (flipId < 0 || flipId >= 7) { |
| return false; |
| } |
| |
| List<Card> flipSource = |
| game.cardCollections[SolitaireGame.OFFSET_DOWN + flipId]; |
| List<Card> flipDest = |
| game.cardCollections[SolitaireGame.OFFSET_UP + flipId]; |
| |
| return flipDest.length == 0 && flipSource.length > 0; |
| default: |
| print(data); |
| assert(false); // How could this have happened? |
| return false; |
| } |
| } |
| |
| @override |
| void execute(Game g) { |
| SolitaireGame game = g as SolitaireGame; |
| |
| print("SolitaireCommand is executing: ${command}"); |
| List<String> parts = data.split(":"); |
| switch (phase) { |
| case "Deal": |
| if (game.phase != SolitairePhase.Deal) { |
| throw new StateError( |
| "Cannot process deal commands when not in Deal phase"); |
| } |
| if (parts.length - 1 != 52) { |
| throw new StateError( |
| "Not enough cards dealt. Need 52, got ${parts.length - 1}"); |
| } |
| |
| // Deal fills out each of the down cards with one of each up card. |
| int index = 0; |
| for (int i = 0; i < 7; i++) { |
| for (int j = 0; j < i; j++) { |
| this.transfer( |
| game.deck, |
| game.cardCollections[SolitaireGame.OFFSET_DOWN + i], |
| new Card.fromString(parts[index])); |
| index++; |
| } |
| this.transfer( |
| game.deck, |
| game.cardCollections[SolitaireGame.OFFSET_UP + i], |
| new Card.fromString(parts[index])); |
| index++; |
| } |
| |
| // The remaining cards are for the draw pile. |
| for (; index < 52; index++) { |
| this.transfer( |
| game.deck, |
| game.cardCollections[SolitaireGame.OFFSET_DRAW], |
| new Card.fromString(parts[index])); |
| } |
| return; |
| case "Move": |
| if (game.phase != SolitairePhase.Play) { |
| throw new StateError( |
| "Cannot process move commands when not in Play phase"); |
| } |
| |
| // Move the card to the pile. |
| Card c = new Card.fromString(parts[0]); |
| int targetId = int.parse(parts[1]); |
| int sourceId = game.findCard(c); |
| if (sourceId == -1) { |
| throw new StateError("Cannot move unknown card ${c.toString()}"); |
| } |
| if (targetId < 0 || targetId >= game.cardCollections.length) { |
| throw new StateError("Cannot move to unknown pile ${targetId}"); |
| } |
| List<Card> source = game.cardCollections[sourceId]; |
| List<Card> dest = game.cardCollections[targetId]; |
| |
| // If the card isn't valid, then we have an error. |
| String reason = game.canPlay(c, dest); |
| if (reason != null) { |
| throw new StateError( |
| "Cannot move ${c.toString()} to Pile ${targetId} because ${reason}"); |
| } |
| this.transferGroup(source, dest, c); |
| return; |
| case "Draw": |
| if (game.phase != SolitairePhase.Play) { |
| throw new StateError( |
| "Cannot process draw commands when not in Play phase"); |
| } |
| |
| List<Card> drawPile = game.cardCollections[SolitaireGame.OFFSET_DRAW]; |
| List<Card> discardPile = |
| game.cardCollections[SolitaireGame.OFFSET_DISCARD]; |
| |
| if (drawPile.length != 0) { |
| this.transfer(drawPile, discardPile, drawPile[0]); |
| } else if (discardPile.length != 0) { |
| this.transferGroup(discardPile, drawPile, discardPile[0]); |
| } else { |
| throw new StateError("No cards left to draw"); |
| } |
| return; |
| case "Flip": |
| if (game.phase != SolitairePhase.Play) { |
| throw new StateError( |
| "Cannot process flip commands when not in Play phase"); |
| } |
| |
| int flipId = int.parse(parts[0]); |
| if (flipId < 0 || flipId >= 7) { |
| throw new StateError( |
| "Cannot process flip command for index ${flipId}"); |
| } |
| |
| List<Card> flipSource = |
| game.cardCollections[SolitaireGame.OFFSET_DOWN + flipId]; |
| List<Card> flipDest = |
| game.cardCollections[SolitaireGame.OFFSET_UP + flipId]; |
| |
| if (flipDest.length != 0) { |
| throw new StateError( |
| "Cannot flip ${flipId} because destination has cards"); |
| } |
| if (flipSource.length == 0) { |
| throw new StateError( |
| "Cannot flip ${flipId} because source has no cards"); |
| } |
| this.transfer(flipSource, flipDest, flipSource[flipSource.length - 1]); |
| return; |
| default: |
| print(data); |
| assert(false); // How could this have happened? |
| } |
| } |
| |
| void transfer(List<Card> sender, List<Card> receiver, Card c) { |
| if (!sender.contains(c)) { |
| throw new StateError( |
| "Sender ${sender.toString()} lacks Card ${c.toString()}"); |
| } |
| sender.remove(c); |
| receiver.add(c); |
| } |
| |
| // Transfers every card from a certain cutoff card onwards. |
| void transferGroup(List<Card> sender, List<Card> receiver, Card c) { |
| int index = sender.indexOf(c); |
| if (index == -1) { |
| throw new StateError( |
| "Sender ${sender.toString()} lacks Card ${c.toString()}"); |
| } |
| List<Card> lost = |
| new List<Card>.from(sender.getRange(index, sender.length)); |
| sender.removeRange(index, sender.length); |
| receiver.addAll(lost); |
| } |
| |
| bool transferCheck(List<Card> sender, List<Card> receiver, Card c) { |
| return sender.contains(c); |
| } |
| } |