/*
* $Id$
*
* Copyright (c) 2004-2012 by Rodney Kinney, Brent Easton
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License (LGPL) as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, copies are available
* at http://www.opensource.org.
*/
package VASSAL.counters;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import javax.swing.Action;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import VASSAL.build.BadDataReport;
import VASSAL.build.GameModule;
import VASSAL.build.module.Chatter;
import VASSAL.build.module.Map;
import VASSAL.build.module.PlayerRoster;
import VASSAL.build.module.map.DeckGlobalKeyCommand;
import VASSAL.build.module.map.DrawPile;
import VASSAL.build.module.map.StackMetrics;
import VASSAL.build.module.properties.MutableProperty;
import VASSAL.build.module.properties.PropertySource;
import VASSAL.command.AddPiece;
import VASSAL.command.ChangeTracker;
import VASSAL.command.Command;
import VASSAL.command.CommandEncoder;
import VASSAL.command.NullCommand;
import VASSAL.configure.ColorConfigurer;
import VASSAL.configure.PropertyExpression;
import VASSAL.i18n.Localization;
import VASSAL.i18n.Resources;
import VASSAL.tools.ArrayUtils;
import VASSAL.tools.ErrorDialog;
import VASSAL.tools.FormattedString;
import VASSAL.tools.NamedKeyStroke;
import VASSAL.tools.NamedKeyStrokeListener;
import VASSAL.tools.ReadErrorDialog;
import VASSAL.tools.ScrollPane;
import VASSAL.tools.SequenceEncoder;
import VASSAL.tools.WriteErrorDialog;
import VASSAL.tools.filechooser.FileChooser;
import VASSAL.tools.io.IOUtils;
/**
* A collection of pieces that behaves like a deck, i.e.: Doesn't move.
* Can't be expanded. Can be shuffled. Can be turned face-up and face-down.
*/
public class Deck extends Stack implements PlayerRoster.SideChangeListener {
public static final String ID = "deck;"; //$NON-NLS-1$
public static final String ALWAYS = "Always";
public static final String NEVER = "Never";
public static final String USE_MENU = "Via right-click Menu";
public static final String NO_USER = "nobody"; // Dummy user ID for turning
protected static StackMetrics deckStackMetrics = new StackMetrics(false,2,2,2,2);
// cards face down
protected boolean drawOutline = true;
protected Color outlineColor = Color.black;
protected Dimension size = new Dimension(40, 40);
protected boolean shuffle = true;
protected String faceDownOption = ALWAYS;
protected String shuffleOption = ALWAYS;
protected String shuffleCommand = "";
protected boolean allowMultipleDraw = false;
protected boolean allowSelectDraw = false;
protected boolean reversible = false;
protected String reshuffleCommand = ""; //$NON-NLS-1$
protected String reshuffleTarget;
protected String reshuffleMsgFormat;
protected NamedKeyStrokeListener reshuffleListener;
protected NamedKeyStroke reshuffleKey;
protected String reverseMsgFormat;
protected String reverseCommand;
protected NamedKeyStroke reverseKey;
protected NamedKeyStrokeListener reverseListener;
protected String shuffleMsgFormat;
protected NamedKeyStrokeListener shuffleListener;
protected NamedKeyStroke shuffleKey;
protected String faceDownMsgFormat;
protected boolean drawFaceUp;
protected boolean persistable;
protected FormattedString selectDisplayProperty = new FormattedString("$"+BasicPiece.BASIC_NAME+"$");
protected String selectSortProperty = "";
protected MutableProperty.Impl countProperty =
new MutableProperty.Impl("",this);
protected List<MutableProperty.Impl> expressionProperties =
new ArrayList<MutableProperty.Impl>();
protected String deckName;
protected String localizedDeckName;
protected boolean faceDown;
protected int dragCount = 0;
protected int maxStack = 10;
protected CountExpression[] countExpressions = new CountExpression[0];
protected boolean expressionCounting = false;
protected List<GamePiece> nextDraw = null;
protected KeyCommand[] commands;
protected List<DeckGlobalKeyCommand> globalCommands =
new ArrayList<DeckGlobalKeyCommand>();
protected boolean hotkeyOnEmpty;
protected NamedKeyStroke emptyKey;
protected boolean restrictOption;
protected PropertyExpression restrictExpression = new PropertyExpression();
protected PropertySource propertySource;
protected CommandEncoder commandEncoder = new CommandEncoder() {
public Command decode(String command) {
Command c = null;
if (command.startsWith(LoadDeckCommand.PREFIX)) {
c = new LoadDeckCommand(Deck.this);
}
return c;
}
public String encode(Command c) {
String s = null;
if (c instanceof LoadDeckCommand) {
s = LoadDeckCommand.PREFIX;
}
return s;
}
};
public Deck() {
this(ID);
PlayerRoster.addSideChangeListener(this);
}
public Deck(String type) {
mySetType(type);
PlayerRoster.addSideChangeListener(this);
}
public Deck (String type, PropertySource source) {
this(type);
propertySource = source;
}
public void setPropertySource(PropertySource source) {
propertySource = source;
if (globalCommands != null) {
for (int i = 0; i < globalCommands.size(); i++) {
globalCommands.get(i).setPropertySource(propertySource);
}
}
}
public void sideChanged(String oldSide, String newSide) {
updateCountsAll();
}
public void addGlobalKeyCommand(DeckGlobalKeyCommand globalCommand) {
globalCommands.add(globalCommand);
}
public void removeGlobalKeyCommand(DeckGlobalKeyCommand globalCommand) {
globalCommands.remove(globalCommand);
}
protected String[] getGlobalCommands() {
String[] commands = new String[globalCommands.size()];
for (int i = 0; i < globalCommands.size(); i++) {
commands[i] = globalCommands.get(i).encode();
}
return commands;
}
protected void setGlobalCommands(String[] commands) {
globalCommands = new ArrayList<DeckGlobalKeyCommand>(commands.length);
for (int i = 0; i < commands.length; i++) {
globalCommands.add(new DeckGlobalKeyCommand(commands[i], propertySource));
}
}
/**
* Update map-level count properties for all "expressions" of pieces that are configured
* to be counted. These are held in the String[] countExpressions.
*/
private void updateCountsAll() {
if (!doesExpressionCounting() || getMap() == null) {
return;
}
//Clear out all of the registered count expressions
for (int index = 0; index < countExpressions.length; index++) {
expressionProperties.get(index).setPropertyValue("0"); //$NON-NLS-1$
}
//Increase all of the pieces with expressions specified in this deck
for (Iterator<GamePiece> i = getPiecesIterator(); i.hasNext();) {
final GamePiece p = i.next();
if (p != null) {
updateCounts(p,true);
}
}
}
/**
* Update map-level count property for a piece located at index
* @param index, increase
*/
private void updateCounts(int index, boolean increase) {
if (!doesExpressionCounting()) {
return;
}
if (index >= 0 && index < contents.length) {
GamePiece p = getPieceAt(index);
if (p == null) {
//can't figure out the piece, do a full update
updateCountsAll();
}
else {
updateCounts(p,increase);
}
}
else {
//can't figure out the piece, do a full update
updateCountsAll();
}
}
/**
* Update map-level count property for a piece
* @param piece, increase
*/
private void updateCounts(GamePiece p, boolean increase) {
if (!doesExpressionCounting() || getMap() == null) {
return;
}
//test all the expressions for this deck
for (int index = 0;index < countExpressions.length;index++) {
MutableProperty.Impl prop = expressionProperties.get(index);
FormattedString formatted =
new FormattedString(countExpressions[index].getExpression());
PieceFilter f = PropertiesPieceFilter.parse(formatted.getText());
if (f.accept(p)) {
String mapProperty = prop.getPropertyValue();
if (mapProperty != null) {
int newValue = (Integer.decode(mapProperty)).intValue();
if (increase) {
newValue++;
}
else {
newValue--;
}
prop.setPropertyValue(String.valueOf(newValue));
}
}
}
}
/**
* Set the <deckName>_numPieces property in the containing Map
* @param oldPieceCount
*/
protected void fireNumCardsProperty() {
countProperty.setPropertyValue(String.valueOf(pieceCount));
}
protected void insertPieceAt(GamePiece p, int index) {
super.insertPieceAt(p, index);
updateCounts(p,true);
fireNumCardsProperty();
}
protected void removePieceAt(int index) {
int startCount = pieceCount;
updateCounts(index,false);
super.removePieceAt(index);
fireNumCardsProperty();
if (hotkeyOnEmpty && emptyKey != null && startCount > 0 && pieceCount == 0) {
GameModule.getGameModule().fireKeyStroke(emptyKey);
}
}
public void removeAll() {
super.removeAll();
updateCountsAll();
fireNumCardsProperty();
}
public void setMap(Map map) {
if (map != getMap()) {
countProperty.removeFromContainer();
if (map != null) countProperty.addTo(map);
for (MutableProperty.Impl prop : expressionProperties) {
prop.removeFromContainer();
if (map != null) prop.addTo(map);
}
}
super.setMap(map);
updateCountsAll();
fireNumCardsProperty();
}
protected void mySetType(String type) {
SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(type, ';');
st.nextToken();
drawOutline = st.nextBoolean(true);
outlineColor = ColorConfigurer.stringToColor(st.nextToken("0,0,0")); //$NON-NLS-1$
size.setSize(st.nextInt(40), st.nextInt(40));
faceDownOption = st.nextToken("Always"); //$NON-NLS-1$
shuffleOption = st.nextToken("Always"); //$NON-NLS-1$
allowMultipleDraw = st.nextBoolean(true);
allowSelectDraw = st.nextBoolean(true);
reversible = st.nextBoolean(true);
reshuffleCommand = st.nextToken(""); //$NON-NLS-1$
reshuffleTarget = st.nextToken(""); //$NON-NLS-1$
reshuffleMsgFormat = st.nextToken(""); //$NON-NLS-1$
setDeckName(st.nextToken("Deck"));
shuffleMsgFormat = st.nextToken(""); //$NON-NLS-1$
reverseMsgFormat = st.nextToken(""); //$NON-NLS-1$
faceDownMsgFormat = st.nextToken(""); //$NON-NLS-1$
drawFaceUp = st.nextBoolean(false);
persistable = st.nextBoolean(false);
shuffleKey = st.nextNamedKeyStroke(null);
reshuffleKey = st.nextNamedKeyStroke(null);
maxStack = st.nextInt(10);
setCountExpressions(st.nextStringArray(0));
expressionCounting = st.nextBoolean(false);
setGlobalCommands(st.nextStringArray(0));
hotkeyOnEmpty = st.nextBoolean(false);
emptyKey = st.nextNamedKeyStroke(null);
selectDisplayProperty.setFormat(st.nextToken("$"+BasicPiece.BASIC_NAME+"$"));
selectSortProperty = st.nextToken("");
restrictOption = st.nextBoolean(false);
restrictExpression.setExpression(st.nextToken(""));
shuffleCommand = st.nextToken(Resources.getString("Deck.shuffle"));
reverseCommand = st.nextToken(Resources.getString("Deck.reverse"));
reverseKey = st.nextNamedKeyStroke(null);
if (shuffleListener == null) {
shuffleListener = new NamedKeyStrokeListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
GameModule.getGameModule().sendAndLog(shuffle());
repaintMap();
}
});
GameModule.getGameModule().addKeyStrokeListener(shuffleListener);
}
shuffleListener.setKeyStroke(getShuffleKey());
if (reshuffleListener == null) {
reshuffleListener = new NamedKeyStrokeListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
GameModule.getGameModule().sendAndLog(sendToDeck());
repaintMap();
}
});
GameModule.getGameModule().addKeyStrokeListener(reshuffleListener);
}
reshuffleListener.setKeyStroke(getReshuffleKey());
if (reverseListener == null) {
reverseListener = new NamedKeyStrokeListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
GameModule.getGameModule().sendAndLog(reverse());
repaintMap();
}
});
GameModule.getGameModule().addKeyStrokeListener(reverseListener);
}
reverseListener.setKeyStroke(getReverseKey());
final DrawPile myPile = DrawPile.findDrawPile(getDeckName());
if (myPile != null && myPile.getDeck() == null) {
myPile.setDeck(this);
}
}
public String getFaceDownOption() {
return faceDownOption;
}
/**
* @return true if cards are turned face up when drawn from this deck
*/
public boolean isDrawFaceUp() {
return drawFaceUp;
}
public void setDrawFaceUp(boolean drawFaceUp) {
this.drawFaceUp = drawFaceUp;
}
public void setFaceDownOption(String faceDownOption) {
this.faceDownOption = faceDownOption;
faceDown = !faceDownOption.equals(NEVER);
}
public Dimension getSize() {
return size;
}
public void setSize(Dimension size) {
this.size.setSize(size);
}
public String getShuffleOption() {
return shuffleOption;
}
public void setShuffleOption(String shuffleOption) {
this.shuffleOption = shuffleOption;
}
public boolean isShuffle() {
return shuffle;
}
public int getMaxStack() {
return maxStack;
}
public int getMaximumVisiblePieceCount() {
return Math.min(pieceCount, maxStack);
}
public String[] getCountExpressions() {
String[] fullstrings = new String[countExpressions.length];
for (int index = 0; index < countExpressions.length;index++) {
fullstrings[index] = countExpressions[index].getFullString();
}
return fullstrings;
}
public boolean doesExpressionCounting() {
return expressionCounting;
}
public String getFaceDownMsgFormat() {
return faceDownMsgFormat;
}
public void setFaceDownMsgFormat(String faceDownMsgFormat) {
this.faceDownMsgFormat = faceDownMsgFormat;
}
public String getReverseMsgFormat() {
return reverseMsgFormat;
}
public void setReverseMsgFormat(String reverseMsgFormat) {
this.reverseMsgFormat = reverseMsgFormat;
}
public String getReverseCommand() {
return reverseCommand;
}
public void setReverseCommand(String s) {
reverseCommand = s;
}
public NamedKeyStroke getReverseKey() {
return reverseKey;
}
public void setReverseKey(NamedKeyStroke reverseKey) {
this.reverseKey = reverseKey;
}
public String getShuffleMsgFormat() {
return shuffleMsgFormat;
}
public void setShuffleMsgFormat(String shuffleMsgFormat) {
this.shuffleMsgFormat = shuffleMsgFormat;
}
public NamedKeyStroke getShuffleKey() {
return shuffleKey;
}
public void setShuffleKey(NamedKeyStroke shuffleKey) {
this.shuffleKey = shuffleKey;
}
public String getShuffleCommand() {
return shuffleCommand;
}
public void setShuffleCommand(String s) {
shuffleCommand = s;
}
public void setShuffle(boolean shuffle) {
this.shuffle = shuffle;
}
public boolean isAllowMultipleDraw() {
return allowMultipleDraw;
}
public void setAllowMultipleDraw(boolean allowMultipleDraw) {
this.allowMultipleDraw = allowMultipleDraw;
}
public boolean isAllowSelectDraw() {
return allowSelectDraw;
}
public void setMaxStack(int maxStack) {
this.maxStack = maxStack;
}
public void setCountExpressions(String[] countExpressionsString) {
CountExpression[] c = new CountExpression[countExpressionsString.length];
int goodExpressionCount = 0;
for (int index = 0; index < countExpressionsString.length;index++) {
CountExpression n = new CountExpression(countExpressionsString[index]);
if (n.getName() != null) {
c[index] = n;
goodExpressionCount++;
}
}
this.countExpressions = ArrayUtils.copyOf(c, goodExpressionCount);
while (countExpressions.length > expressionProperties.size()) {
expressionProperties.add(new MutableProperty.Impl("",this));
}
for (int i = 0; i < countExpressions.length; i++) {
expressionProperties.get(i).setPropertyName(
deckName+"_"+countExpressions[i].getName());
}
}
public void setExpressionCounting(boolean expressionCounting) {
this.expressionCounting = expressionCounting;
}
public void setAllowSelectDraw(boolean allowSelectDraw) {
this.allowSelectDraw = allowSelectDraw;
}
public boolean isReversible() {
return reversible;
}
public void setReversible(boolean reversible) {
this.reversible = reversible;
}
public void setDeckName(String n) {
if (Localization.getInstance().isTranslationInProgress()) {
localizedDeckName = n;
}
else {
deckName = n;
}
countProperty.setPropertyName(deckName+"_numPieces");
for (int i=0;i<countExpressions.length;++i) {
expressionProperties.get(i).setPropertyName(
deckName+"_"+countExpressions[i].getName());
}
}
public String getDeckName() {
return deckName;
}
public String getLocalizedDeckName() {
return localizedDeckName == null ? deckName : localizedDeckName;
}
/**
* The popup menu text for the command that sends the entire deck to another
* deck
*
* @return
*/
public String getReshuffleCommand() {
return reshuffleCommand;
}
public void setReshuffleCommand(String reshuffleCommand) {
this.reshuffleCommand = reshuffleCommand;
}
public NamedKeyStroke getReshuffleKey() {
return reshuffleKey;
}
public void setReshuffleKey(NamedKeyStroke reshuffleKey) {
this.reshuffleKey = reshuffleKey;
}
/**
* The name of the {@link VASSAL.build.module.map.DrawPile} to which the
* contents of this deck will be sent when the reshuffle command is selected
*/
public String getReshuffleTarget() {
return reshuffleTarget;
}
public void setReshuffleTarget(String reshuffleTarget) {
this.reshuffleTarget = reshuffleTarget;
}
/**
* The message to send to the chat window when the deck is reshuffled to
* another deck
*
* @return
*/
public String getReshuffleMsgFormat() {
return reshuffleMsgFormat;
}
public void setReshuffleMsgFormat(String reshuffleMsgFormat) {
this.reshuffleMsgFormat = reshuffleMsgFormat;
}
public boolean isHotkeyOnEmpty() {
return hotkeyOnEmpty;
}
public void setHotkeyOnEmpty(boolean b) {
hotkeyOnEmpty = b;
}
@Deprecated public KeyStroke getEmptyKey() {
return emptyKey.getKeyStroke();
}
public NamedKeyStroke getNamedEmptyKey() {
return emptyKey;
}
@Deprecated public void setEmptyKey(KeyStroke k) {
emptyKey = new NamedKeyStroke(k);
}
public void setEmptyKey(NamedKeyStroke k) {
emptyKey = k;
}
public void setRestrictOption(boolean restrictOption) {
this.restrictOption = restrictOption;
}
public boolean isRestrictOption() {
return restrictOption;
}
public void setRestrictExpression(PropertyExpression restrictExpression) {
this.restrictExpression = restrictExpression;
}
public PropertyExpression getRestrictExpression() {
return restrictExpression;
}
/**
* Does the specified GamePiece meet the rules to be contained
* in this Deck.
*
* @param piece
* @return
*/
public boolean mayContain(GamePiece piece) {
if (! restrictOption || restrictExpression.isNull()) {
return true;
}
else {
return restrictExpression.accept(piece);
}
}
public String getType() {
final SequenceEncoder se = new SequenceEncoder(';');
se.append(drawOutline)
.append(ColorConfigurer.colorToString(outlineColor))
.append(String.valueOf(size.width))
.append(String.valueOf(size.height))
.append(faceDownOption)
.append(shuffleOption)
.append(String.valueOf(allowMultipleDraw))
.append(String.valueOf(allowSelectDraw))
.append(String.valueOf(reversible))
.append(reshuffleCommand)
.append(reshuffleTarget)
.append(reshuffleMsgFormat)
.append(deckName)
.append(shuffleMsgFormat)
.append(reverseMsgFormat)
.append(faceDownMsgFormat)
.append(drawFaceUp)
.append(persistable)
.append(shuffleKey)
.append(reshuffleKey)
.append(String.valueOf(maxStack))
.append(getCountExpressions())
.append(expressionCounting)
.append(getGlobalCommands())
.append(hotkeyOnEmpty)
.append(emptyKey)
.append(selectDisplayProperty.getFormat())
.append(selectSortProperty)
.append(restrictOption)
.append(restrictExpression)
.append(shuffleCommand)
.append(reverseCommand)
.append(reverseKey);
return ID + se.getValue();
}
/** Shuffle the contents of the Deck */
public Command shuffle() {
DragBuffer.getBuffer().clear();
final List<GamePiece> l = Arrays.asList(contents).subList(0, pieceCount);
Collections.shuffle(l, GameModule.getGameModule().getRNG());
return setContents(l).append(reportCommand(shuffleMsgFormat, Resources.getString("Deck.shuffle"))); //$NON-NLS-1$
}
/**
* Return an iterator of pieces to be drawn from the Deck. Normally, a random
* piece will be drawn, but if the Deck supports it, the user may have
* specified a particular set of pieces or a fixed number of pieces to select
* with the next draw.
*/
public PieceIterator drawCards() {
Iterator<GamePiece> it;
if (nextDraw != null) {
it = nextDraw.iterator();
}
else if (getPieceCount() == 0) {
it = Collections.<GamePiece>emptyList().iterator();
}
else {
int count = Math.max(dragCount, Math.min(1, getPieceCount()));
final ArrayList<GamePiece> pieces = new ArrayList<GamePiece>();
if (ALWAYS.equals(shuffleOption)) {
// Instead of shuffling the entire deck, just pick <b>count</b> random elements
final ArrayList<Integer> indices = new ArrayList<Integer>();
for (int i = 0; i < getPieceCount(); ++i) {
indices.add(i);
}
final Random rng = GameModule.getGameModule().getRNG();
while (count-- > 0 && indices.size() > 0) {
final int i = rng.nextInt(indices.size());
final int index = indices.get(i);
indices.remove(i);
final GamePiece p = getPieceAt(index);
pieces.add(p);
}
}
else {
final Iterator<GamePiece> i = getPiecesReverseIterator();
while (count-- > 0 && i.hasNext()) pieces.add(i.next());
}
it = pieces.iterator();
}
dragCount = 0;
nextDraw = null;
return new PieceIterator(it) {
public GamePiece nextPiece() {
GamePiece p = super.nextPiece();
if (faceDown) {
p.setProperty(Properties.OBSCURED_BY, NO_USER);
}
return p;
}
};
}
/** Set the contents of this Deck to a Collection of GamePieces */
protected Command setContents(Collection<GamePiece> c) {
ChangeTracker track = new ChangeTracker(this);
removeAll();
for (GamePiece child : c) {
insertChild(child, pieceCount);
}
return track.getChangeCommand();
}
/**
* Set the contents of this Deck to an Iterator of GamePieces
* @deprecated Use {@link #setContents(Collection<GamePiece>)} instead.
*/
@Deprecated
protected Command setContents(Iterator<GamePiece> it) {
ChangeTracker track = new ChangeTracker(this);
removeAll();
while (it.hasNext()) {
GamePiece child = it.next();
insertChild(child, pieceCount);
}
return track.getChangeCommand();
}
public String getState() {
final SequenceEncoder se = new SequenceEncoder(';');
se.append(getMap() == null ? "null" : getMap().getIdentifier()).append(getPosition().x).append(getPosition().y); //$NON-NLS-1$
se.append(faceDown);
final SequenceEncoder se2 = new SequenceEncoder(',');
for (Iterator<GamePiece> i = getPiecesIterator(); i.hasNext();) {
se2.append(i.next().getId());
}
if (se2.getValue() != null) {
se.append(se2.getValue());
}
return se.getValue();
}
public void setState(String state) {
final SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(state, ';');
final String mapId = st.nextToken();
setPosition(new Point(st.nextInt(0), st.nextInt(0)));
Map m = null;
if (!"null".equals(mapId)) { //$NON-NLS-1$
m = Map.getMapById(mapId);
if (m == null) {
ErrorDialog.dataError(new BadDataReport("No such map",mapId,null));
}
}
if (m != getMap()) {
if (m != null) {
m.addPiece(this);
}
else {
setMap(null);
}
}
faceDown = "true".equals(st.nextToken()); //$NON-NLS-1$
final ArrayList<GamePiece> l = new ArrayList<GamePiece>();
if (st.hasMoreTokens()) {
final SequenceEncoder.Decoder st2 =
new SequenceEncoder.Decoder(st.nextToken(), ',');
while (st2.hasMoreTokens()) {
final GamePiece p = GameModule.getGameModule()
.getGameState()
.getPieceForId(st2.nextToken());
if (p != null) {
l.add(p);
}
}
}
setContents(l);
commands = null; // Force rebuild of popup menu
}
public Command setContentsFaceDown(boolean value) {
ChangeTracker t = new ChangeTracker(this);
Command c = new NullCommand();
faceDown = value;
return t.getChangeCommand().append(c).append(reportCommand(faceDownMsgFormat, value ? Resources.getString("Deck.face_down") : Resources.getString("Deck.face_up"))); //$NON-NLS-1$ //$NON-NLS-2$
}
/** Reverse the order of the contents of the Deck */
public Command reverse() {
final ArrayList<GamePiece> list = new ArrayList<GamePiece>();
for (Iterator<GamePiece> i = getPiecesReverseIterator(); i.hasNext(); ) {
list.add(i.next());
}
return setContents(list).append(reportCommand(
reverseMsgFormat, Resources.getString("Deck.reverse"))); //$NON-NLS-1$
}
public boolean isDrawOutline() {
return drawOutline;
}
public void setOutlineColor(Color outlineColor) {
this.outlineColor = outlineColor;
}
public void setDrawOutline(boolean drawOutline) {
this.drawOutline = drawOutline;
}
public Color getOutlineColor() {
return outlineColor;
}
public boolean isFaceDown() {
return faceDown;
}
public Command pieceAdded(GamePiece p) {
return null;
}
public Command pieceRemoved(GamePiece p) {
ChangeTracker tracker = new ChangeTracker(p);
p.setProperty(Properties.OBSCURED_TO_OTHERS, Boolean.valueOf(isFaceDown() && !isDrawFaceUp()));
return tracker.getChangeCommand();
}
public void setFaceDown(boolean faceDown) {
this.faceDown = faceDown;
}
public void draw(java.awt.Graphics g, int x, int y, Component obs, double zoom) {
int count = Math.min(getPieceCount(), maxStack);
GamePiece top = (nextDraw != null && nextDraw.size() > 0) ?
nextDraw.get(0) : topPiece();
if (top != null) {
Object owner = top.getProperty(Properties.OBSCURED_BY);
top.setProperty(Properties.OBSCURED_BY, faceDown ? NO_USER : null);
Color blankColor = getBlankColor();
Rectangle r = top.getShape().getBounds();
r.setLocation(x + (int) (zoom * (r.x)), y + (int) (zoom * (r.y)));
r.setSize((int) (zoom * r.width), (int) (zoom * r.height));
for (int i = 0; i < count - 1; ++i) {
if (blankColor != null) {
g.setColor(blankColor);
g.fillRect(r.x + (int) (zoom * 2 * i), r.y - (int) (zoom * 2 * i), r.width, r.height);
g.setColor(Color.black);
g.drawRect(r.x + (int) (zoom * 2 * i), r.y - (int) (zoom * 2 * i), r.width, r.height);
}
else if (faceDown) {
top.draw(g, x + (int) (zoom * 2 * i), y - (int) (zoom * 2 * i), obs, zoom);
}
else {
getPieceAt(count - i - 1).draw(g, x + (int) (zoom * 2 * i), y - (int) (zoom * 2 * i), obs, zoom);
}
}
top.draw(g, x + (int) (zoom * 2 * (count - 1)), y - (int) (zoom * 2 * (count - 1)), obs, zoom);
top.setProperty(Properties.OBSCURED_BY, owner);
}
else {
if (drawOutline) {
Rectangle r = boundingBox();
r.setLocation(x + (int) (zoom * r.x), y + (int) (zoom * r.y));
r.setSize((int) (zoom * r.width), (int) (zoom * r.height));
g.setColor(outlineColor);
g.drawRect(r.x, r.y, r.width, r.height);
}
}
}
/**
* The color used to draw boxes representing cards underneath the top one. If
* null, then draw each card normally for face-up decks, and duplicate the top
* card for face-down decks
*
* @return
*/
protected Color getBlankColor() {
Color c = Color.white;
if (getMap() != null) {
c = getMap().getStackMetrics().getBlankColor();
}
return c;
}
public StackMetrics getStackMetrics() {
return deckStackMetrics;
}
public Rectangle boundingBox() {
GamePiece top = topPiece();
Dimension d = top == null ? size : top.getShape().getBounds().getSize();
Rectangle r = new Rectangle(new Point(), d);
r.translate(-r.width / 2, -r.height / 2);
for (int i=0,n=getMaximumVisiblePieceCount();i<n;++i) {
r.y -= 2;
r.height += 2;
r.width += 2;
}
return r;
}
public Shape getShape() {
return boundingBox();
}
public Object getProperty(Object key) {
Object value = null;
if (Properties.NO_STACK.equals(key)) {
value = Boolean.TRUE;
}
else if (Properties.KEY_COMMANDS.equals(key)) {
value = getKeyCommands();
}
return value;
}
protected KeyCommand[] getKeyCommands() {
if (commands == null) {
ArrayList<KeyCommand> l = new ArrayList<KeyCommand>();
KeyCommand c = null;
if (USE_MENU.equals(shuffleOption)) {
c = new KeyCommand(shuffleCommand, getShuffleKey(), this) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
GameModule.getGameModule().sendAndLog(shuffle());
repaintMap();
}
};
l.add(c);
}
if (reshuffleCommand.length() > 0) {
c = new KeyCommand(reshuffleCommand, getReshuffleKey(), this) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent evt) {
GameModule.getGameModule().sendAndLog(sendToDeck());
repaintMap();
}
};
l.add(c);
}
if (USE_MENU.equals(faceDownOption)) {
KeyCommand faceDownAction = new KeyCommand(faceDown ? Resources.getString("Deck.face_up") : Resources.getString("Deck.face_down"), NamedKeyStroke.NULL_KEYSTROKE, this) { //$NON-NLS-1$ //$NON-NLS-2$
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
final Command c = setContentsFaceDown(!faceDown);
GameModule.getGameModule().sendAndLog(c);
repaintMap();
}
};
l.add(faceDownAction);
}
if (reversible) {
c = new KeyCommand(reverseCommand, NamedKeyStroke.NULL_KEYSTROKE, this) { //$NON-NLS-1$
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
final Command c = reverse();
GameModule.getGameModule().sendAndLog(c);
repaintMap();
}
};
l.add(c);
}
if (allowMultipleDraw) {
c = new KeyCommand(Resources.getString("Deck.draw_multiple"), NamedKeyStroke.NULL_KEYSTROKE, this) { //$NON-NLS-1$
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
promptForDragCount();
}
};
l.add(c);
}
if (allowSelectDraw) {
c = new KeyCommand(Resources.getString("Deck.draw_specific"), NamedKeyStroke.NULL_KEYSTROKE, this) { //$NON-NLS-1$
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
promptForNextDraw();
repaintMap();
}
};
l.add(c);
}
if (persistable) {
c = new KeyCommand(Resources.getString(Resources.SAVE), NamedKeyStroke.NULL_KEYSTROKE, this) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
GameModule.getGameModule().sendAndLog(saveDeck());
repaintMap();
}
};
l.add(c);
c = new KeyCommand(Resources.getString(Resources.LOAD), NamedKeyStroke.NULL_KEYSTROKE, this) {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
GameModule.getGameModule().sendAndLog(loadDeck());
repaintMap();
}
};
l.add(c);
}
for (DeckGlobalKeyCommand cmd : globalCommands) {
l.add(cmd.getKeyCommand(this));
}
commands = l.toArray(new KeyCommand[l.size()]);
}
for (int i = 0; i < commands.length; ++i) {
if (Resources.getString("Deck.face_up").equals(commands[i].getValue(Action.NAME)) && !faceDown) { //$NON-NLS-1$
commands[i].putValue(Action.NAME, Resources.getString("Deck.face_down")); //$NON-NLS-1$
}
else if (Resources.getString("Deck.face_down").equals(commands[i].getValue(Action.NAME)) && faceDown) { //$NON-NLS-1$
commands[i].putValue(Action.NAME, Resources.getString("Deck.face_up")); //$NON-NLS-1$
}
}
return commands;
}
/*
* Format command report as per module designers setup.
*/
protected Command reportCommand(String format, String commandName) {
Command c = null;
FormattedString reportFormat = new FormattedString(format);
reportFormat.setProperty(DrawPile.DECK_NAME, getLocalizedDeckName());
reportFormat.setProperty(DrawPile.COMMAND_NAME, commandName);
String rep = reportFormat.getLocalizedText();
if (rep.length() > 0) {
c = new Chatter.DisplayText(GameModule.getGameModule().getChatter(), "* " + rep); //$NON-NLS-1$
c.execute();
}
return c;
}
public void promptForDragCount() {
while (true) {
final String s = JOptionPane.showInputDialog(
Resources.getString("Deck.enter_the_number")); //$NON-NLS-1$
if (s != null) {
try {
dragCount = Integer.parseInt(s);
dragCount = Math.min(dragCount, getPieceCount());
if (dragCount >= 0) break;
}
catch (NumberFormatException ex) {
// Ignore if user doesn't enter a number
}
}
else {
break;
}
}
}
protected void promptForNextDraw() {
final JDialog d = new JDialog((Frame) SwingUtilities.getAncestorOfClass(Frame.class, map.getView()), true);
d.setTitle(Resources.getString("Deck.draw")); //$NON-NLS-1$
d.setLayout(new BoxLayout(d.getContentPane(), BoxLayout.Y_AXIS));
class AvailablePiece implements Comparable<AvailablePiece> {
private GamePiece piece;
public AvailablePiece(GamePiece piece) {
this.piece = piece;
}
public int compareTo(AvailablePiece other) {
if (other == null) return 1;
final String otherProperty =
(String) other.piece.getProperty(selectSortProperty);
if (otherProperty == null) return 1;
final String myProperty =
(String) piece.getProperty(selectSortProperty);
if (myProperty == null) return -1;
return -otherProperty.compareTo(myProperty);
}
public String toString() {
return selectDisplayProperty.getText(piece);
}
public boolean equals(Object o) {
if (! (o instanceof AvailablePiece)) return false;
return ((AvailablePiece)o).piece.equals(piece);
}
}
final AvailablePiece[] pieces = new AvailablePiece[getPieceCount()];
for (int i = 0; i < pieces.length; ++i) {
pieces[pieces.length - i - 1] = new AvailablePiece(getPieceAt(i));
}
if (selectSortProperty != null && selectSortProperty.length() > 0) {
Arrays.sort(pieces);
}
final JList list = new JList(pieces);
list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
d.add(new ScrollPane(list));
d.add(new JLabel(Resources.getString("Deck.select_cards"))); //$NON-NLS-1$
d.add(new JLabel(Resources.getString("Deck.then_click"))); //$NON-NLS-1$
Box box = Box.createHorizontalBox();
JButton b = new JButton(Resources.getString(Resources.OK));
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
int[] selection = list.getSelectedIndices();
if (selection.length > 0) {
nextDraw = new ArrayList<GamePiece>();
for (int i = 0; i < selection.length; ++i) {
nextDraw.add(pieces[selection[i]].piece);
}
}
else {
nextDraw = null;
}
d.dispose();
}
});
box.add(b);
b = new JButton(Resources.getString(Resources.CANCEL));
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
d.dispose();
}
});
box.add(b);
d.add(box);
d.pack();
d.setLocationRelativeTo(d.getOwner());
d.setVisible(true);
}
/**
* Combine the contents of this Deck with the contents of the deck specified
* by {@link #reshuffleTarget}
*/
public Command sendToDeck() {
Command c = null;
nextDraw = null;
DrawPile target = DrawPile.findDrawPile(reshuffleTarget);
if (target != null) {
if (reshuffleMsgFormat.length() > 0) {
c = reportCommand(reshuffleMsgFormat, reshuffleCommand);
if (c == null) {
c = new NullCommand();
}
}
else {
c = new NullCommand();
}
// move cards to deck
int cnt = getPieceCount() - 1;
for (int i = cnt; i >= 0; i--) {
c.append(target.addToContents(getPieceAt(i)));
}
}
return c;
}
public boolean isExpanded() {
return false;
}
/** Return true if this deck can be saved to and loaded from a file on disk */
public boolean isPersistable() {
return persistable;
}
public void setPersistable(boolean persistable) {
this.persistable = persistable;
}
private File getSaveFileName() {
FileChooser fc = GameModule.getGameModule().getFileChooser();
File sf = fc.getSelectedFile();
if (sf != null) {
String name = sf.getPath();
if (name != null) {
int index = name.lastIndexOf('.');
if (index > 0) {
name = name.substring(0, index) + ".sav"; //$NON-NLS-1$
fc.setSelectedFile(new File(name));
}
}
}
if (fc.showSaveDialog(map.getView()) != FileChooser.APPROVE_OPTION)
return null;
File outputFile = fc.getSelectedFile();
if (outputFile != null &&
outputFile.exists() &&
shouldConfirmOverwrite() &&
JOptionPane.NO_OPTION ==
JOptionPane.showConfirmDialog(GameModule.getGameModule().getFrame(),
Resources.getString("Deck.overwrite", outputFile.getName()), Resources.getString("Deck.file_exists"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
JOptionPane.YES_NO_OPTION)) {
outputFile = null;
}
return outputFile;
}
private boolean shouldConfirmOverwrite() {
return System.getProperty("os.name").trim().equalsIgnoreCase("linux"); //$NON-NLS-1$ //$NON-NLS-2$
}
private Command saveDeck() {
final Command c = new NullCommand();
GameModule.getGameModule().warn(Resources.getString("Deck.saving_deck")); //$NON-NLS-1$
final File saveFile = getSaveFileName();
try {
if (saveFile != null) {
saveDeck(saveFile);
GameModule.getGameModule().warn(Resources.getString("Deck.deck_saved")); //$NON-NLS-1$
}
else {
GameModule.getGameModule().warn(Resources.getString("Deck.save_canceled")); //$NON-NLS-1$
}
}
catch (IOException e) {
WriteErrorDialog.error(e, saveFile);
}
return c;
}
public void saveDeck(File f) throws IOException {
Command comm = new LoadDeckCommand(null);
for (Iterator<GamePiece> i = getPiecesIterator(); i.hasNext();) {
final GamePiece p = i.next();
p.setMap(null);
comm = comm.append(new AddPiece(p));
}
BufferedWriter out = null;
try {
out = new BufferedWriter(new FileWriter(f));
GameModule.getGameModule().addCommandEncoder(commandEncoder);
out.write(GameModule.getGameModule().encode(comm));
GameModule.getGameModule().removeCommandEncoder(commandEncoder);
out.close();
}
finally {
IOUtils.closeQuietly(out);
}
}
private File getLoadFileName() {
FileChooser fc = GameModule.getGameModule().getFileChooser();
fc.selectDotSavFile();
if (fc.showOpenDialog(map.getView()) != FileChooser.APPROVE_OPTION)
return null;
return fc.getSelectedFile();
}
private Command loadDeck() {
Command c = new NullCommand();
GameModule.getGameModule().warn(Resources.getString("Deck.loading_deck")); //$NON-NLS-1$
final File loadFile = getLoadFileName();
try {
if (loadFile != null) {
c = loadDeck(loadFile);
GameModule.getGameModule().warn(Resources.getString("Deck.deck_loaded")); //$NON-NLS-1$
}
else {
GameModule.getGameModule().warn(Resources.getString("Deck.load_canceled")); //$NON-NLS-1$
}
}
catch (IOException e) {
ReadErrorDialog.error(e, loadFile);
}
return c;
}
public Command loadDeck(File f) throws IOException {
BufferedReader in = null;
String ds = null;
try {
in = new BufferedReader(new FileReader(f));
ds = IOUtils.toString(in);
in.close();
}
finally {
IOUtils.closeQuietly(in);
}
GameModule.getGameModule().addCommandEncoder(commandEncoder);
Command c = GameModule.getGameModule().decode(ds);
GameModule.getGameModule().removeCommandEncoder(commandEncoder);
if (c instanceof LoadDeckCommand) {
/*
* A LoadDeckCommand doesn't specify the deck to be changed (since the
* saved deck can be loaded into any deck) so the Command we send to other
* players is a ChangePiece command for this deck, which we need to place
* after the AddPiece commands for the contents
*/
final ChangeTracker t = new ChangeTracker(this);
c.execute();
final Command[] sub = c.getSubCommands();
c = new NullCommand();
for (int i = 0; i < sub.length; ++i) {
c.append(sub[i]);
}
c.append(t.getChangeCommand());
updateCountsAll();
}
else {
GameModule.getGameModule().warn(Resources.getString("Deck.not_a_saved_deck", f.getName())); //$NON-NLS-1$
c = null;
}
return c;
}
/**
* Command to set the contents of this deck from a saved file. The contents
* are saved with whatever id's the pieces have in the game when the deck was
* saved, but new copies are created when the deck is re-loaded.
*
* @author rkinney
*
*/
protected static class LoadDeckCommand extends Command {
public static final String PREFIX = "DECK\t"; //$NON-NLS-1$
private Deck target;
public LoadDeckCommand(Deck target) {
this.target = target;
}
protected void executeCommand() {
target.removeAll();
Command[] sub = getSubCommands();
for (int i = 0; i < sub.length; i++) {
if (sub[i] instanceof AddPiece) {
GamePiece p = ((AddPiece) sub[i]).getTarget();
// We set the id to null so that the piece will get a new id
// when the AddPiece command executes
p.setId(null);
target.add(p);
}
}
}
public String getTargetId() {
return target == null ? "" : target.getId(); //$NON-NLS-1$
}
protected Command myUndoCommand() {
return null;
}
}
/**
* An object that parses expression strings from the config window
*/
public static class CountExpression {
private String fullstring;
private String name;
private String expression;
public CountExpression(String expressionString) {
String[] split = expressionString.split("\\s*:\\s*",2); //$NON-NLS-1$
if (split.length == 2) {
name = split[0];
expression = split[1];
fullstring = expressionString;
}
}
public String getName() {
return name;
}
public String getExpression() {
return expression;
}
public String getFullString() {
return fullstring;
}
}
/**
* Return the number of cards to be returned by next call to
* {@link #drawCards()}.
*/
public int getDragCount() {
return dragCount;
}
/**
* Set the number of cards to be returned by next call to
* {@link #drawCards()}.
*
* @param dragCount
*/
public void setDragCount(int dragCount) {
this.dragCount = dragCount;
}
public void setSelectDisplayProperty(String promptDisplayProperty) {
this.selectDisplayProperty.setFormat(promptDisplayProperty);
}
public void setSelectSortProperty(String promptSortProperty) {
this.selectSortProperty = promptSortProperty;
}
public String getSelectDisplayProperty() {
return selectDisplayProperty.getFormat();
}
public String getSelectSortProperty() {
return selectSortProperty;
}
protected void repaintMap() {
if (map != null) {
map.repaint();
}
}
}